From 7daab75414d1312bc9ae43a561adbcaf48760012 Mon Sep 17 00:00:00 2001 From: jdlrobson Date: Tue, 11 Aug 2015 11:16:21 -0700 Subject: [PATCH] Add simplified storage API Provide a standard mechanism for accessing localStorage. It may seems simplistic right now, but to give an idea of the why: * We already have jquery.jStorage.js which is a much more heavyweight approach to storing non-essential values. * We are repeating ourselves a lot in extensions by having to do localStorage detection and then deal with full localStorage. In MobileFrontend we have a settings module. This is one of the reasons Gather depends on MobileFrontend and I'm keen to remove that dependency. * We might want to move to indexdb in future. Having a single API makes moving this easier - we don't have to update everywhere that uses localStorage * Saving non-string support would be useful. The API could be adjusted to take a mixed second parameter that stringifys JSON objects. * Cookie fallbacks are a possible alternative when localStorage is not supported. This allows us to be agnostic of the storage mechanism going forward. Note: This doesn't reuse the handling in mediawiki.js as at this point I am not sure there is value. mw.loader.store.enabled is false when $wgResourceLoaderStorageEnabled is not true and this should work even without that case. We can review this at a latter point. See: Id5c32bb7a662dda8d153490f7c47e972cabc1efd I3fd44b0ae6633a7053aee247bc3c4704ba987bc8 Bug: T96155 Change-Id: Idb37352acecd745beb53aa8d77ea050851448e0d --- maintenance/jsduck/categories.json | 1 + resources/Resources.php | 4 ++ resources/src/mediawiki/mediawiki.storage.js | 69 +++++++++++++++++++ tests/qunit/QUnitTestResources.php | 2 + .../mediawiki/mediawiki.storage.test.js | 53 ++++++++++++++ 5 files changed, 129 insertions(+) create mode 100644 resources/src/mediawiki/mediawiki.storage.js create mode 100644 tests/qunit/suites/resources/mediawiki/mediawiki.storage.test.js diff --git a/maintenance/jsduck/categories.json b/maintenance/jsduck/categories.json index a338ff0f39..07e72bf85d 100644 --- a/maintenance/jsduck/categories.json +++ b/maintenance/jsduck/categories.json @@ -27,6 +27,7 @@ "mw.messagePoster.*", "mw.notification", "mw.Notification_", + "mw.storage", "mw.user", "mw.util", "mw.plugin.*", diff --git a/resources/Resources.php b/resources/Resources.php index 1116b791b2..e1f27f7541 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1092,6 +1092,10 @@ return array( 'styles' => 'resources/src/mediawiki/mediawiki.sectionAnchor.css', 'targets' => array( 'desktop', 'mobile' ), ), + 'mediawiki.storage' => array( + 'scripts' => 'resources/src/mediawiki/mediawiki.storage.js', + 'targets' => array( 'desktop', 'mobile' ), + ), 'mediawiki.Title' => array( 'scripts' => 'resources/src/mediawiki/mediawiki.Title.js', 'dependencies' => array( diff --git a/resources/src/mediawiki/mediawiki.storage.js b/resources/src/mediawiki/mediawiki.storage.js new file mode 100644 index 0000000000..e10b561fb8 --- /dev/null +++ b/resources/src/mediawiki/mediawiki.storage.js @@ -0,0 +1,69 @@ +( function ( mw ) { + 'use strict'; + var storage; + + /** + * Library for storing device specific information. It should be used for storing simple + * strings and is not suitable for storing large chunks of data. + * @class mw.storage + * @singleton + */ + storage = { + isLocalStorageSupported: false, + /** + * Retrieve value from device storage. + * + * @param {String} key of item to retrieve + * @returns {String|Boolean} false when localStorage not available, otherwise string + */ + get: function ( key ) { + if ( this.isLocalStorageSupported ) { + return localStorage.getItem( key ); + } else { + return false; + } + }, + + /** + * Set a value in device storage. + * + * @param {String} key key name to store under. + * @param {String} value to be stored. + * @returns {Boolean} whether the save succeeded or not. + */ + set: function ( key, value ) { + try { + localStorage.setItem( key, value ); + return true; + } catch ( e ) { + return false; + } + }, + + /** + * Remove a value from device storage. + * + * @param {String} key of item to remove. + * @returns {Boolean} whether the save succeeded or not. + */ + remove: function ( key ) { + if ( this.isLocalStorageSupported ) { + localStorage.removeItem( key ); + return true; + } else { + return false; + } + } + }; + + mw.storage = storage; + // See if local storage is supported + try { + localStorage.setItem( 'localStorageTest', 'localStorageTest' ); + localStorage.removeItem( 'localStorageTest' ); + storage.isLocalStorageSupported = true; + } catch ( e ) { + // Already set. No body needed. + } + +}( mediaWiki ) ); diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index 3608a53025..bcfdead56e 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -68,6 +68,7 @@ return array( 'tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.messagePoster.factory.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.RegExp.test.js', + 'tests/qunit/suites/resources/mediawiki/mediawiki.storage.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.template.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js', @@ -113,6 +114,7 @@ return array( 'mediawiki.jqueryMsg', 'mediawiki.messagePoster', 'mediawiki.RegExp', + 'mediawiki.storage', 'mediawiki.Title', 'mediawiki.toc', 'mediawiki.Uri', diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.storage.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.storage.test.js new file mode 100644 index 0000000000..c25641db48 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.storage.test.js @@ -0,0 +1,53 @@ +( function ( mw ) { + QUnit.module( 'mediawiki.storage: normal case.', { + setup: function () { + this.sandbox.stub( mw.storage, 'isLocalStorageSupported', true ); + this.spy = this.sandbox.spy( localStorage, 'setItem' ); + this.sandbox.stub( localStorage, 'getItem' ) + .withArgs( 'foo' ).returns( 'test' ) + .withArgs( 'bar' ).returns( null ); + } + } ); + + QUnit.test( 'set/get with localStorage', 4, function ( assert ) { + mw.storage.set( 'foo', 'test' ); + assert.strictEqual( this.spy.calledOnce, true, 'Check localStorage called.' ); + assert.strictEqual( this.spy.calledWith( 'foo', 'test' ), true, + 'Check prefixed.' ); + assert.strictEqual( mw.storage.get( 'foo' ), 'test', 'Check value gets stored.' ); + assert.strictEqual( mw.storage.get( 'bar' ), null, 'Unset values are null.' ); + } ); + + QUnit.module( 'mediawiki.storage: localStorage does not exist', { + setup: function () { + this.sandbox.stub( mw.storage, 'isLocalStorageSupported', false ); + this.sandbox.stub( localStorage, 'setItem' ).throws(); + } + } ); + + QUnit.test( 'set/get without localStorage', 3, function ( assert ) { + assert.strictEqual( mw.storage.set( 'foo', 'test' ), false, + 'When localStorage not available save fails.' ); + + assert.strictEqual( mw.storage.remove( 'foo', 'test' ), false, + 'When localStorage not available remove fails.' ); + + assert.strictEqual( mw.storage.get( 'foo' ), false, + 'Inability to retrieve values return false to differentiate from null (not set).' ); + } ); + + QUnit.module( 'mediawiki.storage: localStorage exhausted', { + setup: function () { + this.sandbox.stub( mw.storage, 'isLocalStorageSupported', true ); + this.sandbox.stub( localStorage, 'setItem' ).throws(); + this.sandbox.stub( localStorage, 'getItem' ).returns( null ); + } + } ); + + QUnit.test( 'set/get without localStorage', 2, function ( assert ) { + assert.strictEqual( mw.storage.set( 'foo', 'test' ), false, + 'When localStorage not available inform user with false.' ); + assert.strictEqual( mw.storage.get( 'foo' ), null, 'No value registered.' ); + } ); + +}( mediaWiki ) ); -- 2.20.1