File

src/app/test-controller/test-controller.component.ts

Implements

OnInit OnDestroy

Metadata

styleUrls ./test-controller.component.css
templateUrl ./test-controller.component.html

Index

Properties
Methods
HostListeners

Constructor

constructor(appVersion: string, isProductionMode: boolean, mds: MainDataService, tcs: TestControllerService, bs: BackendService, reviewDialog: MatDialog, snackBar: MatSnackBar, router: Router, route: ActivatedRoute, cts: CustomtextService, cmd: CommandService)
Parameters :
Name Type Optional
appVersion string No
isProductionMode boolean No
mds MainDataService No
tcs TestControllerService No
bs BackendService No
reviewDialog MatDialog No
snackBar MatSnackBar No
router Router No
route ActivatedRoute No
cts CustomtextService No
cmd CommandService No

HostListeners

window:unload
Arguments : '$event'
window:unload()

Methods

Private addAppFocusSubscription
addAppFocusSubscription()
Returns : void
Private addTestletContentFromBookletXml
addTestletContentFromBookletXml(targetTestlet: Testlet, node: Element)
Parameters :
Name Type Optional
targetTestlet Testlet No
node Element No
Returns : void
Private getBookletFromXml
getBookletFromXml(xmlString: string)
Parameters :
Name Type Optional
xmlString string No
Returns : Testlet
Private Static getChildElements
getChildElements(element)
Parameters :
Name Optional
element No
Returns : any
handleCommand
handleCommand(commandName: string, params: string[])
Parameters :
Name Type Optional
commandName string No
params string[] No
Returns : void
Private incrementProgressValueBy1
incrementProgressValueBy1()
Returns : void
Private loadUnitOk
loadUnitOk(myUnit: UnitDef, sequenceId: number)
Parameters :
Name Type Optional
myUnit UnitDef No
sequenceId number No
Returns : Observable<number>
ngOnDestroy
ngOnDestroy()
Returns : void
ngOnInit
ngOnInit()
Returns : void
showReviewDialog
showReviewDialog()
Returns : void
Private unsubscribeTestSubscriptions
unsubscribeTestSubscriptions()
Returns : void

Properties

Private allUnitIds
Type : string[]
Default value : []
Private appFocusSubscription
Type : Subscription
Default value : null
Public appVersion
Type : string
Decorators :
@Inject('APP_VERSION')
Private appWindowHasFocusSubscription
Type : Subscription
Default value : null
Public cmd
Type : CommandService
Private commandSubscription
Type : Subscription
Default value : null
debugPane
Default value : false
Private errorReportingSubscription
Type : Subscription
Default value : null
Public isProductionMode
Type : boolean
Decorators :
@Inject('IS_PRODUCTION_MODE')
Private lastTestletIndex
Type : number
Default value : 0
Private lastUnitSequenceId
Type : number
Default value : 0
Private loadedUnitCount
Type : number
Default value : 0
Static localStorageTestKey
Type : string
Default value : 'iqb-tc-t'
Private maxTimerSubscription
Type : Subscription
Default value : null
Private resumeTargetUnitId
Type : number
Default value : 0
Private routingSubscription
Type : Subscription
Default value : null
Public tcs
Type : TestControllerService
Private testStatusSubscription
Type : Subscription
Default value : null
Private timerRunning
Default value : false
timerValue
Type : MaxTimerData
Default value : null
Private unitLoadBlobSubscription
Type : Subscription
Default value : null
Private unitLoadQueue
Type : TaggedString[]
Default value : []
Private unitLoadXmlSubscription
Type : Subscription
Default value : null
unitNavigationTarget
Default value : UnitNavigationTarget
import { ActivatedRoute, Router } from '@angular/router';
import {
  Component, HostListener, Inject, OnDestroy, OnInit
} from '@angular/core';
import {
  from, Observable, of, Subscription, throwError
} from 'rxjs';
import {
  concatMap, debounceTime, distinctUntilChanged, map, switchMap
} from 'rxjs/operators';
import { CustomtextService } from 'iqb-components';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import {
  AppFocusState,
  Command,
  MaxTimerDataType,
  ReviewDialogData,
  StateReportEntry,
  TaggedString,
  TestControllerState,
  TestData,
  TestLogEntryKey,
  TestStateKey,
  UnitData,
  UnitNavigationTarget, UnitStateKey,
  WindowFocusState
} from './test-controller.interfaces';
import {
  EnvironmentData, MaxTimerData, Testlet, UnitDef
} from './test-controller.classes';
import { BackendService } from './backend.service';
import { MainDataService } from '../maindata.service';
import { TestControllerService } from './test-controller.service';
import { ReviewDialogComponent } from './review-dialog/review-dialog.component';
// eslint-disable-next-line import/extensions
import { BookletConfig } from '../config/booklet-config';
import { TestMode } from '../config/test-mode';
import { CommandService } from './command.service';

@Component({
  templateUrl: './test-controller.component.html',
  styleUrls: ['./test-controller.component.css']
})
export class TestControllerComponent implements OnInit, OnDestroy {
  static localStorageTestKey = 'iqb-tc-t';
  private errorReportingSubscription: Subscription = null;
  private testStatusSubscription: Subscription = null;
  private routingSubscription: Subscription = null;
  private maxTimerSubscription: Subscription = null;
  private unitLoadXmlSubscription: Subscription = null;
  private unitLoadBlobSubscription: Subscription = null;
  private appWindowHasFocusSubscription: Subscription = null;
  private appFocusSubscription: Subscription = null;
  private commandSubscription: Subscription = null;
  private lastUnitSequenceId = 0;
  private lastTestletIndex = 0;
  private timerRunning = false;
  private allUnitIds: string[] = [];
  private loadedUnitCount = 0;
  private unitLoadQueue: TaggedString[] = [];
  private resumeTargetUnitId = 0;
  timerValue: MaxTimerData = null;
  unitNavigationTarget = UnitNavigationTarget;
  debugPane = false;

  constructor(
    @Inject('APP_VERSION') public appVersion: string,
    @Inject('IS_PRODUCTION_MODE') public isProductionMode: boolean,
    private mds: MainDataService,
    public tcs: TestControllerService,
    private bs: BackendService,
    private reviewDialog: MatDialog,
    private snackBar: MatSnackBar,
    private router: Router,
    private route: ActivatedRoute,
    private cts: CustomtextService,
    public cmd: CommandService
  ) {
  }

  private static getChildElements(element) {
    return Array.prototype.slice.call(element.childNodes)
      .filter(e => e.nodeType === 1);
  }

  // private: recursive reading testlets/units from xml
  private addTestletContentFromBookletXml(targetTestlet: Testlet, node: Element) {
    const childElements = TestControllerComponent.getChildElements(node);
    if (childElements.length > 0) {
      let codeToEnter = '';
      let codePrompt = '';
      let maxTime = -1;

      let restrictionElement: Element = null;
      for (let childIndex = 0; childIndex < childElements.length; childIndex++) {
        if (childElements[childIndex].nodeName === 'Restrictions') {
          restrictionElement = childElements[childIndex];
          break;
        }
      }
      if (restrictionElement !== null) {
        const restrictionElements = TestControllerComponent.getChildElements(restrictionElement);
        for (let childIndex = 0; childIndex < restrictionElements.length; childIndex++) {
          if (restrictionElements[childIndex].nodeName === 'CodeToEnter') {
            const restrictionParameter = restrictionElements[childIndex].getAttribute('code');
            if ((typeof restrictionParameter !== 'undefined') && (restrictionParameter !== null)) {
              codeToEnter = restrictionParameter.toUpperCase();
              codePrompt = restrictionElements[childIndex].textContent;
            }
          } else if (restrictionElements[childIndex].nodeName === 'TimeMax') {
            const restrictionParameter = restrictionElements[childIndex].getAttribute('minutes');
            if ((typeof restrictionParameter !== 'undefined') && (restrictionParameter !== null)) {
              maxTime = Number(restrictionParameter);
              if (Number.isNaN(maxTime)) {
                maxTime = -1;
              }
            }
          }
        }
      }

      if (codeToEnter.length > 0) {
        targetTestlet.codeToEnter = codeToEnter;
        targetTestlet.codePrompt = codePrompt;
      }
      targetTestlet.maxTimeLeft = maxTime;
      if (this.tcs.LastMaxTimerState) {
        if (this.tcs.LastMaxTimerState.hasOwnProperty(targetTestlet.id)) {
          targetTestlet.maxTimeLeft = this.tcs.LastMaxTimerState[targetTestlet.id];
        }
      }

      for (let childIndex = 0; childIndex < childElements.length; childIndex++) {
        if (childElements[childIndex].nodeName === 'Unit') {
          const myUnitId = childElements[childIndex].getAttribute('id');
          let myUnitAlias = childElements[childIndex].getAttribute('alias');
          if (!myUnitAlias) {
            myUnitAlias = myUnitId;
          }
          let myUnitAliasClear = myUnitAlias;
          let unitIdSuffix = 1;
          while (this.allUnitIds.indexOf(myUnitAliasClear) > -1) {
            myUnitAliasClear = `${myUnitAlias}-${unitIdSuffix.toString()}`;
            unitIdSuffix += 1;
          }
          this.allUnitIds.push(myUnitAliasClear);

          targetTestlet.addUnit(this.lastUnitSequenceId, myUnitId,
            childElements[childIndex].getAttribute('label'), myUnitAliasClear,
            childElements[childIndex].getAttribute('labelshort'));
          this.lastUnitSequenceId += 1;
        } else if (childElements[childIndex].nodeName === 'Testlet') {
          let testletId: string = childElements[childIndex].getAttribute('id');
          if (!testletId) {
            testletId = `Testlet${this.lastTestletIndex.toString()}`;
            this.lastTestletIndex += 1;
          }
          let testletLabel: string = childElements[childIndex].getAttribute('label');
          testletLabel = testletLabel ? testletLabel.trim() : '';

          this.addTestletContentFromBookletXml(targetTestlet.addTestlet(testletId, testletLabel), childElements[childIndex]);
        }
      }
    }
  }

  private getBookletFromXml(xmlString: string): Testlet {
    let rootTestlet: Testlet = null;

    try {
      const oParser = new DOMParser();
      const oDOM = oParser.parseFromString(xmlString, 'text/xml');
      if (oDOM.documentElement.nodeName === 'Booklet') {
        const metadataElements = oDOM.documentElement.getElementsByTagName('Metadata');
        if (metadataElements.length > 0) {
          const metadataElement = metadataElements[0];
          const IdElement = metadataElement.getElementsByTagName('Id')[0];
          const LabelElement = metadataElement.getElementsByTagName('Label')[0];
          rootTestlet = new Testlet(0, IdElement.textContent, LabelElement.textContent);
          const unitsElements = oDOM.documentElement.getElementsByTagName('Units');
          if (unitsElements.length > 0) {
            const customTextsElements = oDOM.documentElement.getElementsByTagName('CustomTexts');
            if (customTextsElements.length > 0) {
              const customTexts = TestControllerComponent.getChildElements(customTextsElements[0]);
              const customTextsForBooklet = {};
              for (let childIndex = 0; childIndex < customTexts.length; childIndex++) {
                if (customTexts[childIndex].nodeName === 'Text') {
                  const customTextKey = customTexts[childIndex].getAttribute('key');
                  if ((typeof customTextKey !== 'undefined') && (customTextKey !== null)) {
                    customTextsForBooklet[customTextKey] = customTexts[childIndex].textContent;
                  }
                }
              }
              this.cts.addCustomTexts(customTextsForBooklet);
            }

            const bookletConfigElements = oDOM.documentElement.getElementsByTagName('BookletConfig');

            this.tcs.bookletConfig = new BookletConfig();
            this.tcs.bookletConfig.setFromKeyValuePairs(MainDataService.getTestConfig());
            if (bookletConfigElements.length > 0) {
              this.tcs.bookletConfig.setFromXml(bookletConfigElements[0]);
            }

            // recursive call through all testlets
            this.lastUnitSequenceId = 1;
            this.lastTestletIndex = 1;
            this.allUnitIds = [];
            this.addTestletContentFromBookletXml(rootTestlet, unitsElements[0]);
          }
        }
      }
    } catch (error) {
      console.error('error reading booklet XML:', error);
      rootTestlet = null;
    }
    return rootTestlet;
  }

  private incrementProgressValueBy1() {
    this.loadedUnitCount += 1;
    this.tcs.loadProgressValue = this.loadedUnitCount * 100 / this.lastUnitSequenceId;
  }

  // private: read unitdata
  private loadUnitOk(myUnit: UnitDef, sequenceId: number): Observable<number> {
    myUnit.setCanEnter('n', 'Fehler beim Laden');
    return this.bs.getUnitData(this.tcs.testId, myUnit.id, myUnit.alias)
      .pipe(
        switchMap(myData => {
          if (myData === false) {
            return throwError(`error requesting unit ${this.tcs.testId}/${myUnit.id}`);
          }
          const myUnitData = myData as UnitData;
          if (myUnitData.restorepoint) {
            this.tcs.addUnitStateData(sequenceId, JSON.parse(myUnitData.restorepoint));
          }
          let playerId = null;
          let definitionRef = '';
          if (myUnitData.laststate && myUnitData.laststate[UnitStateKey.PRESENTATION_PROGRESS]) {
            this.tcs.setOldUnitPresentationComplete(sequenceId, myUnitData.laststate[UnitStateKey.PRESENTATION_PROGRESS]);
          }

          try {
            const oParser = new DOMParser();
            const oDOM = oParser.parseFromString(myUnitData.xml, 'text/xml');

            if (oDOM.documentElement.nodeName === 'Unit') {
              const defElements = oDOM.documentElement.getElementsByTagName('Definition');

              if (defElements.length > 0) {
                const defElement = defElements[0];
                this.tcs.addUnitDefinition(sequenceId, defElement.textContent);
                playerId = defElement.getAttribute('player');
              } else {
                const defRefElements = oDOM.documentElement.getElementsByTagName('DefinitionRef');

                if (defRefElements.length > 0) {
                  const defRefElement = defRefElements[0];
                  definitionRef = defRefElement.textContent;
                  // this.tcs.addUnitDefinition(sequenceId, '');
                  playerId = defRefElement.getAttribute('player');
                }
              }
            }
          } catch (error) {
            return throwError(`error parsing unit def ${this.tcs.testId}/${myUnit.id} (${error.toString()})`);
          }

          if (playerId) {
            myUnit.playerId = playerId;
            if (definitionRef.length > 0) {
              this.unitLoadQueue.push(<TaggedString>{
                tag: sequenceId.toString(),
                value: definitionRef
              });
            }
            myUnit.setCanEnter('y', '');

            if (this.tcs.hasPlayer(playerId)) {
              return of(sequenceId);
            }
            // to avoid multiple calls before returning:
            this.tcs.addPlayer(playerId, '');
            return this.bs.getResource(this.tcs.testId, '', this.tcs.normaliseId(playerId, 'html'), true)
              .pipe(
                switchMap((data: number|TaggedString) => {
                  if (typeof data === 'number') {
                    return throwError(`error getting player "${playerId}"`);
                  }
                  const player = data as TaggedString;
                  if (player.value.length > 0) {
                    this.tcs.addPlayer(playerId, player.value);
                    return of(sequenceId);
                  }
                  return throwError(`error getting player "${playerId}" (size = 0)`);
                })
              );
          }
          return throwError(`player def missing for unit ${this.tcs.testId}/${myUnit.id}`);
        })
      );
  }

  @HostListener('window:unload', ['$event'])
  unloadHandler() {
    if (this.cmd.connectionStatus$.getValue() !== 'ws-online') {
      this.bs.notifyDyingTest(this.tcs.testId);
    }
  }

  ngOnInit(): void {
    setTimeout(() => {
      this.mds.progressVisualEnabled = false;
      // TODO rethink if different behaviour in production and normal mode is dangerous maybe
      if (this.isProductionMode && this.tcs.testMode.saveResponses) {
        this.mds.errorReportingSilent = true;
      }
      this.errorReportingSubscription = this.mds.appError$.subscribe(e => {
        if (this.isProductionMode && this.tcs.testMode.saveResponses) {
          console.error(`${e.label} / ${e.description}`);
        }
        this.tcs.testStatus$.next(TestControllerState.ERROR);
      });
      this.testStatusSubscription = this.tcs.testStatus$.subscribe(testControllerState => {
        if (this.tcs.testMode.saveResponses && [TestControllerState.FINISHED, TestControllerState.INIT, TestControllerState.LOADING].indexOf(testControllerState) === -1) {
          this.bs.updateTestState(this.tcs.testId, [<StateReportEntry>{
            key: TestStateKey.CONTROLLER, timeStamp: Date.now(), content: testControllerState
          }]);
        }

        switch (testControllerState) {
          case TestControllerState.ERROR:
            this.tcs.loadProgressValue = 0;
            this.tcs.setUnitNavigationRequest(UnitNavigationTarget.ERROR);
            break;
          case TestControllerState.PAUSED:
            // TODO pause time
            this.tcs.setUnitNavigationRequest(UnitNavigationTarget.PAUSE, true);
            break;
        }
      });
      this.appWindowHasFocusSubscription = this.mds.appWindowHasFocus$.subscribe(hasFocus => {
        this.tcs.windowFocusState$.next(hasFocus ? WindowFocusState.HOST : WindowFocusState.UNKNOWN);
      });
      this.commandSubscription = this.cmd.command$.pipe(
        distinctUntilChanged((command1: Command, command2: Command): boolean => (command1.id === command2.id))
      )
        .subscribe((command: Command) => {
          this.handleCommand(command.keyword, command.arguments);
        });

      this.routingSubscription = this.route.params.subscribe(params => {
        if (this.tcs.testStatus$.getValue() !== TestControllerState.ERROR) {
          this.tcs.testId = params.t;

          // Reset TestMode to be Demo, before the correct one comes with getTestData
          // TODO maybe it would be better to retrieve the testmode from the login
          this.tcs.testMode = new TestMode();

          localStorage.setItem(TestControllerComponent.localStorageTestKey, params.t);

          this.unsubscribeTestSubscriptions();

          this.maxTimerSubscription = this.tcs.maxTimeTimer$.subscribe(maxTimerData => {
            switch (maxTimerData.type) {
              case MaxTimerDataType.STARTED:
                this.snackBar.open(this.cts.getCustomText('booklet_msgTimerStarted') +
                    maxTimerData.timeLeftMinString, '', { duration: 3000 });
                this.timerValue = maxTimerData;
                break;
              case MaxTimerDataType.ENDED:
                this.snackBar.open(this.cts.getCustomText('booklet_msgTimeOver'), '', { duration: 3000 });
                this.tcs.rootTestlet.setTimeLeft(maxTimerData.testletId, 0);
                this.tcs.LastMaxTimerState[maxTimerData.testletId] = 0;
                if (this.tcs.testMode.saveResponses) {
                  this.bs.updateTestState(this.tcs.testId, [<StateReportEntry>{
                    key: TestStateKey.TESTLETS_TIMELEFT, timeStamp: Date.now(), content: JSON.stringify(this.tcs.LastMaxTimerState)
                  }]);
                }
                this.timerRunning = false;
                this.timerValue = null;
                if (this.tcs.testMode.forceTimeRestrictions) {
                  this.tcs.setUnitNavigationRequest(UnitNavigationTarget.NEXT);
                }
                break;
              case MaxTimerDataType.CANCELLED:
                this.snackBar.open(this.cts.getCustomText('booklet_msgTimerCancelled'), '', { duration: 3000 });
                this.tcs.rootTestlet.setTimeLeft(maxTimerData.testletId, 0);
                this.tcs.LastMaxTimerState[maxTimerData.testletId] = 0;
                if (this.tcs.testMode.saveResponses) {
                  this.bs.updateTestState(this.tcs.testId, [<StateReportEntry>{
                    key: TestStateKey.TESTLETS_TIMELEFT, timeStamp: Date.now(), content: JSON.stringify(this.tcs.LastMaxTimerState)
                  }]);
                }
                this.timerValue = null;
                break;
              case MaxTimerDataType.INTERRUPTED:
                this.tcs.rootTestlet.setTimeLeft(maxTimerData.testletId, this.tcs.LastMaxTimerState[maxTimerData.testletId]);
                this.timerValue = null;
                break;
              case MaxTimerDataType.STEP:
                this.timerValue = maxTimerData;
                if ((maxTimerData.timeLeftSeconds % 15) === 0) {
                  this.tcs.LastMaxTimerState[maxTimerData.testletId] = Math.round(maxTimerData.timeLeftSeconds / 60);
                  if (this.tcs.testMode.saveResponses) {
                    this.bs.updateTestState(this.tcs.testId, [<StateReportEntry>{
                      key: TestStateKey.TESTLETS_TIMELEFT, timeStamp: Date.now(), content: JSON.stringify(this.tcs.LastMaxTimerState)
                    }]);
                  }
                }
                if ((maxTimerData.timeLeftSeconds / 60) === 5) {
                  this.snackBar.open(this.cts.getCustomText('booklet_msgSoonTimeOver5Minutes'), '', { duration: 3000 });
                } else if ((maxTimerData.timeLeftSeconds / 60) === 1) {
                  this.snackBar.open(this.cts.getCustomText('booklet_msgSoonTimeOver1Minute'), '', { duration: 3000 });
                }
                break;
            }
          });

          this.tcs.resetDataStore();
          const loadStartTimeStamp = Date.now();
          const envData = new EnvironmentData(this.appVersion);
          this.tcs.testStatus$.next(TestControllerState.LOADING);
          this.tcs.loadProgressValue = 0;
          this.tcs.loadComplete = false;

          this.bs.getTestData(this.tcs.testId).subscribe((testData: TestData|boolean) => {
            if ((testData === false) || (testData === true)) {
              this.mds.appError$.next({
                label: 'Konnte Testinformation nicht laden',
                description: 'TestController.Component: getTestData()',
                category: 'PROBLEM'
              });
            } else {
              this.tcs.testMode = new TestMode(testData.mode);
              let navTargetUnitId = '';
              let newTestStatus = TestControllerState.RUNNING;
              if (testData.laststate !== null) {
                Object.keys(testData.laststate).forEach(stateKey => {
                  switch (stateKey) {
                    case (TestStateKey.CURRENT_UNIT_ID):
                      navTargetUnitId = testData.laststate[stateKey];
                      break;
                    case (TestStateKey.TESTLETS_TIMELEFT):
                      this.tcs.LastMaxTimerState = JSON.parse(testData.laststate[stateKey]);
                      break;
                    case (TestStateKey.CONTROLLER):
                      if (testData.laststate[stateKey] === TestControllerState.PAUSED) {
                        newTestStatus = TestControllerState.PAUSED;
                      }
                      break;
                    case (TestStateKey.TESTLETS_CLEARED_CODE):
                      this.tcs.clearCodeTestlets = JSON.parse(testData.laststate[stateKey]);
                      break;
                  }
                });
              }
              this.tcs.rootTestlet = this.getBookletFromXml(testData.xml);

              document.documentElement.style.setProperty('--tc-unit-title-height',
                this.tcs.bookletConfig.unit_title === 'ON' ? this.mds.defaultTcUnitTitleHeight : '0');
              document.documentElement.style.setProperty('--tc-header-height',
                this.tcs.bookletConfig.unit_screenheader === 'OFF' ? '0' : this.mds.defaultTcHeaderHeight);
              document.documentElement.style.setProperty('--tc-unit-page-nav-height',
                this.tcs.bookletConfig.page_navibuttons === 'SEPARATE_BOTTOM' ? this.mds.defaultTcUnitPageNavHeight : '0');

              if (this.tcs.rootTestlet === null) {
                this.mds.appError$.next({
                  label: 'Problem beim Laden der Testinformation',
                  description: 'TestController.Component: getBookletFromXml(testData.xml)',
                  category: 'PROBLEM'
                });
                this.tcs.testStatus$.next(TestControllerState.ERROR);
              } else {
                this.tcs.maxUnitSequenceId = this.lastUnitSequenceId - 1;
                if (this.tcs.clearCodeTestlets.length > 0) {
                  this.tcs.rootTestlet.clearTestletCodes(this.tcs.clearCodeTestlets);
                }

                this.loadedUnitCount = 0;
                const sequArray = [];
                for (let i = 1; i < this.tcs.maxUnitSequenceId + 1; i++) {
                  sequArray.push(i);
                }

                this.unitLoadXmlSubscription = from(sequArray).pipe(
                  concatMap(uSequ => {
                    const ud = this.tcs.rootTestlet.getUnitAt(uSequ);
                    return this.loadUnitOk(ud.unitDef, uSequ);
                  })
                ).subscribe(() => {
                  this.incrementProgressValueBy1();
                },
                errorMessage => {
                  this.mds.appError$.next({
                    label: 'Problem beim Laden der Testinformation',
                    description: errorMessage,
                    category: 'PROBLEM'
                  });
                },
                () => {
                  this.tcs.rootTestlet.lockUnitsIfTimeLeftNull();
                  let navTarget = 1;
                  if (navTargetUnitId) {
                    const tmpNavTarget = this.tcs.rootTestlet.getSequenceIdByUnitAlias(navTargetUnitId);
                    if (tmpNavTarget > 0) {
                      navTarget = tmpNavTarget;
                    }
                  }
                  this.tcs.updateMinMaxUnitSequenceId(navTarget);
                  this.loadedUnitCount = 0;

                  this.unitLoadBlobSubscription = from(this.unitLoadQueue).pipe(
                    concatMap(queueEntry => {
                      const unitSequ = Number(queueEntry.tag);
                      if (this.tcs.bookletConfig.loading_mode === 'EAGER') {
                        this.incrementProgressValueBy1();
                      }
                      // avoid to load unit def if not necessary
                      if (unitSequ < this.tcs.minUnitSequenceId) {
                        return of(<TaggedString>{ tag: unitSequ.toString(), value: '' });
                      }
                      return this.bs.getResource(this.tcs.testId, queueEntry.tag, queueEntry.value).pipe(
                        map(response => {
                          if (typeof response === 'number') {
                            return throwError(`error loading voud ${this.tcs.testId} / ${queueEntry.tag} / ${queueEntry.value}: status ${response}`);
                          }
                          return response;
                        })
                      );
                    })
                  ).subscribe(
                    (def: TaggedString) => {
                      this.tcs.addUnitDefinition(Number(def.tag), def.value);
                    },
                    errorMessage => {
                      this.mds.appError$.next({
                        label: 'Problem beim Laden der Testinformation',
                        description: errorMessage,
                        category: 'PROBLEM'
                      });
                      this.tcs.testStatus$.next(TestControllerState.ERROR);
                    },
                    () => { // complete
                      if (this.tcs.testMode.saveResponses) {
                        envData.loadTime = Date.now() - loadStartTimeStamp;
                        this.bs.addTestLog(this.tcs.testId, [<StateReportEntry>{
                          key: TestLogEntryKey.LOADCOMPLETE, timeStamp: Date.now(), content: JSON.stringify(envData)
                        }]);
                      }
                      this.tcs.loadProgressValue = 100;

                      this.tcs.loadComplete = true;
                      if (this.tcs.bookletConfig.loading_mode === 'EAGER') {
                        this.resumeTargetUnitId = navTarget;
                        this.tcs.setUnitNavigationRequest(navTarget.toString());
                        this.tcs.testStatus$.next(newTestStatus);
                        if (this.tcs.testMode.saveResponses) {
                          this.addAppFocusSubscription();
                        }
                      }
                    }
                  );

                  if (this.tcs.bookletConfig.loading_mode === 'LAZY') {
                    this.resumeTargetUnitId = navTarget;
                    this.tcs.setUnitNavigationRequest(navTarget.toString());
                    this.tcs.testStatus$.next(newTestStatus);
                    if (this.tcs.testMode.saveResponses) {
                      this.addAppFocusSubscription();
                    }
                  }
                } // complete
                );
              }
            }
          }); // getTestData
        }
      }); // routingSubscription

      this.cmd.connectionStatus$
        .pipe(
          map(status => status === 'ws-online'),
          distinctUntilChanged()
        )
        .subscribe(isWsConnected => {
          if (this.tcs.testMode.saveResponses) {
            this.bs.updateTestState(this.tcs.testId, [{
              key: TestStateKey.CONNECTION,
              content: isWsConnected ? 'WEBSOCKET' : 'POLLING',
              timeStamp: Date.now()
            }]);
          }
        });
    }); // setTimeOut
  }

  private addAppFocusSubscription() {
    if (this.appFocusSubscription !== null) {
      this.appFocusSubscription.unsubscribe();
    }
    this.appFocusSubscription = this.tcs.windowFocusState$.pipe(
      debounceTime(500)
    ).subscribe((newState: WindowFocusState) => {
      if (newState === WindowFocusState.UNKNOWN) {
        this.bs.updateTestState(this.tcs.testId, [<StateReportEntry>{
          key: TestStateKey.FOCUS, timeStamp: Date.now(), content: AppFocusState.HAS_NOT
        }]);
      } else {
        this.bs.updateTestState(this.tcs.testId, [<StateReportEntry>{
          key: TestStateKey.FOCUS, timeStamp: Date.now(), content: AppFocusState.HAS
        }]);
      }
    });
  }

  showReviewDialog() {
    if (this.tcs.rootTestlet === null) {
      this.snackBar.open('Kein Testheft verfügbar.', '', { duration: 3000 });
    } else {
      const authData = MainDataService.getAuthData();
      const dialogRef = this.reviewDialog.open(ReviewDialogComponent, {
        width: '700px',
        data: <ReviewDialogData>{
          loginname: authData.displayName,
          bookletname: this.tcs.rootTestlet.title,
          unitTitle: this.tcs.currentUnitTitle,
          unitDbKey: this.tcs.currentUnitDbKey
        }
      });

      dialogRef.afterClosed().subscribe(result => {
        if (typeof result !== 'undefined') {
          if (result !== false) {
            const targetSelection = result.target;
            if (targetSelection === 'u') {
              this.bs.saveUnitReview(
                this.tcs.testId,
                this.tcs.currentUnitDbKey,
                result.priority,
                dialogRef.componentInstance.getCategories(),
                result.sender ? `${result.sender}: ${result.entry}` : result.entry
              ).subscribe(ok => {
                if (!ok) {
                  this.snackBar.open('Konnte Kommentar nicht speichern', '', { duration: 3000 });
                } else {
                  this.snackBar.open('Kommentar gespeichert', '', { duration: 1000 });
                }
              });
            } else {
              this.bs.saveTestReview(
                this.tcs.testId,
                result.priority,
                dialogRef.componentInstance.getCategories(),
                result.sender ? `${result.sender}: ${result.entry}` : result.entry
              ).subscribe(ok => {
                if (!ok) {
                  this.snackBar.open('Konnte Kommentar nicht speichern', '', { duration: 3000 });
                } else {
                  this.snackBar.open('Kommentar gespeichert', '', { duration: 1000 });
                }
              });
            }
          }
        }
      });
    }
  }

  private unsubscribeTestSubscriptions() {
    if (this.maxTimerSubscription !== null) {
      this.maxTimerSubscription.unsubscribe();
      this.maxTimerSubscription = null;
    }
    if (this.unitLoadXmlSubscription !== null) {
      this.unitLoadXmlSubscription.unsubscribe();
      this.unitLoadXmlSubscription = null;
    }
    if (this.unitLoadBlobSubscription !== null) {
      this.unitLoadBlobSubscription.unsubscribe();
      this.unitLoadBlobSubscription = null;
    }
  }

  handleCommand(commandName: string, params: string[]) {
    switch (commandName.toLowerCase()) {
      case 'debug':
        this.debugPane = params.length === 0 || params[0].toLowerCase() !== 'off';
        if (this.debugPane) {
          console.log('select (focus) app window to see the debugPane');
        }
        break;
      case 'pause':
        this.tcs.interruptMaxTimer();
        this.tcs.testStatus$.next(TestControllerState.PAUSED);
        this.resumeTargetUnitId = this.tcs.currentUnitSequenceId;
        break;
      case 'resume':
        const navTarget = (this.resumeTargetUnitId > 0) ? this.resumeTargetUnitId.toString() : UnitNavigationTarget.FIRST;
        this.tcs.testStatus$.next(TestControllerState.RUNNING);
        this.tcs.setUnitNavigationRequest(navTarget, true);
        break;
      case 'terminate':
        this.tcs.terminateTest('BOOKLETLOCKEDbyOPERATOR');
        break;
      case 'goto':
        this.tcs.testStatus$.next(TestControllerState.RUNNING);
        let gotoTarget: string;
        if ((params.length === 2) && (params[0] === 'id')) {
          gotoTarget = (this.allUnitIds.indexOf(params[1]) + 1).toString(10);
        } else if (params.length === 1) {
          gotoTarget = params[0];
        }
        if (gotoTarget && gotoTarget !== '0') {
          this.resumeTargetUnitId = 0;
          this.tcs.interruptMaxTimer();
          this.tcs.setUnitNavigationRequest(gotoTarget, true);
        }
        break;
    }
  }

  ngOnDestroy() {
    if (this.routingSubscription !== null) {
      this.routingSubscription.unsubscribe();
    }
    if (this.errorReportingSubscription !== null) {
      this.errorReportingSubscription.unsubscribe();
    }
    if (this.testStatusSubscription !== null) {
      this.testStatusSubscription.unsubscribe();
    }
    if (this.appWindowHasFocusSubscription !== null) {
      this.appWindowHasFocusSubscription.unsubscribe();
    }
    if (this.appFocusSubscription !== null) {
      this.appFocusSubscription.unsubscribe();
    }
    if (this.commandSubscription !== null) {
      this.commandSubscription.unsubscribe();
    }
    this.unsubscribeTestSubscriptions();
    this.mds.progressVisualEnabled = true;
    this.mds.errorReportingSilent = false;
  }
}
<div class="debug-pane" *ngIf="debugPane">
  <div><b>STATUS:</b> {{tcs.testStatus$ | async}}</div>
  <div><b>TIMER:</b> {{timerValue?.timeLeftString}}<b> {{timerValue?.testletId}} {{timerValue?.type}}</b></div>
  <div><b>MODE:</b> {{tcs.testMode.modeId}}</div>
  <div><b>FOCUS:</b> {{tcs.windowFocusState$ | async}}</div>
  <div><b>BS:</b> {{cmd.connectionStatus$ | async}}</div>
</div>

<div id="header" fxLayout="row" fxLayoutAlign="end center">
    <p class="timer" *ngIf="tcs.testMode.showTimeLeft">{{ timerValue?.timeLeftString }}</p>
    <button mat-fab [disabled]="!tcs.unitPrevEnabled" *ngIf="(tcs.bookletConfig.unit_navibuttons !== 'OFF') && ((tcs.testStatus$ | async) === tcs.testStatusEnum.RUNNING)" color="accent"
            (click)="tcs.setUnitNavigationRequest(unitNavigationTarget.PREVIOUS)" matTooltip="Zurück" fxFlex="none">
      <i class="material-icons">chevron_left</i>
    </button>
    <div *ngIf="(tcs.bookletConfig.unit_navibuttons === 'FULL') && ((tcs.testStatus$ | async) === tcs.testStatusEnum.RUNNING)" fxLayout="row wrap" fxLayoutAlign="start center">
      <div *ngFor="let u of tcs.unitListForNaviButtons">
        <div fxLayout="column" fxLayoutAlign="start center">
            <button (click)="tcs.setUnitNavigationRequest(u.sequenceId.toString())" class="unit-button" [disabled]="u.disabled">{{ u.shortLabel }}</button>
            <span class="current-unit-dot" *ngIf="u.isCurrent"></span>
            <span class="notcurrent-unit-dot" *ngIf="!u.isCurrent"></span>
        </div>
      </div>
    </div>
    <button mat-fab [disabled]="!tcs.unitNextEnabled" *ngIf="(tcs.bookletConfig.unit_navibuttons !== 'OFF')  && ((tcs.testStatus$ | async) === tcs.testStatusEnum.RUNNING)" color="accent"
            (click)="tcs.setUnitNavigationRequest(unitNavigationTarget.NEXT)" matTooltip="Weiter" fxFlex="none">
      <i class="material-icons">chevron_right</i>
    </button>
    <div class="plus-buttons" fxFlex="none" fxLayout="row">
      <button mat-button (click)="showReviewDialog()" *ngIf="tcs.testMode.canReview" matTooltip="Kommentar senden" fxFlex="none">
        <mat-icon>rate_review</mat-icon>
      </button>
      <button mat-button (click)="tcs.setUnitNavigationRequest(unitNavigationTarget.MENU)"
              *ngIf="(tcs.bookletConfig.unit_menu !== 'OFF') || tcs.testMode.showUnitMenu"
              matTooltip="Zur Aufgabenliste" fxFlex="none">
        <mat-icon>menu</mat-icon>
      </button>
    </div>
</div>

<div class="tc-body" fxLayout="row" fxLayoutAlign="start stretch">
    <router-outlet></router-outlet>
</div>

./test-controller.component.css

.timer {
  color: lightgray;
  margin: 0 10px;
}

.tcSidenav {
  background-color: teal;
  height: 100%;
  overflow-x: hidden;
  overflow-y: auto;
}

.tcSidenav > p, .tcSidenav > button {
  margin: 15px 10px 0 10px;
}

.tc-body {
  overflow-x: auto;
  position: absolute;
  width: 100%;
  top: var(--tc-header-height);
  bottom: 0;
}

/* --------------------------------------------- */
#header {
  position: absolute;
  width: 100%;
  padding-right: 5px;
  padding-top: 2px;
  padding-left: 25px;
  color: white;
  z-index: 444;
}
#header .material-icons {
  /* font-size: 2.0rem; */
  position: relative;
  top: -8px;
  font-size: 36px;
  padding: 2px;
}

#header .plus-buttons {
  background-color: rgba(0, 51, 51, 0.4);
  height: 48px;
}
/* --------------------------------------------- */
mat-toolbar {
  position: fixed;
  z-index: 100;
  top: 4px;
  right: 90px;
}

.mat-fab {
  margin: 0 6px;
}

.current-unit-dot {
  height: 8px;
  width: 8px;
  background-color: red;
  border-radius: 50%;
  display: inline-block;
}
.notcurrent-unit-dot {
  height: 8px;
  width: 8px;
  background-color: transparent;
  border-radius: 50%;
  display: inline-block;
}
.unit-button[disabled], .mat-fab[disabled] {
  background-color: #bdbdbd;
}

.debug-pane {
  color: orange;
  position: absolute;
  background: black;
  z-index: 10000;
  padding: 2px
}

.debug-pane b {
  color: white;
  font-weight: normal;
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""