summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Webster <awebster@arcx.com>2018-01-25 15:23:08 -0500
committerAndrew Webster <awebster@arcx.com>2018-02-13 10:22:21 -0500
commite0750f9b2c1495473fdcbb2f22dd97961803bc81 (patch)
treebc4fac6974d6d17a97a5de9b2f0d216632a5173b
parent37b4d13db81e0e80e117c07b86ff98714c7b6b1a (diff)
downloadnovnc-e0750f9b2c1495473fdcbb2f22dd97961803bc81.tar.gz
Use localstorage only to initialize settings map
This only reads from localstorage in order to initialize the settings map. After initializaton, reads will return the value from the map. When writing a value, the settings map and the local storage are updated, unless the setting is a default value or derived from the query string. This has a few advantages: 1. Saved settings will not be overridden by settings specified in the query string. This means a setting could be temporarily changed using the query string, but once removed from the query string, the setting would return back to what the user selected. 2. Default values will not be saved. If a user has always used the default value for a setting, then they can move to a new version with different defaults without clearing localstorage. 3. Changes made to localstorage in a session running in a different window will not affect the settings in the current window (until the page is refreshed). Regarding eraseSetting: It is possible that another tab could change the value, leading to an unexpected value change in the tab that deletes. However, this function is currently unused, so this will be evaluted if and when it used.
-rw-r--r--app/ui.js2
-rw-r--r--app/webutil.js24
-rw-r--r--karma.conf.js2
-rw-r--r--tests/test.webutil.js182
4 files changed, 202 insertions, 8 deletions
diff --git a/app/ui.js b/app/ui.js
index 2218d24..7c754ce 100644
--- a/app/ui.js
+++ b/app/ui.js
@@ -731,7 +731,7 @@ var UI = {
// Save the cookie for this session
if (typeof value !== 'undefined') {
- WebUtil.writeSetting(name, value);
+ WebUtil.setSetting(name, value);
}
// Update the settings control
diff --git a/app/webutil.js b/app/webutil.js
index 249a138..1087b33 100644
--- a/app/webutil.js
+++ b/app/webutil.js
@@ -117,21 +117,25 @@ export function initSettings (callback /*, ...callbackArgs */) {
}
});
} else {
- // No-op
+ settings = {};
if (callback) {
callback.apply(this, callbackArgs);
}
}
};
+// Update the settings cache, but do not write to permanent storage
+export function setSetting (name, value) {
+ settings[name] = value;
+};
+
// No days means only for this browser session
export function writeSetting (name, value) {
"use strict";
+ if (settings[name] === value) return;
+ settings[name] = value;
if (window.chrome && window.chrome.storage) {
- if (settings[name] !== value) {
- settings[name] = value;
- window.chrome.storage.sync.set(settings);
- }
+ window.chrome.storage.sync.set(settings);
} else {
localStorage.setItem(name, value);
}
@@ -140,10 +144,11 @@ export function writeSetting (name, value) {
export function readSetting (name, defaultValue) {
"use strict";
var value;
- if (window.chrome && window.chrome.storage) {
+ if ((name in settings) || (window.chrome && window.chrome.storage)) {
value = settings[name];
} else {
value = localStorage.getItem(name);
+ settings[name] = value;
}
if (typeof value === "undefined") {
value = null;
@@ -157,9 +162,14 @@ export function readSetting (name, defaultValue) {
export function eraseSetting (name) {
"use strict";
+ // Deleting here means that next time the setting is read when using local
+ // storage, it will be pulled from local storage again.
+ // If the setting in local storage is changed (e.g. in another tab)
+ // between this delete and the next read, it could lead to an unexpected
+ // value change.
+ delete settings[name];
if (window.chrome && window.chrome.storage) {
window.chrome.storage.sync.remove(name);
- delete settings[name];
} else {
localStorage.removeItem(name);
}
diff --git a/karma.conf.js b/karma.conf.js
index 10bf372..5b9da9f 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -62,6 +62,7 @@ module.exports = function(config) {
{ pattern: 'vendor/sinon.js', included: false },
{ pattern: 'node_modules/sinon-chai/lib/sinon-chai.js', included: false },
{ pattern: 'app/localization.js', included: false },
+ { pattern: 'app/webutil.js', included: false },
{ pattern: 'core/**/*.js', included: false },
{ pattern: 'vendor/pako/**/*.js', included: false },
{ pattern: 'tests/test.*.js', included: false },
@@ -92,6 +93,7 @@ module.exports = function(config) {
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'app/localization.js': ['babel'],
+ 'app/webutil.js': ['babel'],
'core/**/*.js': ['babel'],
'tests/test.*.js': ['babel'],
'tests/fake.*.js': ['babel'],
diff --git a/tests/test.webutil.js b/tests/test.webutil.js
new file mode 100644
index 0000000..b3c5964
--- /dev/null
+++ b/tests/test.webutil.js
@@ -0,0 +1,182 @@
+/* jshint expr: true */
+
+var assert = chai.assert;
+var expect = chai.expect;
+
+import * as WebUtil from '../app/webutil.js';
+
+import sinon from '../vendor/sinon.js';
+
+describe('WebUtil', function() {
+ "use strict";
+
+ describe('settings', function () {
+
+ // on Firefox, localStorage methods cannot be replaced
+ // localStorage is (currently) mockable on Chrome
+ // test to see if localStorage is mockable
+ var mockTest = sinon.spy(window.localStorage, 'setItem');
+ var canMock = window.localStorage.setItem.getCall instanceof Function;
+ mockTest.restore();
+ if (!canMock) {
+ console.warn('localStorage cannot be mocked');
+ }
+ (canMock ? describe : describe.skip)('localStorage', function() {
+ var chrome = window.chrome;
+ before(function() {
+ chrome = window.chrome;
+ window.chrome = null;
+ });
+ after(function() {
+ window.chrome = chrome;
+ });
+
+ var lsSandbox = sinon.createSandbox();
+
+ beforeEach(function() {
+ lsSandbox.stub(window.localStorage, 'setItem');
+ lsSandbox.stub(window.localStorage, 'getItem');
+ lsSandbox.stub(window.localStorage, 'removeItem');
+ WebUtil.initSettings();
+ });
+ afterEach(function() {
+ lsSandbox.restore();
+ });
+
+ describe('writeSetting', function() {
+ it('should save the setting value to local storage', function() {
+ WebUtil.writeSetting('test', 'value');
+ expect(window.localStorage.setItem).to.have.been.calledWithExactly('test', 'value');
+ expect(WebUtil.readSetting('test')).to.equal('value');
+ });
+ });
+
+ describe('setSetting', function() {
+ it('should update the setting but not save to local storage', function() {
+ WebUtil.setSetting('test', 'value');
+ expect(window.localStorage.setItem).to.not.have.been.called;
+ expect(WebUtil.readSetting('test')).to.equal('value');
+ });
+ });
+
+ describe('readSetting', function() {
+ it('should read the setting value from local storage', function() {
+ localStorage.getItem.returns('value');
+ expect(WebUtil.readSetting('test')).to.equal('value');
+ });
+
+ it('should return the default value when not in local storage', function() {
+ expect(WebUtil.readSetting('test', 'default')).to.equal('default');
+ });
+
+ it('should return the cached value even if local storage changed', function() {
+ localStorage.getItem.returns('value');
+ expect(WebUtil.readSetting('test')).to.equal('value');
+ localStorage.getItem.returns('something else');
+ expect(WebUtil.readSetting('test')).to.equal('value');
+ });
+
+ it('should cache the value even if it is not initially in local storage', function() {
+ expect(WebUtil.readSetting('test')).to.be.null;
+ localStorage.getItem.returns('value');
+ expect(WebUtil.readSetting('test')).to.be.null;
+ });
+
+ it('should return the default value always if the first read was not in local storage', function() {
+ expect(WebUtil.readSetting('test', 'default')).to.equal('default');
+ localStorage.getItem.returns('value');
+ expect(WebUtil.readSetting('test', 'another default')).to.equal('another default');
+ });
+
+ it('should return the last local written value', function() {
+ localStorage.getItem.returns('value');
+ expect(WebUtil.readSetting('test')).to.equal('value');
+ WebUtil.writeSetting('test', 'something else');
+ expect(WebUtil.readSetting('test')).to.equal('something else');
+ });
+ });
+
+ // this doesn't appear to be used anywhere
+ describe('eraseSetting', function() {
+ it('should remove the setting from local storage', function() {
+ WebUtil.eraseSetting('test');
+ expect(window.localStorage.removeItem).to.have.been.calledWithExactly('test');
+ });
+ });
+ });
+
+ describe('chrome.storage', function() {
+ var chrome = window.chrome;
+ var settings = {};
+ before(function() {
+ chrome = window.chrome;
+ window.chrome = {
+ storage: {
+ sync: {
+ get: function(cb){ cb(settings); },
+ set: function(){},
+ remove: function() {}
+ }
+ }
+ };
+ });
+ after(function() {
+ window.chrome = chrome;
+ });
+
+ var csSandbox = sinon.createSandbox();
+
+ beforeEach(function() {
+ settings = {};
+ csSandbox.spy(window.chrome.storage.sync, 'set');
+ csSandbox.spy(window.chrome.storage.sync, 'remove');
+ WebUtil.initSettings();
+ });
+ afterEach(function() {
+ csSandbox.restore();
+ });
+
+ describe('writeSetting', function() {
+ it('should save the setting value to chrome storage', function() {
+ WebUtil.writeSetting('test', 'value');
+ expect(window.chrome.storage.sync.set).to.have.been.calledWithExactly(sinon.match({ test: 'value' }));
+ expect(WebUtil.readSetting('test')).to.equal('value');
+ });
+ });
+
+ describe('setSetting', function() {
+ it('should update the setting but not save to chrome storage', function() {
+ WebUtil.setSetting('test', 'value');
+ expect(window.chrome.storage.sync.set).to.not.have.been.called;
+ expect(WebUtil.readSetting('test')).to.equal('value');
+ });
+ });
+
+ describe('readSetting', function() {
+ it('should read the setting value from chrome storage', function() {
+ settings.test = 'value';
+ expect(WebUtil.readSetting('test')).to.equal('value');
+ });
+
+ it('should return the default value when not in chrome storage', function() {
+ expect(WebUtil.readSetting('test', 'default')).to.equal('default');
+ });
+
+ it('should return the last local written value', function() {
+ settings.test = 'value';
+ expect(WebUtil.readSetting('test')).to.equal('value');
+ WebUtil.writeSetting('test', 'something else');
+ expect(WebUtil.readSetting('test')).to.equal('something else');
+ });
+ });
+
+ // this doesn't appear to be used anywhere
+ describe('eraseSetting', function() {
+ it('should remove the setting from chrome storage', function() {
+ WebUtil.eraseSetting('test');
+ expect(window.chrome.storage.sync.remove).to.have.been.calledWithExactly('test');
+ });
+ });
+ });
+ });
+});