From a516ff104fdc43e92fb0aa060f706e89a7c28084 Mon Sep 17 00:00:00 2001 From: Bartek Fabiszewski Date: Sun, 15 Dec 2019 21:43:15 +0100 Subject: [PATCH] Add track view model --- js/src/state.js | 45 ++ js/src/trackviewmodel.js | 316 ++++++++++++++ js/src/utils.js | 13 +- js/test/helpers/trackfactory.js | 5 +- js/test/trackviewmodel.test.js | 753 ++++++++++++++++++++++++++++++++ 5 files changed, 1127 insertions(+), 5 deletions(-) create mode 100644 js/src/state.js create mode 100644 js/src/trackviewmodel.js create mode 100644 js/test/trackviewmodel.test.js diff --git a/js/src/state.js b/js/src/state.js new file mode 100644 index 0000000..7a27050 --- /dev/null +++ b/js/src/state.js @@ -0,0 +1,45 @@ +/* + * μ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 uObserve from './observe.js'; + +/** + * @class + * @property {?uTrack} currentTrack + * @property {?uUser} currentUser + * @property {boolean} showLatest + * @property {boolean} showAllUsers + */ +export default class uState { + + constructor() { + this.currentTrack = null; + this.currentUser = null; + this.showLatest = false; + this.showAllUsers = false; + } + + /** + * @param {string} property + * @param {ObserveCallback} callback + */ + onChanged(property, callback) { + uObserve.observe(this, property, callback); + } +} diff --git a/js/src/trackviewmodel.js b/js/src/trackviewmodel.js new file mode 100644 index 0000000..ceee88b --- /dev/null +++ b/js/src/trackviewmodel.js @@ -0,0 +1,316 @@ +/* + * μ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 { config, lang } from './initializer.js'; +import ViewModel from './viewmodel.js'; +import uObserve from './observe.js'; +import uPositionSet from './positionset.js'; +import uSelect from './select.js'; +import uTrack from './track.js'; +import uUtils from './utils.js'; + +/** + * @class TrackViewModel + */ +export default class TrackViewModel extends ViewModel { + + /** + * @param {uState} state + */ + constructor(state) { + super({ + /** @type {uTrack[]} */ + trackList: [], + /** @type {string} */ + currentTrackId: '', + /** @type {boolean} */ + showLatest: false, + /** @type {boolean} */ + autoReload: false, + /** @type {string} */ + inputFile: false, + // click handlers + /** @type {function} */ + onReload: null, + /** @type {function} */ + onExportGpx: null, + /** @type {function} */ + onExportKml: null, + /** @type {function} */ + onImportGpx: null, + /** @type {function} */ + onSetInterval: null + }); + this.setClickHandlers(); + /** @type HTMLSelectElement */ + const listEl = document.querySelector('#track'); + this.summaryEl = document.querySelector('#summary'); + this.importEl = document.querySelector('#input-file'); + this.intervalEl = document.querySelector('#interval'); + this.select = new uSelect(listEl); + this.state = state; + this.timerId = 0; + this.setObservers(); + this.init(); + } + + setClickHandlers() { + this.model.onReload = () => this.onReload(); + const exportCb = (type) => () => { + if (this.state.currentTrack) { + this.state.currentTrack.export(type); + } + }; + this.model.onExportGpx = exportCb('gpx'); + this.model.onExportKml = exportCb('kml'); + this.model.onImportGpx = () => this.importEl.click(); + this.model.onSetInterval = () => this.setAutoReloadInterval(); + } + + setObservers() { + this.onChanged('trackList', (list) => { this.select.setOptions(list); }); + this.onChanged('currentTrackId', (listValue) => { + this.onTrackSelect(listValue); + }); + this.onChanged('inputFile', (file) => { + if (file) { this.onImport(); } + }); + this.onChanged('autoReload', (reload) => { + this.autoReload(reload); + }); + this.onChanged('showLatest', (showLatest) => { + this.state.showLatest = showLatest; + this.onReload(true); + }); + this.state.onChanged('currentUser', (user) => { + if (user) { + this.loadTrackList(); + } else { + this.model.currentTrackId = ''; + this.model.trackList = []; + } + }); + this.state.onChanged('currentTrack', (track) => { + this.renderSummary(); + if (track) { + uObserve.observe(track, 'positions', () => { + this.renderSummary(); + }); + } + }); + this.state.onChanged('showAllUsers', (showAll) => { + if (showAll) { + this.loadAllUsersPosition(); + } + }); + } + + /** + * Reload or update track view + * @param {boolean} clear Reload if true, update current track otherwise + */ + onReload(clear = false) { + if (this.state.showLatest) { + if (this.state.showAllUsers) { + this.loadAllUsersPosition(); + } else if (this.state.currentUser) { + this.onUserLastPosition(); + } + } else if (this.state.currentTrack instanceof uTrack) { + this.onTrackUpdate(clear); + } else if (this.state.currentTrack instanceof uPositionSet) { + this.state.currentTrack = null; + } else if (this.state.currentUser) { + this.loadTrackList(); + } + } + + /** + * Handle import + */ + onImport() { + 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)); + return; + } + uTrack.import(form) + .then((trackList) => { + if (trackList.length) { + if (trackList.length > 1) { + alert(uUtils.sprintf(lang.strings['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}`)) + .finally(() => { + this.model.inputFile = ''; + }); + } + + /** + * Handle track change + * @param {string} listValue Track list selected option + */ + onTrackSelect(listValue) { + /** @type {(uTrack|undefined)} */ + const track = this.model.trackList.find((_track) => _track.listValue === listValue); + if (!track) { + this.state.currentTrack = null; + } else if (!track.isEqualTo(this.state.currentTrack)) { + track.fetchPositions().then(() => { + console.log(`currentTrack id: ${track.id}, loaded ${track.length} positions`); + this.state.currentTrack = track; + if (this.model.showLatest) { + this.model.showLatest = false; + } + }) + .catch((e) => { uUtils.error(e, `${lang.strings['actionfailure']}\n${e.message}`); }); + } + } + + /** + * Handle track update + * @param {boolean=} clear + */ + onTrackUpdate(clear) { + if (clear) { + this.state.currentTrack.clear(); + } + this.state.currentTrack.fetchPositions() + .catch((e) => { uUtils.error(e, `${lang.strings['actionfailure']}\n${e.message}`); }); + } + + /** + * Handle user last position request + */ + onUserLastPosition() { + this.state.currentUser.fetchLastPosition() + .then((_track) => { + if (_track) { + if (!this.model.trackList.find((listItem) => listItem.listValue === _track.listValue)) { + this.model.trackList.unshift(_track); + } + this.state.currentTrack = _track; + this.model.currentTrackId = _track.listValue; + } + }) + .catch((e) => { uUtils.error(e, `${lang.strings['actionfailure']}\n${e.message}`); }); + } + + /** + * Handle last position of all users request + */ + loadAllUsersPosition() { + uPositionSet.fetchLatest() + .then((_track) => { + if (_track) { + this.model.trackList = []; + this.model.currentTrackId = ''; + this.state.currentTrack = _track; + } + }) + .catch((e) => { uUtils.error(e, `${lang.strings['actionfailure']}\n${e.message}`); }); + } + + loadTrackList() { + uTrack.fetchList(this.state.currentUser) + .then((_tracks) => { + this.model.trackList = _tracks; + if (_tracks.length) { + if (this.state.showLatest) { + this.onUserLastPosition(); + } else { + // autoload first track in list + this.model.currentTrackId = _tracks[0].listValue; + } + } else { + this.model.currentTrackId = ''; + } + }) + .catch((e) => { uUtils.error(e, `${lang.strings['actionfailure']}\n${e.message}`); }); + } + + init() { + this.bindAll(); + } + + /** + * @param {boolean} start + */ + autoReload(start) { + if (start) { + this.startAutoReload(); + } else { + this.stopAutoReload(); + } + } + + startAutoReload() { + this.timerId = setInterval(() => this.onReload(), config.interval * 1000); + } + + stopAutoReload() { + clearInterval(this.timerId); + this.timerId = 0; + this.model.autoReload = false; + } + + setAutoReloadInterval() { + const interval = parseInt(prompt(lang.strings['newinterval'])); + if (!isNaN(interval) && interval !== config.interval) { + config.interval = interval; + this.intervalEl.innerHTML = config.interval.toString(); + // if live tracking on, reload with new interval + if (this.timerId) { + this.stopAutoReload(); + this.startAutoReload(); + } + } + } + + renderSummary() { + if (!this.state.currentTrack || !this.state.currentTrack.hasPositions) { + this.summaryEl.innerHTML = ''; + return; + } + const last = this.state.currentTrack.positions[this.state.currentTrack.length - 1]; + + if (this.state.showLatest) { + const today = new Date(); + const date = new Date(last.timestamp * 1000); + const dateTime = uUtils.getTimeString(date); + const dateString = (date.toDateString() !== today.toDateString()) ? `${dateTime.date}
` : ''; + const timeString = `${dateTime.time}${dateTime.zone}`; + this.summaryEl.innerHTML = ` + + ${dateString} + ${timeString}`; + } else { + this.summaryEl.innerHTML = ` + +
${lang.strings['tdistance']} ${lang.getLocaleDistanceMajor(last.totalMeters, true)}
+
${lang.strings['ttime']} ${lang.getLocaleDuration(last.totalSeconds)}
`; + } + } + +} diff --git a/js/src/utils.js b/js/src/utils.js index f362b9b..218ab58 100644 --- a/js/src/utils.js +++ b/js/src/utils.js @@ -296,11 +296,18 @@ export default class uUtils { } /** - * @param {Error} e - * @param {string} message + * @param {(Error|string)} e + * @param {string=} message */ static error(e, message) { - console.error(`${e.name}: ${e.message} (${e.stack})`); + let details; + if (e instanceof Error) { + details = `${e.name}: ${e.message} (${e.stack})`; + } else { + details = e; + message = e; + } + console.error(details); alert(message); } } diff --git a/js/test/helpers/trackfactory.js b/js/test/helpers/trackfactory.js index dd42cc8..7d120be 100644 --- a/js/test/helpers/trackfactory.js +++ b/js/test/helpers/trackfactory.js @@ -40,14 +40,15 @@ export default class TrackFactory { track = new uPositionSet(); } if (length) { - track.positions = []; + const positions = []; let lat = 21.01; let lon = 52.23; for (let i = 0; i < length; i++) { - track.positions.push(this.getPosition(i + 1, lat, lon)); + positions.push(this.getPosition(i + 1, lat, lon)); lat += 0.5; lon += 0.5; } + track.fromJson(positions, true); } return track; } diff --git a/js/test/trackviewmodel.test.js b/js/test/trackviewmodel.test.js new file mode 100644 index 0000000..5616b0a --- /dev/null +++ b/js/test/trackviewmodel.test.js @@ -0,0 +1,753 @@ +/* + * μ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 { config, lang } from '../src/initializer.js'; +import TrackFactory from './helpers/trackfactory.js'; +import TrackViewModel from '../src/trackviewmodel.js'; +import ViewModel from '../src/viewmodel.js'; +import uObserve from '../src/observe.js'; +import uPositionSet from '../src/positionset.js'; +import uState from '../src/state.js'; +import uTrack from '../src/track.js'; +import uUser from '../src/user.js'; +import uUtils from '../src/utils.js'; + +describe('TrackViewModel tests', () => { + + let vm; + let state; + /** @type {HTMLSelectElement} */ + let trackEl; + /** @type {HTMLDivElement} */ + let summaryEl; + /** @type {HTMLInputElement} */ + let latestEl; + /** @type {HTMLAnchorElement} */ + let exportKmlEl; + /** @type {HTMLAnchorElement} */ + let exportGpxEl; + /** @type {HTMLAnchorElement} */ + let importGpxEl; + /** @type {HTMLAnchorElement} */ + let forceReloadEl; + /** @type {HTMLInputElement} */ + let autoReloadEl; + /** @type {HTMLInputElement} */ + let inputFileEl; + /** @type {HTMLSpanElement} */ + let intervalEl; + /** @type {HTMLAnchorElement} */ + let setIntervalEl; + let tracks; + let track1; + let track2; + let positions; + let user; + const interval = 10; + const MAX_FILE_SIZE = 10; + + beforeEach(() => { + const fixture = `
+
+ + + + (${interval} s) + setInterval + reload +
+
+
+ kml + gpx +
+
+
+ + +
+ gpx +
+
`; + + document.body.insertAdjacentHTML('afterbegin', fixture); + config.initialize(); + lang.init(config); + trackEl = document.querySelector('#track'); + summaryEl = document.querySelector('#summary'); + latestEl = document.querySelector('#latest'); + exportKmlEl = document.querySelector('#export-kml'); + exportGpxEl = document.querySelector('#export-gpx'); + importGpxEl = document.querySelector('#import-gpx'); + forceReloadEl = document.querySelector('#force-reload'); + inputFileEl = document.querySelector('#input-file'); + intervalEl = document.querySelector('#interval'); + setIntervalEl = document.querySelector('#set-interval'); + autoReloadEl = document.querySelector('#auto-reload'); + state = new uState(); + vm = new TrackViewModel(state); + track1 = TrackFactory.getTrack(0, { id: 1, name: 'track1' }); + track2 = TrackFactory.getTrack(0, { id: 2, name: 'track2' }); + tracks = [ + track1, + track2 + ]; + positions = [ TrackFactory.getPosition() ]; + user = new uUser(1, 'testUser'); + }); + + afterEach(() => { + document.body.removeChild(document.querySelector('#fixture')); + }); + + it('should create instance with state as parameter', () => { + // when + const trackViewModel = new TrackViewModel(state); + // then + expect(trackViewModel).toBeInstanceOf(ViewModel); + expect(trackViewModel.summaryEl).toBeInstanceOf(HTMLDivElement); + expect(trackViewModel.importEl).toBeInstanceOf(HTMLInputElement); + expect(trackViewModel.select.element).toBeInstanceOf(HTMLSelectElement); + expect(trackViewModel.state).toBe(state); + }); + + it('should load track list and fetch first track on current user change', (done) => { + // given + spyOn(uTrack, 'fetchList').and.returnValue(Promise.resolve(tracks)); + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + // when + state.currentUser = user; + // then + expect(uObserve.isObserved(vm.model, 'trackList')).toBe(true); + setTimeout(() => { + expect(uTrack.fetchList).toHaveBeenCalledWith(state.currentUser); + expect(uPositionSet.fetch).toHaveBeenCalledWith({ userid: user.id, trackid: track1.id }); + expect(trackEl.options.length).toBe(tracks.length); + expect(trackEl.options[0].selected).toBe(true); + expect(trackEl.options[0].value).toBe(track1.listValue); + expect(state.currentTrack).toBe(track1); + expect(state.currentTrack.length).toBe(positions.length); + expect(vm.model.currentTrackId).toBe(track1.listValue); + expect(summaryEl.innerText.length).not.toBe(0); + done(); + }, 100); + }); + + it('should clear current track on empty track list loaded on current user change', (done) => { + // given + spyOn(uTrack, 'fetchList').and.returnValue(Promise.resolve([])); + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + // when + state.currentUser = user; + // then + setTimeout(() => { + expect(uTrack.fetchList).toHaveBeenCalledWith(state.currentUser); + expect(uPositionSet.fetch).not.toHaveBeenCalled(); + expect(trackEl.options.length).toBe(0); + expect(state.currentTrack).toBe(null); + expect(vm.model.currentTrackId).toBe(''); + expect(summaryEl.innerText.length).toBe(0); + done(); + }, 100); + }); + + it('should load track list, load user latest position and select coresponding track on current user change', (done) => { + // given + positions[0].trackid = track2.id; + positions[0].trackname = track2.name; + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + spyOn(uTrack, 'fetchList').and.returnValue(Promise.resolve(tracks)); + uObserve.setSilently(vm.model, 'showLatest', true); + uObserve.setSilently(state, 'showLatest', true); + // when + state.currentUser = user; + // then + setTimeout(() => { + expect(uTrack.fetchList).toHaveBeenCalledWith(state.currentUser); + expect(uPositionSet.fetch).toHaveBeenCalledWith({ userid: user.id, last: true }); + expect(trackEl.options.length).toBe(tracks.length); + expect(trackEl.options[1].selected).toBe(true); + expect(trackEl.options[1].value).toBe(track2.listValue); + expect(state.currentTrack.id).toEqual(track2.id); + expect(state.currentTrack.name).toEqual(track2.name); + expect(state.currentTrack.length).toBe(positions.length); + expect(vm.model.currentTrackId).toBe(track2.listValue); + expect(summaryEl.innerText.length).not.toBe(0); + done(); + }, 100); + }); + + it('should clear track when no user is selected on user list', (done) => { + // given + const options = ''; + trackEl.insertAdjacentHTML('afterbegin', options); + uObserve.setSilently(vm.model, 'trackList', tracks); + uObserve.setSilently(vm.model, 'currentTrackId', track1.listValue); + uObserve.setSilently(state, 'currentTrack', track1); + uObserve.setSilently(state, 'currentUser', user); + // when + state.currentUser = null; + // then + setTimeout(() => { + expect(trackEl.options.length).toBe(0); + expect(state.currentTrack).toBe(null); + expect(vm.model.currentTrackId).toBe(''); + expect(summaryEl.innerText.length).toBe(0); + done(); + }, 100); + }); + + it('should load track when selected in form select options', (done) => { + // given + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + const options = ''; + trackEl.insertAdjacentHTML('afterbegin', options); + uObserve.setSilently(vm.model, 'trackList', tracks); + uObserve.setSilently(vm.model, 'currentTrackId', track1.listValue); + uObserve.setSilently(state, 'currentTrack', track1); + uObserve.setSilently(state, 'currentUser', user); + // when + trackEl.value = track2.listValue; + trackEl.dispatchEvent(new Event('change')); + // then + setTimeout(() => { + expect(uPositionSet.fetch).toHaveBeenCalledWith({ userid: user.id, trackid: track2.id }); + expect(trackEl.options.length).toBe(tracks.length); + expect(trackEl.options[0].value).toBe(track1.listValue); + expect(trackEl.options[1].value).toBe(track2.listValue); + expect(trackEl.options[1].selected).toBe(true); + expect(state.currentTrack).toBe(track2); + expect(state.currentTrack.length).toBe(positions.length); + expect(vm.model.currentTrackId).toBe(track2.listValue); + expect(summaryEl.innerText.length).not.toBe(0); + done(); + }, 100); + }); + + it('should load user latest position when "show latest" is checked and insert new track to track list', (done) => { + // given + positions[0].trackid = 100; + positions[0].trackname = 'new track'; + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + const options = ''; + trackEl.insertAdjacentHTML('afterbegin', options); + const optLength = trackEl.options.length; + uObserve.setSilently(vm.model, 'trackList', tracks); + uObserve.setSilently(vm.model, 'currentTrackId', track1.listValue); + uObserve.setSilently(state, 'currentTrack', track1); + uObserve.setSilently(state, 'currentUser', user); + // when + latestEl.checked = true; + latestEl.dispatchEvent(new Event('change')); + // then + setTimeout(() => { + expect(uPositionSet.fetch).toHaveBeenCalledWith({ userid: user.id, last: true }); + expect(state.currentTrack.id).toBe(positions[0].trackid); + expect(state.currentTrack.name).toBe(positions[0].trackname); + expect(state.currentTrack.length).toBe(positions.length); + expect(trackEl.options.length).toBe(optLength + 1); + expect(trackEl.options.length).toBe(tracks.length); + expect(trackEl.value).toBe(state.currentTrack.listValue); + expect(trackEl.options[0].value).toBe(state.currentTrack.listValue); + expect(trackEl.options[0].selected).toBe(true); + expect(state.showLatest).toBe(true); + expect(vm.model.currentTrackId).toBe(state.currentTrack.listValue); + expect(summaryEl.innerText.length).not.toBe(0); + done(); + }, 100); + }); + + + it('should load user latest position when "show latest" is checked and select respective track in list', (done) => { + // given + positions[0].trackid = track2.id; + positions[0].trackname = track2.name; + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + const options = ''; + trackEl.insertAdjacentHTML('afterbegin', options); + const optLength = trackEl.options.length; + uObserve.setSilently(vm.model, 'trackList', tracks); + uObserve.setSilently(vm.model, 'currentTrackId', track1.listValue); + uObserve.setSilently(state, 'currentTrack', track1); + uObserve.setSilently(state, 'currentUser', user); + // when + latestEl.checked = true; + latestEl.dispatchEvent(new Event('change')); + // then + setTimeout(() => { + expect(uPositionSet.fetch).toHaveBeenCalledWith({ userid: user.id, last: true }); + expect(state.currentTrack.id).toBe(track2.id); + expect(state.currentTrack.name).toBe(track2.name); + expect(state.currentTrack.length).toBe(positions.length); + expect(trackEl.options.length).toBe(optLength); + expect(trackEl.options.length).toBe(tracks.length); + expect(trackEl.value).toBe(state.currentTrack.listValue); + expect(trackEl.options[1].value).toBe(state.currentTrack.listValue); + expect(trackEl.options[1].selected).toBe(true); + expect(state.showLatest).toBe(true); + expect(vm.model.currentTrackId).toBe(state.currentTrack.listValue); + expect(summaryEl.innerText.length).not.toBe(0); + done(); + }, 100); + }); + + it('should load all current track positions when "show latest" is unchecked', (done) => { + // given + positions[0].trackid = track1.id; + positions[0].trackname = track1.name; + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + const options = ''; + trackEl.insertAdjacentHTML('afterbegin', options); + const optLength = trackEl.options.length; + uObserve.setSilently(vm.model, 'trackList', tracks); + uObserve.setSilently(vm.model, 'currentTrackId', track1.listValue); + uObserve.setSilently(vm.model, 'showLatest', true); + uObserve.setSilently(state, 'currentUser', user); + uObserve.setSilently(state, 'showLatest', true); + state.currentTrack = track1; + latestEl.checked = true; + // when + latestEl.checked = false; + latestEl.dispatchEvent(new Event('change')); + // then + setTimeout(() => { + expect(uPositionSet.fetch).toHaveBeenCalledWith({ userid: user.id, trackid: track1.id }); + expect(state.currentTrack.id).toBe(track1.id); + expect(state.currentTrack.name).toBe(track1.name); + expect(state.currentTrack.length).toBe(positions.length); + expect(trackEl.options.length).toBe(optLength); + expect(trackEl.options.length).toBe(tracks.length); + expect(trackEl.value).toBe(state.currentTrack.listValue); + expect(trackEl.options[0].value).toBe(state.currentTrack.listValue); + expect(trackEl.options[0].selected).toBe(true); + expect(state.showLatest).toBe(false); + expect(vm.model.currentTrackId).toBe(state.currentTrack.listValue); + expect(summaryEl.innerText.length).not.toBe(0); + done(); + }, 100); + }); + + it('should clear track list and fetch all users positions on "all users" option selected', (done) => { + // given + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + const options = ''; + trackEl.insertAdjacentHTML('afterbegin', options); + uObserve.setSilently(vm.model, 'trackList', tracks); + uObserve.setSilently(vm.model, 'currentTrackId', track1.listValue); + uObserve.setSilently(state, 'currentTrack', track1); + uObserve.setSilently(state, 'currentUser', user); + uObserve.setSilently(state, 'showLatest', true); + latestEl.checked = true; + // when + state.showAllUsers = true; + // then + setTimeout(() => { + expect(uPositionSet.fetch).toHaveBeenCalledWith({ last: true }); + expect(trackEl.options.length).toBe(0); + // noinspection JSUnresolvedFunction + expect(state.currentTrack).not.toBeInstanceOf(uTrack); + expect(state.currentTrack).toBeInstanceOf(uPositionSet); + expect(state.currentTrack.positions.length).toBe(positions.length); + expect(state.currentTrack.positions[0].id).toBe(positions[0].id); + expect(state.currentTrack.length).toBe(positions.length); + expect(vm.model.currentTrackId).toBe(''); + expect(summaryEl.innerText.length).not.toBe(0); + done(); + }, 100); + }); + + it('should clear current track if "show latest" is unchecked when "all users" is set', (done) => { + // given + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + uObserve.setSilently(vm.model, 'trackList', []); + uObserve.setSilently(vm.model, 'currentTrackId', ''); + uObserve.setSilently(vm.model, 'showLatest', true); + uObserve.setSilently(state, 'currentUser', null); + uObserve.setSilently(state, 'showLatest', true); + uObserve.setSilently(state, 'showAllUsers', true); + state.currentTrack = TrackFactory.getPositionSet(1); + latestEl.checked = true; + // when + latestEl.checked = false; + latestEl.dispatchEvent(new Event('change')); + // then + setTimeout(() => { + expect(uPositionSet.fetch).not.toHaveBeenCalled(); + expect(state.currentTrack).toBe(null); + expect(vm.model.currentTrackId).toBe(''); + expect(trackEl.options.length).toBe(0); + expect(state.showLatest).toBe(false); + expect(summaryEl.innerText.length).toBe(0); + done(); + }, 100); + }); + + it('should uncheck "show latest" when selected track in form select options', (done) => { + // given + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + const options = ''; + trackEl.insertAdjacentHTML('afterbegin', options); + uObserve.setSilently(vm.model, 'trackList', tracks); + uObserve.setSilently(vm.model, 'currentTrackId', track1.listValue); + uObserve.setSilently(vm.model, 'showLatest', true); + uObserve.setSilently(state, 'currentTrack', track1); + uObserve.setSilently(state, 'currentUser', user); + uObserve.setSilently(state, 'showLatest', true); + latestEl.checked = true; + // when + trackEl.value = track2.listValue; + trackEl.dispatchEvent(new Event('change')); + // then + setTimeout(() => { + expect(state.showLatest).toBe(false); + expect(vm.model.showLatest).toBe(false); + expect(latestEl.checked).toBe(false); + done(); + }, 100); + }); + + it('should export track to KML on link click', (done) => { + // given + spyOn(track1, 'export'); + uObserve.setSilently(state, 'currentTrack', track1); + // when + exportKmlEl.click(); + // then + setTimeout(() => { + expect(track1.export).toHaveBeenCalledWith('kml'); + done(); + }, 100); + }); + + it('should export track to GPX on link click', (done) => { + // given + spyOn(track1, 'export'); + uObserve.setSilently(state, 'currentTrack', track1); + // when + exportGpxEl.click(); + // then + setTimeout(() => { + expect(track1.export).toHaveBeenCalledWith('gpx'); + done(); + }, 100); + }); + + it('should import tracks on link click', (done) => { + // given + const imported = [ + TrackFactory.getTrack(0, { id: 3, name: 'track3', user: user }), + TrackFactory.getTrack(0, { id: 4, name: 'track4', user: user }) + ]; + const file = new File([ 'blob' ], '/path/filepath.gpx'); + spyOn(uTrack, 'import').and.callFake((form) => { + expect(form.elements['gpx'].files[0]).toEqual(file); + return Promise.resolve(imported); + }); + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + spyOn(uUtils, 'sprintf'); + const options = ''; + trackEl.insertAdjacentHTML('afterbegin', options); + const optLength = trackEl.options.length; + uObserve.setSilently(vm.model, 'trackList', tracks); + uObserve.setSilently(vm.model, 'currentTrackId', track1.listValue); + uObserve.setSilently(state, 'currentTrack', track1); + uObserve.setSilently(state, 'currentUser', user); + inputFileEl.onclick = () => { + const dt = new DataTransfer(); + dt.items.add(file); + inputFileEl.files = dt.files; + inputFileEl.dispatchEvent(new Event('change')); + }; + // when + importGpxEl.click(); + // then + setTimeout(() => { + expect(uTrack.import).toHaveBeenCalledTimes(1); + expect(uTrack.import).toHaveBeenCalledWith(jasmine.any(HTMLFormElement)); + expect(state.currentTrack).toBe(imported[0]); + expect(vm.model.currentTrackId).toBe(imported[0].listValue); + expect(state.currentTrack.length).toBe(positions.length); + expect(uUtils.sprintf.calls.mostRecent().args[1]).toBe(imported.length); + expect(trackEl.options.length).toBe(optLength + imported.length); + expect(vm.model.trackList.length).toBe(optLength + imported.length); + expect(vm.model.inputFile).toBe(''); + expect(inputFileEl.files.length).toBe(0); + done(); + }, 100); + }); + + it('should raise error on file size above MAX_FILE_SIZE limit on link click', (done) => { + // given + const imported = [ + TrackFactory.getTrack(0, { id: 3, name: 'track3', user: user }), + TrackFactory.getTrack(0, { id: 4, name: 'track4', user: user }) + ]; + spyOn(uTrack, 'import').and.returnValue(Promise.resolve(imported)); + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + spyOn(uUtils, 'sprintf'); + spyOn(uUtils, 'error'); + const options = ''; + trackEl.insertAdjacentHTML('afterbegin', options); + const optLength = trackEl.options.length; + uObserve.setSilently(vm.model, 'trackList', tracks); + uObserve.setSilently(vm.model, 'currentTrackId', track1.listValue); + uObserve.setSilently(state, 'currentTrack', track1); + uObserve.setSilently(state, 'currentUser', user); + inputFileEl.onclick = () => { + const dt = new DataTransfer(); + dt.items.add(new File([ '12345678901' ], 'filepath.gpx')); + inputFileEl.files = dt.files; + inputFileEl.dispatchEvent(new Event('change')); + }; + // when + importGpxEl.click(); + // then + setTimeout(() => { + expect(uTrack.import).not.toHaveBeenCalled(); + expect(state.currentTrack).toBe(track1); + expect(vm.model.currentTrackId).toBe(track1.listValue); + expect(uUtils.sprintf.calls.mostRecent().args[1]).toBe(MAX_FILE_SIZE.toString()); + expect(trackEl.options.length).toBe(optLength); + expect(vm.model.trackList.length).toBe(optLength); + done(); + }, 100); + }); + + it('should get interval value from user prompt on interval click', (done) => { + // given + const newInterval = 99; + spyOn(window, 'prompt').and.returnValue(newInterval); + spyOn(vm, 'stopAutoReload'); + spyOn(vm, 'startAutoReload'); + config.interval = interval; + vm.timerId = 0; + // when + setIntervalEl.click(); + // then + setTimeout(() => { + expect(intervalEl.innerHTML).toBe(newInterval.toString()); + expect(config.interval).toBe(newInterval); + expect(vm.stopAutoReload).not.toHaveBeenCalled(); + expect(vm.startAutoReload).not.toHaveBeenCalled(); + done(); + }, 100); + }); + + it('should get interval value from user prompt on interval click and restart running auto-reload', (done) => { + // given + const newInterval = 99; + spyOn(window, 'prompt').and.returnValue(newInterval); + spyOn(vm, 'stopAutoReload'); + spyOn(vm, 'startAutoReload'); + config.interval = interval; + vm.timerId = 1; + // when + setIntervalEl.click(); + // then + setTimeout(() => { + expect(intervalEl.innerHTML).toBe(newInterval.toString()); + expect(config.interval).toBe(newInterval); + expect(vm.stopAutoReload).toHaveBeenCalledTimes(1); + expect(vm.startAutoReload).toHaveBeenCalledTimes(1); + done(); + }, 100); + }); + + it('should start auto-reload on checkbox checked and stop on checkbox unchecked', (done) => { + // given + spyOn(vm, 'onReload').and.callFake(() => { + // then + expect(vm.model.autoReload).toBe(true); + autoReloadEl.checked = false; + autoReloadEl.dispatchEvent(new Event('change')); + }); + autoReloadEl.checked = false; + config.interval = 0.001; + vm.timerId = 0; + // when + autoReloadEl.checked = true; + autoReloadEl.dispatchEvent(new Event('change')); + // then + setTimeout(() => { + expect(vm.onReload).toHaveBeenCalledTimes(1); + expect(vm.model.autoReload).toBe(false); + expect(autoReloadEl.checked).toBe(false); + done(); + }, 100); + }); + + describe('on reload clicked', () => { + + it('should reload selected track', (done) => { + // given + track1 = TrackFactory.getTrack(2, { id: 1, name: 'track1' }); + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + const options = ''; + trackEl.insertAdjacentHTML('afterbegin', options); + const optLength = trackEl.options.length; + const posLength = track1.length; + uObserve.setSilently(vm.model, 'trackList', [ track1, track2 ]); + uObserve.setSilently(vm.model, 'currentTrackId', track1.listValue); + uObserve.setSilently(state, 'currentTrack', track1); + uObserve.setSilently(state, 'currentUser', user); + // when + forceReloadEl.click(); + // then + setTimeout(() => { + expect(uPositionSet.fetch).toHaveBeenCalledWith({ userid: user.id, trackid: track1.id, afterid: track1.maxId }); + expect(state.currentTrack.length).toBe(posLength + positions.length); + expect(trackEl.options.length).toBe(optLength); + expect(trackEl.value).toBe(track1.listValue); + done(); + }, 100); + }); + + it('should fetch user latest position if "show latest" is checked', (done) => { + // given + track1 = TrackFactory.getTrack(1, { id: 1, name: 'track1' }); + positions[0].trackid = track1.id; + positions[0].trackname = track1.name; + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + const options = ''; + trackEl.insertAdjacentHTML('afterbegin', options); + const optLength = trackEl.options.length; + uObserve.setSilently(vm.model, 'trackList', [ track1, track2 ]); + uObserve.setSilently(vm.model, 'currentTrackId', track1.listValue); + uObserve.setSilently(vm.model, 'showLatest', true); + uObserve.setSilently(state, 'currentTrack', track1); + uObserve.setSilently(state, 'currentUser', user); + uObserve.setSilently(state, 'showLatest', true); + latestEl.checked = true; + // when + forceReloadEl.click(); + // then + setTimeout(() => { + expect(uPositionSet.fetch).toHaveBeenCalledWith({ userid: user.id, last: true }); + expect(state.currentTrack.id).toEqual(track1.id); + expect(state.currentTrack.name).toEqual(track1.name); + expect(state.currentTrack.length).toBe(1); + expect(trackEl.options.length).toBe(optLength); + expect(trackEl.value).toBe(positions[0].trackid.toString()); + done(); + }, 100); + }); + + it('should fetch user latest position if "show latest" is checked and add track if position is on a new track', (done) => { + // given + track1 = TrackFactory.getTrack(1, { id: 1, name: 'track1' }); + positions[0].trackid = 100; + positions[0].trackname = 'track100'; + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(positions)); + const options = ''; + trackEl.insertAdjacentHTML('afterbegin', options); + const optLength = trackEl.options.length; + uObserve.setSilently(vm.model, 'trackList', [ track1, track2 ]); + uObserve.setSilently(vm.model, 'currentTrackId', track1.listValue); + uObserve.setSilently(vm.model, 'showLatest', true); + uObserve.setSilently(state, 'currentTrack', track1); + uObserve.setSilently(state, 'currentUser', user); + uObserve.setSilently(state, 'showLatest', true); + latestEl.checked = true; + // when + forceReloadEl.click(); + // then + setTimeout(() => { + expect(uPositionSet.fetch).toHaveBeenCalledWith({ userid: user.id, last: true }); + expect(state.currentTrack.id).toEqual(positions[0].trackid); + expect(state.currentTrack.name).toEqual(positions[0].trackname); + expect(state.currentTrack.length).toBe(1); + expect(trackEl.options.length).toBe(optLength + 1); + expect(trackEl.value).toBe(positions[0].trackid.toString()); + done(); + }, 100); + }); + + it('should fetch all users latest position if "all users" is selected', (done) => { + // given + const set = TrackFactory.getPositionSet(2, { id: 1, name: 'track1' }); + set.positions[0].trackid = track1.id; + set.positions[0].trackname = track1.name; + set.positions[1].trackid = track2.id; + set.positions[1].trackname = track2.name; + spyOn(uPositionSet, 'fetch').and.returnValue(Promise.resolve(set.positions)); + uObserve.setSilently(vm.model, 'trackList', []); + uObserve.setSilently(vm.model, 'currentTrackId', ''); + uObserve.setSilently(vm.model, 'showLatest', true); + uObserve.setSilently(state, 'currentTrack', null); + uObserve.setSilently(state, 'currentUser', null); + uObserve.setSilently(state, 'showLatest', true); + uObserve.setSilently(state, 'showAllUsers', true); + latestEl.checked = true; + // when + forceReloadEl.click(); + // then + setTimeout(() => { + expect(uPositionSet.fetch).toHaveBeenCalledWith({ last: true }); + expect(state.currentTrack.length).toEqual(set.length); + expect(state.currentTrack.positions[0]).toEqual(set.positions[0]); + expect(state.currentTrack.positions[1]).toEqual(set.positions[1]); + expect(trackEl.options.length).toBe(0); + expect(trackEl.value).toBe(''); + done(); + }, 100); + }); + + it('should fetch track list if user is selected and no track is selected', (done) => { + // given + spyOn(uTrack, 'fetchList').and.returnValue(Promise.resolve([])); + uObserve.setSilently(vm.model, 'trackList', []); + uObserve.setSilently(vm.model, 'currentTrackId', ''); + uObserve.setSilently(state, 'currentTrack', null); + uObserve.setSilently(state, 'currentUser', user); + // when + forceReloadEl.click(); + // then + setTimeout(() => { + expect(uTrack.fetchList).toHaveBeenCalledWith(user); + expect(state.currentTrack).toBe(null); + expect(trackEl.options.length).toBe(0); + expect(trackEl.value).toBe(''); + done(); + }, 100); + }); + + it('should do nothing if no user is selected and no track is selected', (done) => { + // given + spyOn(uTrack, 'fetchList'); + spyOn(uPositionSet, 'fetch'); + uObserve.setSilently(vm.model, 'trackList', []); + uObserve.setSilently(vm.model, 'currentTrackId', ''); + uObserve.setSilently(state, 'currentTrack', null); + uObserve.setSilently(state, 'currentUser', null); + // when + forceReloadEl.click(); + // then + setTimeout(() => { + expect(uTrack.fetchList).not.toHaveBeenCalled(); + expect(uPositionSet.fetch).not.toHaveBeenCalled(); + expect(state.currentTrack).toBe(null); + expect(trackEl.options.length).toBe(0); + expect(trackEl.value).toBe(''); + done(); + }, 100); + }); + + }); + +});