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.getLocaleDistanceMajor(last.totalMeters, true)}
+ ${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 = ``;
+
+ 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 = 'track1 track2 ';
+ 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 = 'track1 track2 ';
+ 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 = 'track1 track2 ';
+ 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 = 'track1 track2 ';
+ 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 = 'track1 track2 ';
+ 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 = 'track1 track2 ';
+ 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 = 'track1 track2 ';
+ 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 = 'track1 track2 ';
+ 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 = 'track1 track2 ';
+ 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 = 'track1 track2 ';
+ 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 = 'track1 track2 ';
+ 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 = 'track1 track2 ';
+ 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);
+ });
+
+ });
+
+});