From 83c139a03594f7de463b848e07859b10e3f1bf74 Mon Sep 17 00:00:00 2001 From: Bartek Fabiszewski Date: Thu, 19 Dec 2019 22:20:55 +0100 Subject: [PATCH] Add lang class --- js/src/chartviewmodel.js | 4 +- js/src/configviewmodel.js | 4 +- js/src/lang.js | 128 ++++++++++++++++++++++++ js/src/mapapi/api_gmaps.js | 8 +- js/src/mapviewmodel.js | 32 +++--- js/src/trackviewmodel.js | 26 ++--- js/src/userviewmodel.js | 6 +- js/test/api_gmaps.test.js | 15 ++- js/test/chartviewmodel.test.js | 1 + js/test/configviewmodel.test.js | 5 +- js/test/lang.test.js | 167 ++++++++++++++++++++++++++++++++ js/test/mapviewmodel.test.js | 6 +- js/test/trackviewmodel.test.js | 2 + 13 files changed, 357 insertions(+), 47 deletions(-) create mode 100644 js/src/lang.js create mode 100644 js/test/lang.test.js diff --git a/js/src/chartviewmodel.js b/js/src/chartviewmodel.js index b9f67ea..f84c549 100644 --- a/js/src/chartviewmodel.js +++ b/js/src/chartviewmodel.js @@ -17,10 +17,10 @@ * along with this program; if not, see . */ +import { lang as $ } from './initializer.js'; import Chartist from 'chartist' import ViewModel from './viewmodel.js'; import ctAxisTitle from 'chartist-plugin-axistitle'; -import { lang } from './initializer.js'; import uUtils from './utils.js'; /** @@ -81,7 +81,7 @@ export default class ChartViewModel extends ViewModel { plugins: [ ctAxisTitle({ axisY: { - axisTitle: `${lang.strings['altitude']} (${lang.unit('unitDistance')})`, + axisTitle: `${$._('altitude')} (${$.unit('unitDistance')})`, axisClass: 'ct-axis-title', offset: { x: 0, diff --git a/js/src/configviewmodel.js b/js/src/configviewmodel.js index 58cc6cd..ee3a3ad 100644 --- a/js/src/configviewmodel.js +++ b/js/src/configviewmodel.js @@ -17,7 +17,7 @@ * along with this program; if not, see . */ -import { config, lang } from './initializer.js'; +import { lang as $, config } from './initializer.js'; import ViewModel from './viewmodel.js'; import uUtils from './utils.js'; @@ -61,7 +61,7 @@ export default class ConfigViewModel extends ViewModel { } setAutoReloadInterval() { - const interval = parseInt(prompt(lang.strings['newinterval'])); + const interval = parseInt(prompt($._('newinterval'))); if (!isNaN(interval) && interval !== this.model.interval) { this.model.interval = interval; } diff --git a/js/src/lang.js b/js/src/lang.js new file mode 100644 index 0000000..f5e899d --- /dev/null +++ b/js/src/lang.js @@ -0,0 +1,128 @@ +/* + * μlogger + * + * Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net) + * + * This is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +export default class uLang { + constructor() { + this.strings = {}; + this.config = null; + } + + /** + * @param {uConfig} config + * @param {Object} data + */ + init(config, data) { + this.config = config; + if (data) { + /** @type {Object} */ + this.strings = data; + } + } + + _(name) { + if (typeof this.strings[name] === 'undefined') { + throw new Error('Unknown localized string'); + } + return this.strings[name]; + } + + /** + * @param {string} name + * @return {string} + */ + unit(name) { + const unitName = this.config[name]; + if (typeof this.config[name] === 'undefined') { + throw new Error('Unknown localized unit'); + } + return this._(unitName); + } + + /** + * Get speed converted to locale units + * @param {number} ms Speed in meters per second + * @param {boolean} withUnit + * @return {(number|string)} String when with unit + */ + getLocaleSpeed(ms, withUnit) { + const value = Math.round(ms * this.config.factorSpeed * 360) / 100; + if (withUnit) { + return `${value} ${this.unit('unitSpeed')}`; + } + return value; + } + + /** + * Get distance converted to locale units + * @param {number} m Distance in meters + * @param {boolean} withUnit + * @return {(number|string)} String when with unit + */ + getLocaleDistanceMajor(m, withUnit) { + const value = Math.round(m * this.config.factorDistanceMajor / 10) / 100; + if (withUnit) { + return `${value} ${this.unit('unitDistanceMajor')}` + } + return value; + } + + /** + * @param {number} m Distance in meters + * @param {boolean} withUnit + * @return {(number|string)} String when with unit + */ + getLocaleDistance(m, withUnit) { + const value = Math.round(m * this.config.factorDistance * 100) / 100; + if (withUnit) { + return `${value} ${this.unit('unitDistance')}`; + } + return value; + } + + /** + * @param {number} m Distance in meters + * @param {boolean} withUnit + * @return {(number|string)} String when with unit + */ + getLocaleAltitude(m, withUnit) { + return this.getLocaleDistance(m, withUnit); + } + + /** + * @param {number} m Distance in meters + * @param {boolean} withUnit + * @return {(number|string)} String when with unit + */ + getLocaleAccuracy(m, withUnit) { + return this.getLocaleDistance(m, withUnit); + } + + /** + * @param {number} s Duration in seconds + * @return {string} Formatted to (d) h:m:s + */ + getLocaleDuration(s) { + const d = Math.floor(s / 86400); + const h = Math.floor((s % 86400) / 3600); + const m = Math.floor(((s % 86400) % 3600) / 60); + s = ((s % 86400) % 3600) % 60; + return ((d > 0) ? (`${d} ${this.unit('unitDay')} `) : '') + + ((`00${h}`).slice(-2)) + ':' + ((`00${m}`).slice(-2)) + ':' + ((`00${s}`).slice(-2)) + ''; + } +} diff --git a/js/src/mapapi/api_gmaps.js b/js/src/mapapi/api_gmaps.js index 04c575d..0190dcf 100644 --- a/js/src/mapapi/api_gmaps.js +++ b/js/src/mapapi/api_gmaps.js @@ -17,7 +17,7 @@ * along with this program; if not, see . */ -import { config, lang } from '../initializer.js'; +import { lang as $, config } from '../initializer.js'; import MapViewModel from '../mapviewmodel.js'; import uTrack from '../track.js'; import uUtils from '../utils.js'; @@ -74,9 +74,9 @@ export default class GoogleMapsApi { }; window.gm_authFailure = () => { GoogleMapsApi.authError = true; - let message = uUtils.sprintf(lang.strings['apifailure'], 'Google Maps'); - message += '

' + lang.strings['gmauthfailure']; - message += '

' + lang.strings['gmapilink']; + let message = uUtils.sprintf($._('apifailure'), 'Google Maps'); + message += '

' + $._('gmauthfailure'); + message += '

' + $._('gmapilink'); if (GoogleMapsApi.gmInitialized) { alert(message); } diff --git a/js/src/mapviewmodel.js b/js/src/mapviewmodel.js index 4cde5bb..0d950e2 100644 --- a/js/src/mapviewmodel.js +++ b/js/src/mapviewmodel.js @@ -17,7 +17,7 @@ * along with this program; if not, see . */ -import { config, lang } from './initializer.js'; +import { lang as $, config } from './initializer.js'; import GoogleMapsApi from './mapapi/api_gmaps.js'; import OpenLayersApi from './mapapi/api_openlayers.js'; import ViewModel from './viewmodel.js'; @@ -78,7 +78,7 @@ export default class MapViewModel extends ViewModel { this.api.init() .then(() => this.onReady()) .catch((e) => { - let txt = uUtils.sprintf(lang.strings['apifailure'], apiName); + let txt = uUtils.sprintf($._('apifailure'), apiName); if (e && e.message) { txt += ` (${e.message})`; } @@ -134,36 +134,36 @@ export default class MapViewModel extends ViewModel { } let provider = ''; if (pos.provider === 'gps') { - provider = ` (${lang.strings['gps']})`; + provider = ` (${$._('gps')})`; } else if (pos.provider === 'network') { - provider = ` (${lang.strings['network']})`; + provider = ` (${$._('network')})`; } let stats = ''; if (!this.state.showLatest) { stats = `
- ${lang.strings['track']}
- ${lang.strings['ttime']} ${lang.getLocaleDuration(pos.totalSeconds)}
- ${lang.strings['aspeed']} ${lang.getLocaleSpeed(pos.totalSpeed, true)}
- ${lang.strings['tdistance']} ${lang.getLocaleDistanceMajor(pos.totalMeters, true)}
+ ${$._('track')}
+ ${$._('ttime')} ${$.getLocaleDuration(pos.totalSeconds)}
+ ${$._('aspeed')} ${$.getLocaleSpeed(pos.totalSpeed, true)}
+ ${$._('tdistance')} ${$.getLocaleDistanceMajor(pos.totalMeters, true)}
`; } return ``; } diff --git a/js/src/trackviewmodel.js b/js/src/trackviewmodel.js index 71b7908..1d4e02b 100644 --- a/js/src/trackviewmodel.js +++ b/js/src/trackviewmodel.js @@ -17,7 +17,7 @@ * along with this program; if not, see . */ -import { config, lang } from './initializer.js'; +import { lang as $, config } from './initializer.js'; import ViewModel from './viewmodel.js'; import uObserve from './observe.js'; import uPositionSet from './positionset.js'; @@ -154,20 +154,20 @@ export default class TrackViewModel extends ViewModel { const form = this.importEl.parentElement; const sizeMax = form.elements['MAX_FILE_SIZE'].value; if (this.importEl.files && this.importEl.files.length === 1 && this.importEl.files[0].size > sizeMax) { - uUtils.error(uUtils.sprintf(lang.strings['isizefailure'], sizeMax)); + uUtils.error(uUtils.sprintf($._('isizefailure'), sizeMax)); return; } uTrack.import(form) .then((trackList) => { if (trackList.length) { if (trackList.length > 1) { - alert(uUtils.sprintf(lang.strings['imultiple'], trackList.length)); + alert(uUtils.sprintf($._('imultiple'), trackList.length)); } this.model.trackList = trackList.concat(this.model.trackList); this.model.currentTrackId = trackList[0].listValue; } }) - .catch((e) => uUtils.error(e, `${lang.strings['actionfailure']}\n${e.message}`)) + .catch((e) => uUtils.error(e, `${$._('actionfailure')}\n${e.message}`)) .finally(() => { this.model.inputFile = ''; }); @@ -190,7 +190,7 @@ export default class TrackViewModel extends ViewModel { this.model.showLatest = false; } }) - .catch((e) => { uUtils.error(e, `${lang.strings['actionfailure']}\n${e.message}`); }); + .catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); }); } } @@ -203,7 +203,7 @@ export default class TrackViewModel extends ViewModel { this.state.currentTrack.clear(); } this.state.currentTrack.fetchPositions() - .catch((e) => { uUtils.error(e, `${lang.strings['actionfailure']}\n${e.message}`); }); + .catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); }); } /** @@ -220,7 +220,7 @@ export default class TrackViewModel extends ViewModel { this.model.currentTrackId = _track.listValue; } }) - .catch((e) => { uUtils.error(e, `${lang.strings['actionfailure']}\n${e.message}`); }); + .catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); }); } /** @@ -235,7 +235,7 @@ export default class TrackViewModel extends ViewModel { this.state.currentTrack = _track; } }) - .catch((e) => { uUtils.error(e, `${lang.strings['actionfailure']}\n${e.message}`); }); + .catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); }); } loadTrackList() { @@ -253,7 +253,7 @@ export default class TrackViewModel extends ViewModel { this.model.currentTrackId = ''; } }) - .catch((e) => { uUtils.error(e, `${lang.strings['actionfailure']}\n${e.message}`); }); + .catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); }); } /** @@ -291,14 +291,14 @@ export default class TrackViewModel extends ViewModel { const dateString = (date.toDateString() !== today.toDateString()) ? `${dateTime.date}
` : ''; const timeString = `${dateTime.time}${dateTime.zone}`; this.model.summary = ` - + ${dateString} ${timeString}`; } else { this.model.summary = ` - -
${lang.strings['tdistance']} ${lang.getLocaleDistanceMajor(last.totalMeters, true)}
-
${lang.strings['ttime']} ${lang.getLocaleDuration(last.totalSeconds)}
`; + +
${$._('tdistance')} ${$.getLocaleDistanceMajor(last.totalMeters, true)}
+
${$._('ttime')} ${$.getLocaleDuration(last.totalSeconds)}
`; } } diff --git a/js/src/userviewmodel.js b/js/src/userviewmodel.js index 643ff40..e5de8ea 100644 --- a/js/src/userviewmodel.js +++ b/js/src/userviewmodel.js @@ -17,7 +17,7 @@ * along with this program; if not, see . */ -import { auth, lang } from './initializer.js'; +import { lang as $, auth } from './initializer.js'; import ViewModel from './viewmodel.js'; import uSelect from './select.js'; import uUser from './user.js'; @@ -40,7 +40,7 @@ export default class UserViewModel extends ViewModel { }); /** @type HTMLSelectElement */ const listEl = document.querySelector('#user'); - this.select = new uSelect(listEl, lang.strings['suser'], `- ${lang.strings['allusers']} -`); + this.select = new uSelect(listEl, $._('suser'), `- ${$._('allusers')} -`); this.state = state; } @@ -61,7 +61,7 @@ export default class UserViewModel extends ViewModel { this.model.currentUserId = userId; } }) - .catch((e) => { uUtils.error(e, `${lang.strings['actionfailure']}\n${e.message}`); }); + .catch((e) => { uUtils.error(e, `${$._('actionfailure')}\n${e.message}`); }); } /** diff --git a/js/test/api_gmaps.test.js b/js/test/api_gmaps.test.js index e95bf33..ad5bade 100644 --- a/js/test/api_gmaps.test.js +++ b/js/test/api_gmaps.test.js @@ -21,6 +21,7 @@ import * as gmStub from './googlemaps.stub.js'; import { config, lang } from '../src/initializer.js' import GoogleMapsApi from '../src/mapapi/api_gmaps.js'; import TrackFactory from './helpers/trackfactory.js'; +import uObserve from '../src/observe.js'; import uUtils from '../src/utils.js'; describe('Google Maps map API tests', () => { @@ -45,10 +46,14 @@ describe('Google Maps map API tests', () => { spyOn(google.maps, 'Polyline').and.callThrough(); spyOnProperty(GoogleMapsApi, 'loadTimeoutMs', 'get').and.returnValue(loadTimeout); spyOn(window, 'alert'); + spyOn(lang, '_').and.returnValue('{placeholder}'); gmStub.applyPrototypes(); }); - afterEach(() => gmStub.clear()); + afterEach(() => { + gmStub.clear(); + uObserve.unobserveAll(lang); + }); it('should timeout initialization of map engine', (done) => { // given @@ -124,13 +129,13 @@ describe('Google Maps map API tests', () => { it('should fail with authorization error', (done) => { // given spyOn(uUtils, 'loadScript').and.returnValue(Promise.resolve()); - lang.strings['apifailure'] = 'authfailure'; + lang._.and.returnValue('authfailure'); // when api.init() .then(() => done.fail('resolve callback called')) .catch((e) => { // then - expect(e.message).toContain(lang.strings['apifailure']); + expect(e.message).toContain('authfailure'); done(); }); window.gm_authFailure(); @@ -139,7 +144,7 @@ describe('Google Maps map API tests', () => { it('should show alert if authorization error occurs after initialization', (done) => { // given spyOn(uUtils, 'loadScript').and.returnValue(Promise.resolve()); - lang.strings['apifailure'] = 'authfailure'; + lang._.and.returnValue('authfailure'); // when api.init() .then(() => { @@ -151,7 +156,7 @@ describe('Google Maps map API tests', () => { window.gm_authFailure(); expect(window.alert).toHaveBeenCalledTimes(1); - expect(window.alert.calls.mostRecent().args[0]).toContain(lang.strings['apifailure']); + expect(window.alert.calls.mostRecent().args[0]).toContain('authfailure'); }); it('should clean up class fields', () => { diff --git a/js/test/chartviewmodel.test.js b/js/test/chartviewmodel.test.js index b9483a9..e2a245b 100644 --- a/js/test/chartviewmodel.test.js +++ b/js/test/chartviewmodel.test.js @@ -86,6 +86,7 @@ describe('ChartViewModel tests', () => { state = new uState(); vm = new ChartViewModel(state); spyOn(lang, 'unit'); + spyOn(lang, '_').and.returnValue('{placeholder}'); mockChart = jasmine.createSpyObj('mockChart', { 'on': { /* ignored */ }, 'update': { /* ignored */ } diff --git a/js/test/configviewmodel.test.js b/js/test/configviewmodel.test.js index d5198a0..15d9faa 100644 --- a/js/test/configviewmodel.test.js +++ b/js/test/configviewmodel.test.js @@ -17,9 +17,10 @@ * along with this program; if not, see . */ +import { config, lang } from '../src/initializer.js'; import ConfigViewModel from '../src/configviewmodel.js'; import ViewModel from '../src/viewmodel.js'; -import { config } from '../src/initializer.js'; +import uObserve from '../src/observe.js'; import uState from '../src/state.js'; import uUtils from '../src/utils.js'; @@ -87,10 +88,12 @@ describe('ConfigViewModel tests', () => { vm.init(); spyOn(uUtils, 'setCookie').and.returnValue(newInterval); spyOn(ConfigViewModel, 'reload'); + spyOn(lang, '_').and.returnValue('{placeholder}'); }); afterEach(() => { document.body.removeChild(document.querySelector('#fixture')); + uObserve.unobserveAll(lang); }); it('should create instance with state as parameter', () => { diff --git a/js/test/lang.test.js b/js/test/lang.test.js new file mode 100644 index 0000000..d68fd12 --- /dev/null +++ b/js/test/lang.test.js @@ -0,0 +1,167 @@ +/* + * μlogger + * + * Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net) + * + * This is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +import uLang from '../src/lang.js'; + +describe('Lang tests', () => { + + let lang; + let mockConfig; + let mockStrings; + const value = 1000; + + beforeEach(() => { + lang = new uLang(); + mockConfig = { + factorSpeed: 0.33, + unitSpeed: 'units', + factorDistance: 1.3, + unitDistance: 'unitd', + factorDistanceMajor: 0.55, + unitDistanceMajor: 'unitdm', + unitDay: 'unitday' + }; + mockStrings = { + string1: 'łańcuch1', + units: 'jp', + unitd: 'jo', + unitdm: 'jo / 1000', + unitday: 'd' + } + }); + + it('should create instance', () => { + expect(lang.strings).toEqual({}); + expect(lang.config).toBe(null); + }); + + it('should initialize', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang.strings).toBe(mockStrings); + expect(lang.config).toBe(mockConfig); + }); + + it('should return localized string', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang._('string1')).toBe(mockStrings.string1); + }); + + it('should throw error on unknown string', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(() => lang._('unknown_string')).toThrowError(/Unknown/); + }); + + it('should return localized unit', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang.unit('unitSpeed')).toBe(mockStrings.units); + }); + + it('should return localized speed value', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang.getLocaleSpeed(value, false)).toBe(1188); + }); + + it('should return localized speed value with unit', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang.getLocaleSpeed(value, true)).toBe(`1188 ${mockStrings.units}`); + }); + + it('should return localized distance major value', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang.getLocaleDistanceMajor(value, false)).toBe(0.55); + }); + + it('should return localized distance major value with unit', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang.getLocaleDistanceMajor(value, true)).toBe(`0.55 ${mockStrings.unitdm}`); + }); + + it('should return localized distance value', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang.getLocaleDistance(value, false)).toBe(1300); + }); + + it('should return localized distance value with unit', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang.getLocaleDistance(value, true)).toBe(`1300 ${mockStrings.unitd}`); + }); + + it('should return localized altitude value', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang.getLocaleDistance(value, false)).toBe(1300); + }); + + it('should return localized altitude value with unit', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang.getLocaleDistance(value, true)).toBe(`1300 ${mockStrings.unitd}`); + }); + + it('should return localized accuracy value', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang.getLocaleDistance(value, false)).toBe(1300); + }); + + it('should return localized accuracy value with unit', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang.getLocaleDistance(value, true)).toBe(`1300 ${mockStrings.unitd}`); + }); + + it('should return localized time duration', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang.getLocaleDuration(12345)).toBe('03:25:45'); + }); + + it('should return localized time duration with day unit', () => { + // when + lang.init(mockConfig, mockStrings); + // then + expect(lang.getLocaleDuration(123456789)).toBe(`1428 ${mockStrings.unitday} 21:33:09`); + }); + +}); diff --git a/js/test/mapviewmodel.test.js b/js/test/mapviewmodel.test.js index 9c58ab7..b9df30e 100644 --- a/js/test/mapviewmodel.test.js +++ b/js/test/mapviewmodel.test.js @@ -44,7 +44,6 @@ describe('MapViewModel tests', () => { config.reinitialize(); config.mapApi = defaultApi; lang.init(config); - lang.strings['apifailure'] = 'api failure: %s'; mockApi = jasmine.createSpyObj('mockApi', { 'init': Promise.resolve(), 'getBounds': { /* ignored */ }, @@ -57,12 +56,17 @@ describe('MapViewModel tests', () => { state = new uState(); vm = new MapViewModel(state); spyOn(vm, 'getApi').and.returnValue(mockApi); + spyOn(lang, 'getLocaleSpeed'); + spyOn(lang, 'getLocaleDistance'); + spyOn(lang, 'getLocaleDistanceMajor'); + spyOn(lang, '_').and.returnValue('{placeholder}'); bounds = [ 1, 2, 3, 4 ]; track = TrackFactory.getTrack(0); }); afterEach(() => { document.body.removeChild(document.querySelector('#fixture')); + uObserve.unobserveAll(lang); }); it('should create instance', () => { diff --git a/js/test/trackviewmodel.test.js b/js/test/trackviewmodel.test.js index b720c53..ff7cbcc 100644 --- a/js/test/trackviewmodel.test.js +++ b/js/test/trackviewmodel.test.js @@ -83,6 +83,7 @@ describe('TrackViewModel tests', () => { config.reinitialize(); config.interval = 10; lang.init(config); + spyOn(lang, '_').and.returnValue('{placeholder}'); trackEl = document.querySelector('#track'); summaryEl = document.querySelector('#summary'); latestEl = document.querySelector('#latest'); @@ -106,6 +107,7 @@ describe('TrackViewModel tests', () => { afterEach(() => { document.body.removeChild(document.querySelector('#fixture')); + uObserve.unobserveAll(lang); }); it('should create instance with state as parameter', () => {