I've got things operating as desired so far and am hoping fresh eyes can point out any mistakes or bits that can be improved, a live demo is hosted and updated on GitHub Pages, Travis-CI build logs of Jest tests are available for review, and the source code is just a fork
or clone
away....
Browser_Storagebrowser-storage.js
/**
* @author S0AndS0
* @copyright AGPL-3.0
* @example <caption>Quick Usage for Browser Storage</caption>
* // Initialize new class instance
* const storage = new Browser_Storage();
* if (!storage.storage_available) {
* console.error('No browser storage available!');
* } else {
* if (!storage.supports_local_storage) {
* console.warn('Falling back to cookies!');
* }
* // Do stuff with local storage of browser!
* storage.setItem('test__string', 'Spam!', 7);
* console.log("storage.getItem('test__string') -> " + storage.getItem('test__string'));
* }
*/
class Browser_Storage {
/**
* Sets properties used by other methods of this class
* @returns {none}
* @property {boolean} supports_local_storage - What `this.constructor.supportsLocalStorage()` had to say
* @property {boolean} supports_cookies - What `this.constructor.supportsCookies()` had to say
* @property {boolean} storage_available - If either of the above is `true`
* @this Browser_Storage
* @class
*/
constructor() {
this.supports_local_storage = this.constructor.supportsLocalStorage();
this.supports_cookies = this.constructor.supportsCookies();
this.storage_available = (this.supports_cookies || this.supports_local_storage) ? true : false;
}
/**
* Copy of `this.constructor` that should not throw `TypeError` when called
* @returns {none}
* @this Browser_Storage
*/
constructorRefresh() {
this.supports_local_storage = this.constructor.supportsLocalStorage();
this.supports_cookies = this.constructor.supportsCookies();
this.storage_available = (this.supports_cookies || this.supports_local_storage) ? true : false;
}
/**
* Tests and reports `boolean` if `localStorage` has `setItem` and `removeItem` methods
* @returns {boolean}
*/
static supportsLocalStorage() {
// Because Opera and may be other browsers `setItem`
// is available but with space set to _`0`_
try {
localStorage.setItem('test_key', true);
} catch (e) {
if (!(e instanceof ReferenceError)) throw e;
return false;
} finally {
localStorage.removeItem('test_key');
}
return true;
}
/**
* Reports if `navigator.cookieEnabled` or `document.cookie`cookies are available
*enabled. Note, use `this.supports_cookies` instead within tests.
* @returns {boolean}
* @this Browser_Storage
*/
static supportsCookies() {
// Browser support detection must be interactive as some
// may be _full_ or not enabled without updating state!
if (navigatorthis.cookieEnabled_setCookieItem('testcookie', '', 7)) {
return true;this._setCookieItem('testcookie', '', -7);
}
return false;
}
/**
try * Use `this.setItem` instead. Attempts to set cookie
* @returns {boolean}
* @param {string|number} document. key - _variable name_ to store value under
* @param {*} value - stored either under localStorage or as a cookie
* @param {number} [days_to_live=false] - how long a browser is suggested to keep cookies
*/
_setCookieItem(key, value, days_to_live) {
const encoded_key = 'testcookie';encodeURIComponent(key);
}const catchencoded_value = encodeURIComponent(eJSON.stringify(value));
let expires = '';
if (isNaN(days_to_live) == false) {
returnconst false;date = new Date();
const now = date.getTime();
if (days_to_live == 0) {
date.setTime(now);
} else {
date.setTime(now + (days_to_live * 24 * 60 * 60 * 1000));
}
expires = '; expires=' + date.toGMTString();
}
iftry ({
document.cookie.indexOf('testcookie') != -1encoded_key + '=' + encoded_value + expires + '; path=/';
} catch (e) {
this.removeif ('testcookie'!(e instanceof ReferenceError);) throw e;
return false
} finally {
return true;
}
return false;
}
/**
* Use `this.getItem` instead. Attempts to get cookie by _key_ via `match`
* @returns {*}
* @param {string|number} key - Name of key to look up value for.
*/
_getCookieItem(key) {
const encoded_key = encodeURIComponent(key);
const cookie_data = document.cookie.match('(^|;) ?' + encoded_key + '=([^;]*)(;|$)');
if (cookie_data === null || cookie_data[2] === 'undefined') return undefined;
return JSON.parse(decodeURIComponent(cookie_data[2]));
}
/**
* Gets decoded/JSON value for given key
* @returns {*}
* @throws {ReferenceError} When no browser based storage is available
* @param {string|number} key - Name of key to look up value for.
* @this Browser_Storage
*/
getgetItem(key) {
const encoded_key = encodeURIComponent(key);
let decoded_value = undefined;
if (this.supports_local_storage) {
const encoded_key = encodeURIComponent(key);
const raw_value = localStorage.getItem(encoded_key);
if (raw_value === null || raw_value === 'undefined') return undefined;
return JSON.parse(decodeURIComponent(raw_value));
} else if (this.supports_cookies) {
const cookie_data = document.cookie.match('(^|;) ?' + encoded_key + '=([^;]*)(;|$)');
if (cookie_data === null) return undefined;
return JSONthis.parse(decodeURIComponent_getCookieItem(cookie_data[2])key);
}
throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
}
/**
* Removes value by key from browser storage; cookies require page refresh
* @returns {boolean}
* @throws {ReferenceError} When no browser based storage is available
* @this Browser_Storage
*/
removeremoveItem(key) {
if (this.supports_local_storage) {
localStorage.removeItem(key);
return true;
} else if (this.supports_cookies) {
// Note, unsetting and expiring in the past is how
// to remove one cookie upon the next page load.
return this.setsetItem(key, '', -7);
return true;
}
throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
}
/**
* Stores encoded JSON within browser
* @returns {boolean}
* @throws {ReferenceError} When no browser based storage is available
* @param {string|number} key - _variable name_ to store value under
* @param {*} value - stored either under localStorage or as a cookie
* @param {number} [days_to_live=false] - how long a browser is suggested to keep cookies
* @this Browser_Storage
*/
setsetItem(key, value, days_to_live = false) {
const encoded_key = encodeURIComponent(key);
const encoded_value = encodeURIComponent(JSON.stringify(value));
if (this.supports_local_storage) {
localStorage.setItem(encoded_key
encodeURIComponent(key),
encoded_value encodeURIComponent(JSON.stringify(value))
);
return true;
} else if (this.supports_cookies) {
let expires = '';
if (days_to_live) {
const date = new Date();
if (days_to_live == 0) {
date.setTime(date.getTime());
} else {
return date.setTime(datethis.getTime_setCookieItem()key, +value, (days_to_live * 24 * 60 * 60 * 1000));
}
expires = '; expires=' + date.toGMTString();
}
document.cookie = encoded_key + '=' + encoded_value + expires + '; path=/';
return true;
}
throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
}
/**
* Lists keys that may point to values
* @returns {boolean}
* @throws {ReferenceError} When no browser based storage is available
* @this Browser_Storage
*/
keys() {
if (this.supports_local_storage) {
return Object.keys(localStorage);
} else if (this.supports_cookies) {
let cookie_keys = [];
document.cookie.split(';').forEach((pare) => {
cookie_keys.push(pare.split('=')[0].trim());
});
return cookie_keys;
}
throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
}
/**
* Clears **all** stored values from either localStorage or cookies
* @returns {boolean}
* @throws {ReferenceError} When no browser based storage is available
* @this Browser_Storage
*/
clear() {
if (this.supports_local_storage) {
localStorage.clear();
return true;
} else if (this.supports_cookies) {
document.cookie.split(';').forEach((cookie) => {
const keydecoded_key = encodeURIComponentdecodeURIComponent(cookie.split('=')[0].replace(/trim(^\s+|\s+$)/g, ''));
if (key) this.removeremoveItem(keydecoded_key);
});
return true;
}
throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
}
/**
* Generates `{data.key: data.value}` JSON from localStorage or cookies
* @yields {stored_data}
* @this Browser_Storage
*/
*iterator() {
if (this.supports_local_storage) {
const keys = Object.keys(localStorage);
for (let i = 0; i < keys.length; i++) {
const encoded_key = keys[i];
const decoded_key = decodeURIComponent(encoded_key);
const raw_value = localStorage.getItem(encoded_key);
// Note, `JSON.pars()` has a _finicky appetite_
if (raw_value === null || raw_value === undefined || raw_value === 'undefined') {
yield {key: decoded_key, value: undefined};
} else {
yield {key: decoded_key, value: JSON.parse(decodeURIComponent(raw_value))};
}
}
} else if (this.supports_cookies) {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const decoded_key = decodeURIComponent(cookies[i].split('=')[0].trim());
yield {key: decoded_key, value: this._getCookieItem(decoded_key)};
}
}
else {
throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
}
}
/**
* See `this.iterator()` method
* @returns {stored_data}
* @this Browser_Storage
*/
[Symbol.iterator]() {
return this.iterator();
}
}
/**
* Exports are for Jest who uses Node for Travis-CI tests on gh-pages branch
* https://javascript-utilities.github.io/browser-storage/
*/
if (typeof(module) !== 'undefined') module.exports = Browser_Storage;
/**
* @typedef stored_data
* @type {Object}
* @property {string|number|float} key - `data.key` from `localStorage` or cookies
* @property {*} value - `data.value` from `localStorage` or cookies
*/
/**
* 1. Center element
* 2. Re-justify text
*/
body {
text-align: center; /* 1 */
}
.container__main_content {
max-width: 80%;
max-width: 80vw;
display: inline-block; /* 1 */
text-align: left; /* 2 */
}
/**
* Privacy notice footer stuff
*/
.container__privacy_notice {
position: fixed;
bottom: 5px;
max-width: 80%;
max-width: 80vw;
background-color: lightgrey;
text-align: left; /* 2 */
}
/**
* 1. Prevent label text from being selectable
*/
.privacy_notice__label {
background-color: black;
color: white;
font-size: 1.5rem;
padding: 5px 10px;
float: right;
-webkit-user-select: none; /* 1 */
-moz-user-select: none; /* 1 */
-ms-user-select: none; /* 1 */
user-select: none; /* 1 */
}
.privacy_notice__label:hover {
color: lightgrey;
}
/* State of label when checkbox is unchecked */
.privacy_notice__label::after {
content: attr(value);
}
/**
* Styling test input/output elements
*/
.container__live_tests {
list-style: none;
text-align: left;
max-width: 80%;
max-width: 80vw;
margin-left: auto;
margin-right: auto;
}
.live_tests__output__get__value:empty::after {
content: attr(data-after);
display: block;
}
.mouse__no_touch {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.mouse__default {
cursor: default;
}
/**
* Checkbox hacks
*/
.hidden__input {
display: none;
visibility: hidden;
opacity: 0;
filter: alpha(opacity=0);
}
/* State updates for when checkbox is checked */
.privacy_notice__checkbox:checked ~ .container__privacy_notice {
box-shadow: unset;
max-width: 1.5rem;
max-height: 1.5rem;
}
.privacy_notice__checkbox:checked ~ .container__privacy_notice * {
visibility: hidden;
opacity: 0;
filter: alpha(opacity=0);
}
.privacy_notice__checkbox:checked ~ .container__privacy_notice .privacy_notice__label::after {
content: attr(checked);
}
/* State of label when checkbox is checked */
.privacy_notice__checkbox:checked ~ .container__privacy_notice::after {
content: '\1F56D';
position: absolute;
bottom: 5px;
left: 5px;
}
/* Combo of checked and hover state updates */
.privacy_notice__checkbox:checked ~ .container__privacy_notice:hover {
max-width: 80%;
max-width: 80vw;
max-height: unset;
}
.privacy_notice__checkbox:checked ~ .container__privacy_notice:hover::after {
display: none;
visibility: hidden;
opacity: 0;
filter: alpha(opacity=0);
}
.privacy_notice__checkbox:checked ~ .container__privacy_notice:hover * {
visibility: visible;
opacity: 1;
filter: alpha(opacity=100);
}
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<title>Browser_Storage Example</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link type="text/css" href="assets/css/main.css" rel="stylesheet"/>
<script src="assets/javascript-/modules/browser-storage/browser-storage.js" type="text/javascript"></script>
<link type="text/css"<script href="assetssrc="assets/cssjavascript/main.css"js" rel="stylesheet"type="text/>
javascript"></script>
</head>
<script<body type="text/javascript">onload="restore_settings();" onunload="store_settings();">
<div class="container__main_content">
const storage = new Browser_Storage(); <p>
if (storage.storage_available != true)This throwis newa Error('Westatic maysite neverthat haveprovides anythe memoryillusion of this..remembering a client.');
function restore_settings() {
Test by accepting the <i>privacy notice</i> and then letrefreshing privacy_notice_reador
= storage.get('checkbox__privacy_notice');
closing and ifre-opening (privacy_notice_reada !=tab 'true'pointing &&to privacy_notice_readthe !=current true)URL.
return false; </p>
<p>
document.getElementById('trigger__privacy_notice').checked = true; <kbd>Ctrl</kbd> <kbd>R</kbd> or <kbd>Ctrl</kbd> <kbd>Shift</kbd> <kbd>R</kbd>
returnwill true;refresh most desktop browsers.
}</p>
<p>
function store_settings() {Then reject the privacy notice by clicking on the Acceptable button
letagain privacy_notice_readto =revert storagethe behavior of this site.get('checkbox__privacy_notice');
</p>
if (privacy_notice_read != true) return false;<p>
ifUse (document.getElementById('trigger__privacy_notice').checked<kbd>Ctrl</kbd> !=<kbd>Shift</kbd> true)<kbd>I</kbd> returnto false;
open debugger on a
storage.set('checkbox__privacy_notice',desktop truebrowser, 365);then navigate to the Storage tab/pane to observe
returnJavaScript true;doing things for the above mentioned actions.
}</p>
</div>
<ul class="container__live_tests">
function toggle_privacy_notice(input_element) { <h2>Test some features</h2>
<li>
if (input_element.checked == true) {
<h3>Set a value under a key</h3>
storage.set('checkbox__privacy_notice', input_element.checked, 365); <form class="">
return<textarea true;name="set_key"
}
storage.remove('checkbox__privacy_notice'); id="input_text__set__key"
storage.clear();
return false; class="live_tests__input_text__set__key"
}
</script> rows="1"
< >Input a Key</head>
textarea>
<body onload="restore_settings();" onunload="store_settings();">
<div class="container__main_content"> <br>
<p> <textarea name="set_value"
This is a static site that provides the illusion of remembering a client.id="input_text__set__value"
Test by accepting the <i>privacy notice</i> and then refreshing or class="live_tests__input_text__set__value"
closing and re-opening a tab pointing to the current URL. rows="1"
< >Input a Value</p>textarea>
<br>
<p> <input type="button"
<kbd>Ctrl</kbd> <kbd>R</kbd> or <kbd>Ctrl</kbd> <kbd>Shift</kbd> <kbd>R</kbd> onclick="input_text_setItem('input_text__set__key', 'input_text__set__value');"
will refresh most desktop browsers. name="set"
value="Set">
</form>
</p>li>
<p><li>
Then<h3>Get rejectsome thevalue privacyfrom noticea bykey</h3>
clicking on the Acceptable button <input class="live_tests__input__get__key"
again to revert the behavior of this site.id="input_text__get__key"
</p>
type="text"
<p> name="key"
Use <kbd>Ctrl</kbd> <kbd>Shift</kbd> <kbd>I</kbd> to open debugger onvalue="input a Key">
desktop<input browsertype="button"
onclick="input_text_get('input_text__get__key', then'output__value', navigatefalse);"
to the Storage tab/pane to observe name="get"
JavaScript doing things for the above mentioned actions.value="Get">
< <p class="live_tests__output__get__value mouse__no_touch mouse__default"
id="output__value"
data-after="undefined"></p>
</div>li>
</ul>
<input type="checkbox"
name="checkbox__privacy_notice"
class="hidden__input privacy_notice__checkbox"
id="trigger__privacy_notice"
onchange="toggle_privacy_notice(this);">
<div class="container__privacy_notice">
<p>
This site uses JavaScript, and localStorage or cookies of your browser,
to enhance your experience here, however, this site currently does
<strong>not</strong> store any data about your activity severer-side.
</p>
<p>
Review the <a href="https://github.com/javascript-utilities/browser-storage#privacy">Privacy</a>
and <a href="https://github.com/javascript-utilities/browser-storage#security">Security</a>
notices regarding the
<a href="https://github.com/javascript-utilities/browser-storage/blob/master/browser-storage.js">
browser-storage.js
</a>, for more details on how client-side storage and scripting are utilized.
</p>
<label for="trigger__privacy_notice"
class="menu__trigger__label privacy_notice__label"
title="Click to toggle."
value="false"
checked="true"
>Acceptable: </label>
</div>
</body>
</html>
Note, I did try to use the web code option when writing this question but that caused various errors... probably for good reasons based on the kinds of errors, so see the live demo linked above in order to check it out in console without cloning.
Jest tests preformed on above questionable JavaScript code. Note, none of the string questions within tests are directed at readers; it's more of questions that the CI server asks of code revisions.
/**
* @typedef@author forced_statesS0AndS0
* @type@copyright {Object|boolean}AGPL-3.0
* @property {boolean}@example supports_local_storage<caption>Jest -Tests cachedfor fromBrowser `this.supportsLocalStorage()`Storage</caption>
* @property// {boolean}Initialize supports_cookiesnew class instance and run tests
* -const cachedtest_storage from= `this.supportsCookiesnew Browser_Storage_Test()`;
* @property {boolean} storage_available - cached on if either localStorage or cookies are supportedtest_storage.runTests();
*/
class Browser_Storage_Test {
/**
* Sets properties used by other methods of this class
* @returns {none}
* @property {object} storage - Class instance of `Browser_Storage` to test
* @this Browser_Storage_Test
* @class
*/
constructor() {
this.storage = new (require('../assets/javascript-/modules/browser-storage/browser-storage.js'))();
}
/**
* Runs the tests in an order that made since to someone at the time
* @returns {none}
* @this Browser_Storage_Test
*/
runTests() {
this.setAndGet();
this.setAndGet({supports_local_storage: false});
this.iterability();
this.iterability({supports_local_storage: true});
this.errorThrowers();
this.resetsAndWipes();
}
/**
* Sets properties for `this.storage` from key value pares
* @returns {none}
* @param {forced_states} states - JSON/_dictionary_ of states to force upon storage instance
* @this Browser_Storage_Test
*/
forceStorageState(states) {
Object.keys(states).forEach((key) => {
test('Forcing ' + this.storage.constructor.name + '[' + key + '] to be ' + states[key], () => {
this.storage[key] = states[key];
expect(this.storage[key]).toBe(states[key]);
});
});
}
/**
* Attempts to break `set`setItem()` and `get`getItem()` methods for `Browser_Storage` instance
* @returns {none}
* @param {forced_states} states - If object, passes to `this.forceStorageState`
* @this Browser_Storage_Test
*/
setAndGet(states = false) {
if (typeof(states) === 'object') {
this.forceStorageState(states);
}
test('Boolean with `set()` and `get()` methods', () => {
expect(this.storage.set('test__boolean', true, 3)).toBe(true);
expect(this.storage.get('test__boolean')).toBe(true);
});
test('String with `set()` and `get()` methods', () => {
expect(this.storage.set('test__string', 'Spam!', 3)).toBe(true);
expect(this.storage.get('test__string')).toBe('Spam!');
});
test('List with `set()` and `get()` methods', () => {
expect(this.storage.set('test__list', [1, "two", 4.2], 3)).toBe(true);
expect(this.storage.get('test__list')).toEqual([1, "two", 4.2]);
});
test('JSON with `set()` and `get()` methods', () => {
const test_json = {
first_key: true,
second_key: "Spam!",
third_key: null,
fourth_key: undefined,
fifth_key: [
'Well may be just\n\ta little\nbit',
"more testing, shouldn't _hurt_",
{
but: [
'just', 2, 'be on the',
'"safer side"'
],
},
],
};
const test_char expect(this.storage= String.setfromCharCode('test__json''9749');
const data_map = {
true: true,
test_json false: false,
char: test_char,
list: [1, 'two', 3)).toBe(true);5, ["inner", "and 'nested' strings", {obj_key: 'obj_value'}]],
inner_json: {'one more thing?': {or: 'perhaps', two: 'more?'}},
1: 'one',
2.0: 'float',
'&^': '?!',
'~': "*.+",
empty_sting: '',
bad_idea: null,
bad_idea2: 'null',
bad_idea3: 'undefined',
bad_idea4: undefined
}
test('Do all consttest stored_jsonkey/value =pares this.storage.getfrom `data_map` operate with `setItem` and `getItem` methods?', ('test__json'); => {
let days_to_live = 1;
Object.keys(test_jsondata_map).forEach((key) => {
expect(test_json[key]this.storage.setItem(key, data_map[key], days_to_live)).toBe(true);
expect(this.storage.getItem(key)).toStrictEqual(stored_json[key]data_map[key]);
days_to_live++
});
});
test('CharCode value with `set()` and `get()` methods', () => {
// `9749` is decimal for _`hot beverage`_
const test_char = String.fromCharCode('9749');
expect(this.storage.set('test__charcode', test_char, 3)).toBe(true);
expect(this.storage.get('test__charcode')).toBe(test_char);
});
'How about a test('CharCodeCharCode key with `set`setItem()` and `get`getItem()` methods'methods?', () => {
// `9842` is decimal for _`universal recycling`_
const test_char = String.fromCharCode('9842');
expect(this.storage.setsetItem(test_char, 'universal recycling', 3)).toBe(true);
expect(this.storage.getgetItem(test_char)).toBe('universal recycling');
});
test('Does `days_to_live=0` with `setItem()` return `true`?', () => {
expect(this.storage.setItem('Casper', 'Was never here?', 0)).toBe(true);
});
test('With `getItem("Casper")` is there any trace of the _ghoast_ data?', () => {
// Most browsers this would not be required
if (this.storage.supports_local_storage) {
expect(this.storage.keys()).toContain('Casper');
} else {
expect(this.storage.keys()).not.toContain('Casper');
}
});
}
/**
* Reports failures if things do not throw errors when they should
* @returns {none}
* @this Browser_Storage_Test
*/
errorThrowers() {
this.forceStorageState({supports_local_storage: false, supports_cookies: false});
test('Boolean'Will `set`setItem()` throw a `ReferenceError` without storage support'support?', () => {
expect(() => {
this.storage.setsetItem('test__boolean', true, 3);
}).toThrow(ReferenceError);
});
test('Getting'Will previously`getItem()` setthrow `test__boolean`a `ReferenceError` without storage support'support?', () => {
expect(() => {
this.storage.getgetItem('test__boolean''Anything?');
}).toThrow(ReferenceError);
});
test('Will `removeItem()` throw a `ReferenceError` without storage support?', () => {
expect(() => {
this.storage.removeItem('Anything?');
}).toThrow(ReferenceError);
});
test('Will `clear()` throw a `ReferenceError` without storage support?', () => {
expect(() => {
this.storage.clear();
}).toThrow(ReferenceError);
});
test('Will `keys()` throw a `ReferenceError` without storage support?', () => {
expect(() => {
this.storage.keys();
}).toThrow(ReferenceError);
});
test('Will `while (data = storage_iter.next().value)` throw a `ReferenceError` without storage support?', () => {
const storage_iter = this.storage.iterator();
expect(() => {
storage_iter.next().value;
}).toThrow(ReferenceError);
});
}
/**
* Tests ways class may be iterated with
* @returns {none}
* @this Browser_Storage_Test
*/
iterability(states = false) {
if (typeof(states) === 'object') this.forceStorageState(states);
test('What does `for of storage` do?', () => {
for (const data of this.storage) {
expect(this.storage.getItem(data.key)).toStrictEqual(data.value);
}
});
test('What does `while (data=storage_iter.next().value) do?`', () => {
const storage_iter = this.storage.iterator();
let data; while (data = storage_iter.next().value) {
expect(this.storage.getItem(data.key)).toStrictEqual(data.value);
}
});
}
/**
* Reports failures if any values _linger_ when wiping
* @returns {none}
* @this Browser_Storage_Test
*/
resetsAndWipes() {
test('Do supportsCookies and supportsLocalStorage return boolean', () => {
expect(this.storage.supportsCookies()).toBe(true);
expect(this.storage.constructor.supportsLocalStorage()).toBe(true);
});
test('Re-freshing class properties', () => {
expect(this.storage.constructorRefresh()).toBeUndefined();
this.storage.constructorRefresh();
expect(this.storage.supports_local_storage).toBe(true);
});
test('That values can be removed by key', () => {
this.storage.keys().forEach((key) => {
expect(this.storage.removeremoveItem(key)).toBe(true);
expect(this.storage.getgetItem(key)).toBetoBeUndefined(undefined);
});
expect(this.storage.keys()).toStrictEqual([]);
});
this.forceStorageState({supports_local_storage: false});
test('Clearing cookies'cookies returns true', () => {
expect(this.storage.clear()).toBe(true);
});
test('Than'Are non-setthere keyany isremaining undefined'values not `undefined`?', () => {
this.storage.keys().forEach((key) => {
expect(this.storage.getgetItem('null'key)).toBetoBeUndefined(undefined);
});
});
this.forceStorageState({supports_local_storage: true});
test('Clearing localStorage returns true', () => {
expect(this.storage.clear()).toBe(true);
});
}
}
const test_storage = new Browser_Storage_Test();
test_storage.runTests();
/**
* @typedef forced_states
* @type {Object|boolean}
* @property {boolean} supports_local_storage - cached from `this.supportsLocalStorage()`
* @property {boolean} supports_cookies - cached from `this.supportsCookies()`
* @property {boolean} storage_available - cached on if either localStorage or cookies are supported
*/
My goal is to have near 100% coverage for browsers that have JavaScript and either localStorage
or document.cookie
storage options;
inIn this day-n-age is there any point in using cookies for a local storage fallback?
is there any point in using the old syntax of getting OO behavior from JavaScript or will using the
class
andfunction
keywords be sufficient?
if there are any good arguments for not using
class
, please include some feature detection examples for loading an alternate version.
- I keep running across
npm
but that seems to be more focused on Node related JavaScript, should I consider issuingnpm init .
or would that pollute their package management system?I keep running across
npm
but that seems to be more focused on Node related JavaScript, should I consider issuingnpm init .
or would that pollute their package management system?
Never mind that last bit, got it sorted and now things return
undefined
when something is undefined... mostly
Testing related questions
Currently lines 55
,56
...
static supportsLocalStorage() {
// Because Opera and may be other browsers `setItem`
// is available but with space set to _`0`_
try {
localStorage.setItem('test_key', true);
} catch (e) {
if (!(e instanceof ReferenceError)) throw e; // Line ~55
return false; // Line ~56
} finally {
localStorage.removeItem('test_key');
}
return true;
}
... and line 74
...
supportsCookies() {
if (navigator.cookieEnabled) return true;
if (this._setCookieItem('testcookie', '', 7)) {
return this._setCookieItem('testcookie', '', -7);
}
return false; // Line ~74
}
... and lines 103
through 104
...
_setCookieItem(key, value, days_to_live) {
// ... Trimmed
try {
document.cookie = encoded_key + '=' + encoded_value + expires + '; path=/';
} catch (e) {
if (!(e instanceof ReferenceError)) throw e; // Line ~103
return false // Line ~104
} finally {
return true;
}
}
... seem inaccessible to Jest without resorting to Mocking both storage options.
Additionally for Jests tests...
errorThrowers() {
// ... trimmed
test('Will `for (data of storage)` throw a `ReferenceError` without storage support?', () => {
expect(() => {
for (data of s) console.error('We should have not seen -> ' + data.key + ': ' + data.value);
}).toThrow(ReferenceError);
});
// ... above _lowers_ coverage score for `statements`
// where as the following does not...
test('Will `while (data = storage_iter.next().value)` throw a `ReferenceError` without storage support?', () => {
const storage_iter = this.storage.iterator();
expect(() => {
storage_iter.next().value;
}).toThrow(ReferenceError);
});
}
... how can I get Jest to know that the for loop's inner bits shouldn't ever get touched?