Add a bunch of unit tests. Apply more strict linting rules and reformat /...

Add a bunch of unit tests. Apply more strict linting rules and reformat / refactor the code base. Add lint test to deployment config.
parent e88eafb4
Pipeline #15455 failed with stages
in 1 minute and 17 seconds
......@@ -2,7 +2,6 @@ stages:
- install
- test
- build
- compress
- deploy
install:
......@@ -21,6 +20,7 @@ test:
tags:
- nodejs
script:
- node_modules/tslint/bin/tslint -c tslint.json -p tsconfig.json
- npm test
build:
......
import { browser, by, element } from 'protractor';
import { promise } from 'selenium-webdriver';
export class FrontendPage {
navigateTo() {
public navigateTo(): promise.Promise<any> {
return browser.get('/');
}
getParagraphText() {
public getParagraphText(): promise.Promise<string> {
return element(by.css('app-root h1')).getText();
}
}
<div id="quizSummaryHeader" class="quizSummary mt-md-3 px-md-0">
<div id="quizSummaryHeader"
class="quizSummary mt-md-3 px-md-0">
<div class="row no-gutters">
......@@ -8,7 +8,8 @@
[attr.data-intro]="'component.quiz_summary.description.header' | translate"
[attr.aria-label]="'component.quiz_summary.description.header' | translate">
<h5 class="text-center">{{'component.quiz_summary.quiz_name' | translate}}</h5>
<p class="mb-0 d-flex align-self-center align-items-end h-100 text-bold" role="listitem">
<p class="mb-0 d-flex align-self-center align-items-end h-100 text-bold"
role="listitem">
<span>{{questionGroupItem.hashtag}}</span>
</p>
</div>
......@@ -19,7 +20,8 @@
[attr.data-intro]="'component.quiz_summary.description.question_group_validation' | translate"
[attr.aria-label]="'component.quiz_summary.description.question_group_validation' | translate">
<h5 class="text-center">{{'component.quiz_summary.validation_question_group' | translate}}</h5>
<p class="mb-0 d-flex align-self-center align-items-end h-100 text-bold" role="listitem">
<p class="mb-0 d-flex align-self-center align-items-end h-100 text-bold"
role="listitem">
<span [class.text-success]="questionGroupItem.isValid()"
[class.text-danger]="!questionGroupItem.isValid()">
{{('component.quiz_summary.question_group_' + (questionGroupItem.isValid() ? 'successful' : 'failed')) | translate}}
......@@ -34,7 +36,8 @@
[attr.data-intro]="'component.quiz_summary.description.session_url' | translate"
[attr.aria-label]="'component.quiz_summary.description.session_url' | translate">
<h5 class="text-center">{{'component.quiz_summary.quiz_url' | translate}}</h5>
<p class="mb-0 text-wrap" role="listitem">
<p class="mb-0 text-wrap"
role="listitem">
<span>{{document.location.origin + '/quiz/' + questionGroupItem.hashtag}}</span>
</p>
</div>
......@@ -44,7 +47,8 @@
class="col-6 col-md-12 mt-sm-2 mb-2 mb-sm-0">
<div class="element-wrapper bg-white px-2 py-2 mb-2 h-100">
<h5 class="text-center">{{'component.quiz_summary.isRestrictingRudeNicks' | translate}}</h5>
<p class="mb-0" role="listitem">
<p class="mb-0"
role="listitem">
<span>{{('global.' + questionGroupItem.sessionConfig.nicks.blockIllegalNicks) | translate}}</span>
</p>
</div>
......@@ -56,7 +60,8 @@
[attr.data-intro]="'component.quiz_summary.description.restrict_to_cas' | translate"
[attr.aria-label]="'component.quiz_summary.description.restrict_to_cas' | translate">
<h5 class="text-center">{{'component.quiz_summary.isRestrictingToCAS' | translate}}</h5>
<p class="mb-0" role="listitem">
<p class="mb-0"
role="listitem">
<span>{{('global.' + questionGroupItem.sessionConfig.nicks.restrictToCasLogin) | translate}}</span>
</p>
</div>
......
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateCompiler, TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler';
import { createTranslateLoader } from '../../../lib/translation.factory';
import { ActiveQuestionGroupMockService } from '../../service/active-question-group/active-question-group.mock.service';
import { ActiveQuestionGroupService } from '../../service/active-question-group/active-question-group.service';
import { TrackingMockService } from '../../service/tracking/tracking.mock.service';
import { TrackingService } from '../../service/tracking/tracking.service';
import { AdditionalDataComponent } from './additional-data.component';
import {TrackingMockService} from '../../service/tracking.mock.service';
import {ActiveQuestionGroupMockService} from '../../service/active-question-group.mock.service';
import {TranslateCompiler, TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {createTranslateLoader} from '../../../lib/translation.factory';
import {TranslateMessageFormatCompiler} from 'ngx-translate-messageformat-compiler';
import {RouterTestingModule} from '@angular/router/testing';
import {ActiveQuestionGroupService} from '../../service/active-question-group.service';
import {TrackingService} from '../../service/tracking.service';
describe('AdditionalDataComponent', () => {
let component: AdditionalDataComponent;
......@@ -20,25 +21,25 @@ describe('AdditionalDataComponent', () => {
imports: [
RouterTestingModule,
HttpClientModule,
HttpClientTestingModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: (createTranslateLoader),
deps: [HttpClient]
deps: [HttpClient],
},
compiler: {
provide: TranslateCompiler,
useClass: TranslateMessageFormatCompiler
}
useClass: TranslateMessageFormatCompiler,
},
}),
],
providers: [
{provide: ActiveQuestionGroupService, useClass: ActiveQuestionGroupMockService},
{provide: TrackingService, useClass: TrackingMockService}
{ provide: ActiveQuestionGroupService, useClass: ActiveQuestionGroupMockService },
{ provide: TrackingService, useClass: TrackingMockService },
],
declarations: [ AdditionalDataComponent ]
})
.compileComponents();
declarations: [AdditionalDataComponent],
}).compileComponents();
}));
beforeEach(() => {
......@@ -54,4 +55,12 @@ describe('AdditionalDataComponent', () => {
it('should contain a TYPE definition', async(() => {
expect(AdditionalDataComponent.TYPE).toEqual('AdditionalDataComponent');
}));
it('#switchShowMoreOrLess', (inject([HttpClient, HttpTestingController],
(http: HttpClient, backend: HttpTestingController) => {
const baseState = window.innerWidth >= 768;
expect(component.isShowingMore).toEqual(baseState);
component.switchShowMoreOrLess();
expect(component.isShowingMore).toEqual(!baseState);
})));
});
import {Component, HostListener, Inject, OnInit} from '@angular/core';
import {TranslateService} from '@ngx-translate/core';
import {ActiveQuestionGroupService} from '../../service/active-question-group.service';
import {IQuestionGroup} from 'arsnova-click-v2-types/src/questions/interfaces';
import {TrackingService} from '../../service/tracking.service';
import {DOCUMENT} from '@angular/common';
import { DOCUMENT } from '@angular/common';
import { Component, HostListener, Inject } from '@angular/core';
import { IQuestionGroup } from 'arsnova-click-v2-types/src/questions/interfaces';
import { ActiveQuestionGroupService } from '../../service/active-question-group/active-question-group.service';
import { TrackingService } from '../../service/tracking/tracking.service';
@Component({
selector: 'additional-data',
selector: 'app-additional-data',
templateUrl: './additional-data.component.html',
styleUrls: ['./additional-data.component.scss']
styleUrls: ['./additional-data.component.scss'],
})
export class AdditionalDataComponent implements OnInit {
export class AdditionalDataComponent {
public static TYPE = 'AdditionalDataComponent';
public readonly questionGroupItem: IQuestionGroup;
private _isShowingMore: boolean = window.innerWidth >= 768;
set isShowingMore(value: boolean) {
this._isShowingMore = value;
}
get isShowingMore(): boolean {
return this._isShowingMore;
}
readonly questionGroupItem: IQuestionGroup;
private _isShowingMore: boolean = window.innerWidth >= 768;
set isShowingMore(value: boolean) {
this._isShowingMore = value;
}
constructor(
@Inject(DOCUMENT) readonly document,
......@@ -31,10 +31,7 @@ export class AdditionalDataComponent implements OnInit {
this.questionGroupItem = activeQuestionGroupService.activeQuestionGroup;
}
ngOnInit() {
}
switchShowMoreOrLess() {
public switchShowMoreOrLess(): void {
if (this.isShowingMore) {
this.trackingService.trackClickEvent({
action: AdditionalDataComponent.TYPE,
......@@ -50,7 +47,7 @@ export class AdditionalDataComponent implements OnInit {
}
@HostListener('window:resize', ['$event'])
onResize() {
private onResize(): void {
this.isShowingMore = window.innerWidth >= 768;
}
......
......@@ -2,7 +2,12 @@
id="navbar-footer-container"
class="nav navbar-footer flex-nowrap flex-sm-column justify-content-sm-start p-1"
role="navigation">
<input class="hidden" id="upload-session" type="file" (change)="fileChange($event)" accept="application/json" multiple="multiple"/>
<input class="hidden"
id="upload-session"
type="file"
(change)="fileChange($event)"
accept="application/json"
multiple="multiple"/>
<div *ngFor="let elem of footerElements"
class="footerElement px-2 py-2 px-sm-1 py-sm-2 pointer d-flex align-items-center justify-content-center justify-content-sm-start"
[class.success]="elem.selectable && elem.isActive"
......@@ -12,13 +17,18 @@
[title]="elem.textName | translate"
[attr.data-intro]="elem.showIntro ? (('region.footer.footer_bar.description.' + elem.id) | translate) : null"
(click)="toggleSetting(elem)">
<p class="footerElemIcon mb-0"><i [class]="elem.iconClass" aria-hidden="true"></i></p>
<p class="footerElemText text-truncate text-left mb-0 ml-2 d-md-block"><span [class]="elem.textClass" [innerHTML]="elem.textName | translate"></span></p>
<p class="footerElemIcon mb-0"><i [class]="elem.iconClass"
aria-hidden="true"></i></p>
<p class="footerElemText text-truncate text-left mb-0 ml-2 d-md-block"><span [class]="elem.textClass"
[innerHTML]="elem.textName | translate"></span></p>
</div>
</nav>
<span class="before d-sm-none d-none position-absolute justify-content-center align-items-center h-100"
<span class="d-flex d-sm-none position-absolute justify-content-center align-items-center h-100"
id="footer-move-left"
[style.visibility]="footerElemIndex === 1 ? 'hidden' : 'visible'"
(click)="moveLeft()">&#x21E6;</span>
<span class="after d-sm-none d-flex position-absolute justify-content-center align-items-center h-100"
*ngIf="hasRightScrollElement"
<span class="d-flex d-sm-none position-absolute justify-content-center align-items-center h-100"
id="footer-move-right"
[style.visibility]="hideRight() ? 'hidden' : 'visible'"
(click)="moveRight()">&#x21E8;</span>
\ No newline at end of file
......@@ -9,8 +9,8 @@
}
}
span.before,
span.after {
#footer-move-left,
#footer-move-right {
width: 50px;
top: 0;
color: #fff;
......@@ -18,12 +18,12 @@ span.after {
z-index: 1050;
}
span.before {
background: linear-gradient(to right, #000, rgba(0,0,0, 0.2));
#footer-move-left {
border-right: 1px dashed;
}
span.after {
background: linear-gradient(to left, #000, rgba(0,0,0, 0.2));
#footer-move-right {
border-left: 1px dashed;
right: 0;
}
......
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateCompiler, TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler';
import { FooterbarElement } from '../../../lib/footerbar-element/footerbar-element';
import { createTranslateLoader } from '../../../lib/translation.factory';
import { ActiveQuestionGroupMockService } from '../../service/active-question-group/active-question-group.mock.service';
import { ActiveQuestionGroupService } from '../../service/active-question-group/active-question-group.service';
import { ConnectionMockService } from '../../service/connection/connection.mock.service';
import { ConnectionService } from '../../service/connection/connection.service';
import { CurrentQuizMockService } from '../../service/current-quiz/current-quiz.mock.service';
import { CurrentQuizService } from '../../service/current-quiz/current-quiz.service';
import { FileUploadService } from '../../service/file-upload/file-upload.service';
import { FooterBarService } from '../../service/footer-bar/footer-bar.service';
import { SettingsService } from '../../service/settings/settings.service';
import { SharedService } from '../../service/shared/shared.service';
import { TrackingMockService } from '../../service/tracking/tracking.mock.service';
import { TrackingService } from '../../service/tracking/tracking.service';
import { WebsocketMockService } from '../../service/websocket/websocket.mock.service';
import { WebsocketService } from '../../service/websocket/websocket.service';
import {FooterBarComponent} from './footer-bar.component';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {TranslateCompiler, TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {TranslateMessageFormatCompiler} from 'ngx-translate-messageformat-compiler';
import {createTranslateLoader} from '../../../lib/translation.factory';
import {RouterTestingModule} from '@angular/router/testing';
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
import {ActiveQuestionGroupService} from '../../service/active-question-group.service';
import {SettingsService} from '../../service/settings.service';
import {FooterBarService} from '../../service/footer-bar.service';
import {FileUploadService} from '../../service/file-upload.service';
import {WebsocketService} from '../../service/websocket.service';
import {TrackingService} from '../../service/tracking.service';
import {SharedService} from '../../service/shared.service';
import {ConnectionService} from '../../service/connection.service';
import {CurrentQuizService} from '../../service/current-quiz.service';
import {WebsocketMockService} from '../../service/websocket.mock.service';
import {CurrentQuizMockService} from '../../service/current-quiz.mock.service';
import {ConnectionMockService} from '../../service/connection.mock.service';
import {ActiveQuestionGroupMockService} from '../../service/active-question-group.mock.service';
import {TrackingMockService} from '../../service/tracking.mock.service';
import { FooterBarComponent } from './footer-bar.component';
describe('FooterBarComponent', () => {
let component: FooterBarComponent;
......@@ -31,33 +33,34 @@ describe('FooterBarComponent', () => {
imports: [
RouterTestingModule,
HttpClientModule,
HttpClientTestingModule,
NgbModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: (createTranslateLoader),
deps: [HttpClient]
deps: [HttpClient],
},
compiler: {
provide: TranslateCompiler,
useClass: TranslateMessageFormatCompiler
}
useClass: TranslateMessageFormatCompiler,
},
}),
],
providers: [
FooterBarService,
SharedService,
{provide: CurrentQuizService, useClass: CurrentQuizMockService},
{ provide: CurrentQuizService, useClass: CurrentQuizMockService },
SettingsService,
{provide: ConnectionService, useClass: ConnectionMockService},
{provide: WebsocketService, useClass: WebsocketMockService},
{provide: TrackingService, useClass: TrackingMockService},
{ provide: ConnectionService, useClass: ConnectionMockService },
{ provide: WebsocketService, useClass: WebsocketMockService },
{ provide: TrackingService, useClass: TrackingMockService },
FileUploadService,
{provide: ActiveQuestionGroupService, useClass: ActiveQuestionGroupMockService},
{ provide: ActiveQuestionGroupService, useClass: ActiveQuestionGroupMockService },
],
declarations: [
FooterBarComponent
]
FooterBarComponent,
],
}).compileComponents();
}));
......@@ -74,4 +77,52 @@ describe('FooterBarComponent', () => {
it('should contain a TYPE definition', async(() => {
expect(FooterBarComponent.TYPE).toEqual('FooterBarComponent');
}));
it('#getLinkTarget', (inject([HttpClient, HttpTestingController, FooterBarService],
(http: HttpClient, backend: HttpTestingController, footerBarService: FooterBarService) => {
expect(component.getLinkTarget(footerBarService.footerElemAbout)).toEqual(jasmine.arrayContaining(['info', 'about']));
})));
it('#toggleSetting', (inject([HttpClient, HttpTestingController, FooterBarService, TrackingService],
(http: HttpClient, backend: HttpTestingController, footerBarService: FooterBarService, trackingService: TrackingService) => {
const elem = footerBarService.footerElemAbout;
spyOn(elem, 'onClickCallback').and.callFake(() => {});
spyOn(trackingService, 'trackClickEvent').and.callFake(() => {});
component.toggleSetting(elem);
expect(elem.onClickCallback).toHaveBeenCalled();
expect(trackingService.trackClickEvent).toHaveBeenCalled();
})));
it('#fileChange', (inject([HttpClient, HttpTestingController, FileUploadService],
(http: HttpClient, backend: HttpTestingController, fileUploadService: FileUploadService) => {
spyOn(fileUploadService, 'uploadFile').and.callFake(() => {});
component.fileChange({ target: { files: [{ name: 'testFile' }] } });
expect(fileUploadService.uploadFile).toHaveBeenCalled();
})));
it('#moveLeft', (inject([HttpClient, HttpTestingController, FooterBarService],
(http: HttpClient, backend: HttpTestingController, footerBarService: FooterBarService) => {
component.footerElements = [
...Object.keys(footerBarService).map(t => footerBarService[t] instanceof FooterbarElement ? footerBarService[t] : false),
];
component.footerElemIndex = 2;
component.moveLeft();
expect(component.footerElemIndex).toEqual(1);
component.moveLeft();
expect(component.footerElemIndex).toEqual(1);
})));
it('#moveRight', (inject([HttpClient, HttpTestingController, FooterBarService],
(http: HttpClient, backend: HttpTestingController, footerBarService: FooterBarService) => {
component.footerElements = [
...Object.keys(footerBarService).map(t => footerBarService[t] instanceof FooterbarElement ? footerBarService[t] : false),
];
component.footerElemIndex = 1;
component.moveRight();
expect(component.footerElemIndex).toEqual(2);
for (let i = 0; i < component.footerElements.length; i++) {
component.moveRight();
}
expect(component.footerElemIndex).toEqual(component.footerElements.length - 1);
})));
});
import {Component, Inject, Input, OnDestroy, OnInit, PLATFORM_ID} from '@angular/core';
import {FooterbarElement, FooterBarService} from '../../service/footer-bar.service';
import {Router} from '@angular/router';
import {Subscription} from 'rxjs';
import {FileUploadService} from '../../service/file-upload.service';
import {CurrentQuizService} from '../../service/current-quiz.service';
import {TrackingService} from '../../service/tracking.service';
import {isPlatformBrowser} from '@angular/common';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { Component, Inject, Input, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { IFooterBarElement } from '../../../lib/footerbar-element/interfaces';
import { CurrentQuizService } from '../../service/current-quiz/current-quiz.service';
import { FileUploadService } from '../../service/file-upload/file-upload.service';
import { FooterBarService } from '../../service/footer-bar/footer-bar.service';
import { TrackingService } from '../../service/tracking/tracking.service';
@Component({
selector: 'app-footer-bar',
templateUrl: './footer-bar.component.html',
styleUrls: ['./footer-bar.component.scss']
styleUrls: ['./footer-bar.component.scss'],
})
export class FooterBarComponent implements OnInit, OnDestroy {
public static TYPE = 'FooterBarComponent';
private _footerElements: Array<IFooterBarElement> = [];
get footerElements(): Array<IFooterBarElement> {
return this._footerElements;
}
@Input()
set footerElements(value: Array<FooterbarElement>) {
set footerElements(value: Array<IFooterBarElement>) {
this.hasRightScrollElement = value.length > 1;
this._footerElements = value;
}
get hasRightScrollElement(): boolean {
return this._hasRightScrollElement;
}
set hasRightScrollElement(value: boolean) {
this._hasRightScrollElement = value;
}
private _footerElemIndex = 1;
get footerElemIndex(): number {
return this._footerElemIndex;
}
......@@ -32,17 +37,18 @@ export class FooterBarComponent implements OnInit, OnDestroy {
set footerElemIndex(value: number) {
this._footerElemIndex = value;
}
public static TYPE = 'FooterBarComponent';
get footerElements(): Array<FooterbarElement> {
return this._footerElements;
private _hasRightScrollElement = false;
get hasRightScrollElement(): boolean {
return this._hasRightScrollElement;
}
private _footerElements: Array<FooterbarElement> = [];
set hasRightScrollElement(value: boolean) {
this._hasRightScrollElement = value;
}
private _routerSubscription: Subscription;
private _footerElemIndex = 1;
private _hasRightScrollElement = false;
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
......@@ -50,17 +56,16 @@ export class FooterBarComponent implements OnInit, OnDestroy {
private footerBarService: FooterBarService,
private currentQuizService: CurrentQuizService,
private trackingService: TrackingService,
private fileUploadService: FileUploadService
private fileUploadService: FileUploadService,
) {
}
ngOnInit() {
public ngOnInit(): void {
this._routerSubscription = this.router.events.subscribe((val) => {
if (isPlatformBrowser(this.platformId)) {
const navbarFooter = document.getElementById('navbar-footer-container');
if (navbarFooter) {
navbarFooter.scrollLeft = 0;
this.hideElement('left');
}
}
this.footerElemIndex = 1;
......@@ -70,15 +75,15 @@ export class FooterBarComponent implements OnInit, OnDestroy {
});
}
ngOnDestroy() {
public ngOnDestroy(): void {
this._routerSubscription.unsubscribe();
}
getLinkTarget(elem: FooterbarElement): void {
public getLinkTarget(elem: IFooterBarElement): Function | string {
return typeof elem.linkTarget === 'function' ? elem.linkTarget(elem) : elem.linkTarget;
}
toggleSetting(elem: FooterbarElement): void {
public toggleSetting(elem: IFooterBarElement): void {
this.currentQuizService.toggleSetting(elem);
elem.onClickCallback(elem);
this.trackingService.trackClickEvent({
......@@ -86,8 +91,8 @@ export class FooterBarComponent implements OnInit, OnDestroy {
label: `footer-${elem.id}`,
customDimensions: {
dimension1: elem.selectable,
dimension2: elem.isActive
}
dimension2: elem.isActive,
},
});
}
......@@ -104,64 +109,63 @@ export class FooterBarComponent implements OnInit, OnDestroy {
this.fileUploadService.uploadFile(formData);
}
private hideElement(elem: 'left'): void {
const element = this.getElement(elem);
element.classList.add('d-none');
element.classList.remove('d-flex');
}
private showElement(elem: 'left'): void {
const element = this.getElement(elem);
element.classList.remove('d-none');
element.classList.add('d-flex');
}
private getElement(elem: