Skip to main content
Added latest versions of all source code as of `master: 0a4cadf` and `gh-pages: 03f3b66` commits, and updated/added to questions regarding unit tests
Source Link
S0AndS0
  • 968
  • 1
  • 6
  • 15

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 and function 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 issuing npm 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 issuing npm 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?

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, and the source code is just a fork or clone away....

Browser_Storage.js

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` are available
   * Note, use `this.supports_cookies` instead within tests.
   * @returns {boolean}
   * @this Browser_Storage
   */
  static supportsCookies() {
    if (navigator.cookieEnabled) return true;

    try {
      document.cookie = 'testcookie';
    } catch (e) {
      return false;
    }

    if (document.cookie.indexOf('testcookie') != -1) {
      this.remove('testcookie');
      return true;
    }
    return false;
  }

  /**
   * 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
   */
  get(key) {
    const encoded_key = encodeURIComponent(key);
    let decoded_value = undefined;
    if (this.supports_local_storage) {
      const raw_value = localStorage.getItem(encoded_key);
      if (raw_value === null) 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 JSON.parse(decodeURIComponent(cookie_data[2]));
    }

    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
   */
  remove(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.
      this.set(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
   */
  set(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, encoded_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 {
          date.setTime(date.getTime() + (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]);
      });
      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 key = encodeURIComponent(cookie.split('=')[0].replace(/(^\s+|\s+$)/g, ''));
        if (key) this.remove(key);
      });
      return true;
    }

    throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
  }

}
/**
 * 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);
}


/**
 * 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">

    <script src="assets/javascript-modules/browser-storage/browser-storage.js" type="text/javascript"></script>
    <link type="text/css" href="assets/css/main.css" rel="stylesheet"/>

    <script type="text/javascript">
      const storage = new Browser_Storage();
      if (storage.storage_available != true) throw new Error('We may never have any memory of this...');


      function restore_settings() {
        let privacy_notice_read = storage.get('checkbox__privacy_notice');
        if (privacy_notice_read != 'true' && privacy_notice_read != true) return false;

        document.getElementById('trigger__privacy_notice').checked = true;
        return true;
      }


      function store_settings() {
        let privacy_notice_read = storage.get('checkbox__privacy_notice');
        if (privacy_notice_read != true) return false;
        if (document.getElementById('trigger__privacy_notice').checked != true) return false;

        storage.set('checkbox__privacy_notice', true, 365);
        return true;
      }


      function toggle_privacy_notice(input_element) {
        if (input_element.checked == true) {
          storage.set('checkbox__privacy_notice', input_element.checked, 365);
          return true;
        }
        storage.remove('checkbox__privacy_notice');
        storage.clear();
        return false;
      }
    </script>
  </head>



  <body onload="restore_settings();" onunload="store_settings();">
    <div class="container__main_content">
      <p>
        This is a static site that provides the illusion of remembering a client.
        Test by accepting the <i>privacy notice</i> and then refreshing or
        closing and re-opening a tab pointing to the current URL.
      </p>

      <p>
        <kbd>Ctrl</kbd> <kbd>R</kbd> or <kbd>Ctrl</kbd> <kbd>Shift</kbd> <kbd>R</kbd>
        will refresh most desktop browsers.
      </p>

      <p>
        Then reject the privacy notice by clicking on the Acceptable button
        again to revert the behavior of this site.
      </p>

      <p>
        Use <kbd>Ctrl</kbd> <kbd>Shift</kbd> <kbd>I</kbd> to open debugger on a
        desktop browser, then navigate to the Storage tab/pane to observe
        JavaScript doing things for the above mentioned actions.
      </p>
    </div>

    <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.

Jest tests preformed on above questionable JavaScript code.

/**
 * @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
 */


 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'))();
  }

  runTests() {
    this.setAndGet();
    this.setAndGet({supports_local_storage: false});
    this.errorThrowers();
    this.resetsAndWipes();
  }

  /**
   * Sets properties for `this.storage` from key value pares
   * @returns {none}
   * @param {forced_states} states -
   * @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()` and `get()` 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"'
            ],
          },
        ],
      };

      expect(this.storage.set('test__json', test_json, 3)).toBe(true);

      const stored_json = this.storage.get('test__json');
      Object.keys(test_json).forEach((key) => {
        expect(test_json[key]).toStrictEqual(stored_json[key]);
      });
    });

    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);
    });

    test('CharCode key with `set()` and `get()` methods', () => {
      // `9842` is decimal for _`universal recycling`_
      const test_char = String.fromCharCode('9842');
      expect(this.storage.set(test_char, 'universal recycling', 3)).toBe(true);
      expect(this.storage.get(test_char)).toBe('universal recycling');
    });

  }

  /**
   * 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 `set()` without storage support', () => {
      expect(() => {
        this.storage.set('test__boolean', true, 3);
      }).toThrow(ReferenceError);
    });

    test('Getting previously set `test__boolean` without storage support', () => {
      expect(() => {
        this.storage.get('test__boolean');
      }).toThrow(ReferenceError);
    });
  }

  /**
   * Reports failures if any values _linger_ when wiping
   * @returns {none}
   * @this Browser_Storage_Test
   */
  resetsAndWipes() {
    test('Re-freshing class properties', () => {
      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.remove(key)).toBe(true);
        expect(this.storage.get(key)).toBe(undefined);
      });
    });

    this.forceStorageState({supports_local_storage: false});
    test('Clearing cookies', () => {
      expect(this.storage.clear()).toBe(true);
    });

    test('Than non-set key is undefined', () => {
      expect(this.storage.get('null')).toBe(undefined);
    });
  }

}


const test_storage = new Browser_Storage_Test();

test_storage.runTests();

My goal is to have near 100% coverage for browsers that have JavaScript and either localStorage or document.cookie storage options;

  • in 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 and function 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 issuing npm 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.

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-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.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.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.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 cookies are enabled. Note, use `this.supports_cookies` instead within tests.
   * @returns {boolean}
   * @this Browser_Storage
   */
  supportsCookies() {
    // Browser support detection must be interactive as some
    // may be _full_ or not enabled without updating state!
    if (this._setCookieItem('testcookie', '', 7)) {
      return this._setCookieItem('testcookie', '', -7);
    }
    return false;
  }

  /**
   * Use `this.setItem` instead. Attempts to set cookie
   * @returns {boolean}
   * @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
   */
  _setCookieItem(key, value, days_to_live) {
    const encoded_key = encodeURIComponent(key);
    const encoded_value = encodeURIComponent(JSON.stringify(value));

    let expires = '';
    if (isNaN(days_to_live) == false) {
      const 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();
    }

    try {
      document.cookie = encoded_key + '=' + encoded_value + expires + '; path=/';
    } catch (e) {
      if (!(e instanceof ReferenceError)) throw e;
      return false
    } finally {
      return true;
    }
  }

  /**
   * 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
   */
  getItem(key) {
    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) {
      return this._getCookieItem(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
   */
  removeItem(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.setItem(key, '', -7);
    }
    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
   */
  setItem(key, value, days_to_live = false) {
    if (this.supports_local_storage) {
      localStorage.setItem(
        encodeURIComponent(key), 
        encodeURIComponent(JSON.stringify(value))
      );
      return true;
    } else if (this.supports_cookies) {
      return this._setCookieItem(key, value, days_to_live);
    }

    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 decoded_key = decodeURIComponent(cookie.split('=')[0].trim());
        this.removeItem(decoded_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>
    <script src="assets/javascript/main.js" type="text/javascript"></script>
  </head>



  <body onload="restore_settings();" onunload="store_settings();">
    <div class="container__main_content">
      <p>
        This is a static site that provides the illusion of remembering a client.
        Test by accepting the <i>privacy notice</i> and then refreshing or
        closing and re-opening a tab pointing to the current URL.
      </p>

      <p>
        <kbd>Ctrl</kbd> <kbd>R</kbd> or <kbd>Ctrl</kbd> <kbd>Shift</kbd> <kbd>R</kbd>
        will refresh most desktop browsers.
      </p>

      <p>
        Then reject the privacy notice by clicking on the Acceptable button
        again to revert the behavior of this site.
      </p>

      <p>
        Use <kbd>Ctrl</kbd> <kbd>Shift</kbd> <kbd>I</kbd> to open debugger on a
        desktop browser, then navigate to the Storage tab/pane to observe
        JavaScript doing things for the above mentioned actions.
      </p>
    </div>


    <ul class="container__live_tests">
      <h2>Test some features</h2>
      <li>
        <h3>Set a value under a key</h3>
        <form class="">
          <textarea name="set_key"
                    id="input_text__set__key"
                    class="live_tests__input_text__set__key"
                    rows="1"
                    >Input a Key</textarea>
          <br>
          <textarea name="set_value"
                    id="input_text__set__value"
                    class="live_tests__input_text__set__value"
                    rows="1"
                    >Input a Value</textarea>
          <br>
          <input type="button"
                 onclick="input_text_setItem('input_text__set__key', 'input_text__set__value');"
                 name="set"
                 value="Set">
        </form>
      </li>

      <li>
        <h3>Get some value from a key</h3>
        <input class="live_tests__input__get__key"
               id="input_text__get__key"
               type="text"
               name="key"
               value="input a Key">
        <input type="button"
               onclick="input_text_get('input_text__get__key', 'output__value', false);"
               name="get"
               value="Get">

        <p class="live_tests__output__get__value mouse__no_touch mouse__default"
           id="output__value"
           data-after="undefined"></p>
      </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.

/**
 * @author S0AndS0
 * @copyright AGPL-3.0
 * @example <caption>Jest Tests for Browser Storage</caption>
 * // Initialize new class instance and run tests
 * const test_storage = new Browser_Storage_Test();
 * test_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 `setItem()` and `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);

    const test_char = String.fromCharCode('9749');
    const data_map = {
      true: true, 
      false: false,
      char: test_char,
      list: [1, 'two', 3.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 test key/value pares from `data_map` operate with `setItem` and `getItem` methods?', () => {
      let days_to_live = 1;
      Object.keys(data_map).forEach((key) => {
        expect(this.storage.setItem(key, data_map[key], days_to_live)).toBe(true);
        expect(this.storage.getItem(key)).toStrictEqual(data_map[key]);
        days_to_live++
      });
    });

    test('How about a CharCode key with `setItem()` and `getItem()` methods?', () => {
      // `9842` is decimal for _`universal recycling`_
      const test_char = String.fromCharCode('9842');
      expect(this.storage.setItem(test_char, 'universal recycling', 3)).toBe(true);
      expect(this.storage.getItem(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('Will `setItem()` throw a `ReferenceError` without storage support?', () => {
      expect(() => {
        this.storage.setItem('test__boolean', true, 3);
      }).toThrow(ReferenceError);
    });

    test('Will `getItem()` throw a `ReferenceError` without storage support?', () => {
      expect(() => {
        this.storage.getItem('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.removeItem(key)).toBe(true);
        expect(this.storage.getItem(key)).toBeUndefined();
      });
      expect(this.storage.keys()).toStrictEqual([]);
    });

    this.forceStorageState({supports_local_storage: false});
    test('Clearing cookies returns true', () => {
      expect(this.storage.clear()).toBe(true);
    });

    test('Are there any remaining values not `undefined`?', () => {
      this.storage.keys().forEach((key) => {
        expect(this.storage.getItem(key)).toBeUndefined();
      });
    });

    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
 */
  • In 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 and function keywords be sufficient?

  • I keep running across npm but that seems to be more focused on Node related JavaScript, should I consider issuing npm init . or would that pollute their package management system?

Never mind that last bit, got it sorted... 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?

Tweeted twitter.com/StackCodeReview/status/1145663463458529282
Edited existing JavaScript based off tests and review, also added Jest tests for those into such things
Source Link
S0AndS0
  • 968
  • 1
  • 6
  • 15
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` ishas available`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` are available
   * Note, use `this.supports_cookies` instead within tests.
   * @returns {boolean}
   * @this Browser_Storage
   */
  static supportsCookies() {
    if (navigator.cookieEnabled) return true;

    try {
      document.cookie = 'testcookie';
    } catch (e) {
      return false;
    }

    if (document.cookie.indexOf('testcookie') != -1) {
      this.remove('testcookie');
      return true;
    }
    return false;
  }

  /**
   * Gets decoded/JSON value for given key
   * @returns {?boolean|?number|?string|?undefined*}
   * @throws {ReferenceError} When no browser based storage is available
   * @param {string|number} key - Name of key to look up value for.
   * @this Browser_Storage
   */
  get(key) {
    const encoded_key = encodeURIComponent(key);
    let decoded_value = undefined;
    if (this.supports_local_storage) {
      decoded_valueconst raw_value = decodeURIComponent(localStorage.getItem(encoded_key);
      if (raw_value === null) return undefined;
      return JSON.parse(decodeURIComponent(raw_value));
    } else if (this.supports_cookies) {
      letconst cookie_data = document.cookie.match('(^|;) ?' + encoded_key + '=([^;]*)(;|$)');
      decoded_value = cookie_data ? decodeURIComponent(cookie_data[2]) : null;
    }

    if (this.supports_local_storagecookie_data ||=== this.supports_cookiesnull) {
      // Ignore SyntaxError from decoded strings
      //  and if so return decoded value instead
      try {undefined;
        return JSON.parse(decoded_value);
      } catch (e) {
        if (!decodeURIComponent(e instanceof SyntaxErrorcookie_data[2])) throw e;
      }
      return decoded_value;;
    }

    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
   */
  remove(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.
      this.set(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 {boolean|number|string*}                     value - stored either under localStorage or as a cookie
   * @param {number}         days_to_live[days_to_live=false] - how long a browser is suggested to keep cookies
   * @this Browser_Storage
   */
  set(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, encoded_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 {
          date.setTime(date.getTime() + (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]);
      });
      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 key = encodeURIComponent(cookie.split('=')[0].replace(/(^\s+|\s+$)/g, ''));
        if (key) this.remove(key);
      });
      return true;
    }

    throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
  }

}

Note, I did try to use the web code option when writing this question but that caused various errors.

browser-storage.test.js

Jest tests preformed on above questionable JavaScript code.

/**
 * @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
 */


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'))();
  }

  runTests() {
    this.setAndGet();
    this.setAndGet({supports_local_storage: false});
    this.errorThrowers();
    this.resetsAndWipes();
  }

  /**
   * Sets properties for `this.storage` from key value pares
   * @returns {none}
   * @param {forced_states} states -
   * @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()` and `get()` 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"'
            ],
          },
        ],
      };

      expect(this.storage.set('test__json', test_json, 3)).toBe(true);

      const stored_json = this.storage.get('test__json');
      Object.keys(test_json).forEach((key) => {
        expect(test_json[key]).toStrictEqual(stored_json[key]);
      });
    });

    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);
    });

    test('CharCode key with `set()` and `get()` methods', () => {
      // `9842` is decimal for _`universal recycling`_
      const test_char = String.fromCharCode('9842');
      expect(this.storage.set(test_char, 'universal recycling', 3)).toBe(true);
      expect(this.storage.get(test_char)).toBe('universal recycling');
    });

  }

  /**
   * 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 `set()` without storage support', () => {
      expect(() => {
        this.storage.set('test__boolean', true, 3);
      }).toThrow(ReferenceError);
    });

    test('Getting previously set `test__boolean` without storage support', () => {
      expect(() => {
        this.storage.get('test__boolean');
      }).toThrow(ReferenceError);
    });
  }

  /**
   * Reports failures if any values _linger_ when wiping
   * @returns {none}
   * @this Browser_Storage_Test
   */
  resetsAndWipes() {
    test('Re-freshing class properties', () => {
      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.remove(key)).toBe(true);
        expect(this.storage.get(key)).toBe(undefined);
      });
    });

    this.forceStorageState({supports_local_storage: false});
    test('Clearing cookies', () => {
      expect(this.storage.clear()).toBe(true);
    });

    test('Than non-set key is undefined', () => {
      expect(this.storage.get('null')).toBe(undefined);
    });
  }

}


const test_storage = new Browser_Storage_Test();

test_storage.runTests();
  • Currently I'm using decodeURIComponent to JSON.parse when returning browser stored values, however, that leads to requiring a try/catch for strings which seems messy, so is there a better way of retrieving values?Currently I'm using decodeURIComponent to JSON.parse when returning browser stored values, however, that leads to requiring a try/catch for strings which seems messy, so is there a better way of retrieving values?

At this timeNever mind that last bit, got it almost feels like I'll have to do some pre-checking prior to returning in order to get some consistency in behaviorsorted and now things return undefined when something is undefined.


Thanks, and I hope that even if the above is me being a bad influence that we can all learn why together.

class Browser_Storage {
  /**
   * Sets properties used by other methods of this class
   * @returns {none}
   * @property {boolean} supports_local_storage - What `this.supportsLocalStorage()` had to say
   * @property {boolean} supports_cookies       - What `this.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.supportsLocalStorage();
    this.supports_cookies = this.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.supportsLocalStorage();
    this.supports_cookies = this.supportsCookies();
    this.storage_available = (this.supports_cookies || this.supports_local_storage) ? true : false;
  }

  /**
   * Tests and reports `boolean` if `localStorage` is available
   * @returns {boolean}
   */
  supportsLocalStorage() {
    try {
      localStorage.setItem('test_key', true);
    } catch (e) {
      return false;
    } finally {
      localStorage.removeItem('test_key');
    }
    return true;
  }

  /**
   * Reports if `navigator.cookieEnabled` or `document.cookie` are available
   * Note, use `this.supports_cookies` instead within tests.
   * @returns {boolean}
   * @this Browser_Storage
   */
  supportsCookies() {
    if (navigator.cookieEnabled) return true;

    try {
      document.cookie = 'testcookie';
    } catch (e) {
      return false;
    }

    if (document.cookie.indexOf('testcookie') != -1) {
      this.remove('testcookie');
      return true;
    }
    return false;
  }

  /**
   * Gets decoded/JSON value for given key
   * @returns {?boolean|?number|?string|?undefined}
   * @throws {ReferenceError} When no browser based storage is available
   * @param {string|number} key - Name of key to look up value for.
   * @this Browser_Storage
   */
  get(key) {
    const encoded_key = encodeURIComponent(key);
    let decoded_value = undefined;
    if (this.supports_local_storage) {
      decoded_value = decodeURIComponent(localStorage.getItem(encoded_key));
    } else if (this.supports_cookies) {
      let cookie_data = document.cookie.match('(^|;) ?' + encoded_key + '=([^;]*)(;|$)');
      decoded_value = cookie_data ? decodeURIComponent(cookie_data[2]) : null;
    }

    if (this.supports_local_storage || this.supports_cookies) {
      // Ignore SyntaxError from decoded strings
      //  and if so return decoded value instead
      try {
        return JSON.parse(decoded_value);
      } catch (e) {
        if (!(e instanceof SyntaxError)) throw e;
      }
      return decoded_value;
    }

    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
   */
  remove(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.
      this.set(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 {boolean|number|string} value - stored either under localStorage or as a cookie
   * @param {number}         days_to_live - how long a browser is suggested to keep cookies
   * @this Browser_Storage
   */
  set(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, encoded_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 {
          date.setTime(date.getTime() + (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]);
      });
      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 key = encodeURIComponent(cookie.split('=')[0].replace(/(^\s+|\s+$)/g, ''));
        if (key) this.remove(key);
      });
      return true;
    }

    throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
  }

}

Note, I did try to use the web code option when writing this question but that caused various errors.

  • Currently I'm using decodeURIComponent to JSON.parse when returning browser stored values, however, that leads to requiring a try/catch for strings which seems messy, so is there a better way of retrieving values?

At this time it almost feels like I'll have to do some pre-checking prior to returning in order to get some consistency in behavior.


Thanks, and I hope that even if the above is me being a bad influence that we can all learn why together.

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` are available
   * Note, use `this.supports_cookies` instead within tests.
   * @returns {boolean}
   * @this Browser_Storage
   */
  static supportsCookies() {
    if (navigator.cookieEnabled) return true;

    try {
      document.cookie = 'testcookie';
    } catch (e) {
      return false;
    }

    if (document.cookie.indexOf('testcookie') != -1) {
      this.remove('testcookie');
      return true;
    }
    return false;
  }

  /**
   * 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
   */
  get(key) {
    const encoded_key = encodeURIComponent(key);
    let decoded_value = undefined;
    if (this.supports_local_storage) {
      const raw_value = localStorage.getItem(encoded_key);
      if (raw_value === null) 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 JSON.parse(decodeURIComponent(cookie_data[2]));
    }

    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
   */
  remove(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.
      this.set(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
   */
  set(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, encoded_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 {
          date.setTime(date.getTime() + (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]);
      });
      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 key = encodeURIComponent(cookie.split('=')[0].replace(/(^\s+|\s+$)/g, ''));
        if (key) this.remove(key);
      });
      return true;
    }

    throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
  }

}

Note, I did try to use the web code option when writing this question but that caused various errors.

browser-storage.test.js

Jest tests preformed on above questionable JavaScript code.

/**
 * @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
 */


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'))();
  }

  runTests() {
    this.setAndGet();
    this.setAndGet({supports_local_storage: false});
    this.errorThrowers();
    this.resetsAndWipes();
  }

  /**
   * Sets properties for `this.storage` from key value pares
   * @returns {none}
   * @param {forced_states} states -
   * @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()` and `get()` 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"'
            ],
          },
        ],
      };

      expect(this.storage.set('test__json', test_json, 3)).toBe(true);

      const stored_json = this.storage.get('test__json');
      Object.keys(test_json).forEach((key) => {
        expect(test_json[key]).toStrictEqual(stored_json[key]);
      });
    });

    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);
    });

    test('CharCode key with `set()` and `get()` methods', () => {
      // `9842` is decimal for _`universal recycling`_
      const test_char = String.fromCharCode('9842');
      expect(this.storage.set(test_char, 'universal recycling', 3)).toBe(true);
      expect(this.storage.get(test_char)).toBe('universal recycling');
    });

  }

  /**
   * 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 `set()` without storage support', () => {
      expect(() => {
        this.storage.set('test__boolean', true, 3);
      }).toThrow(ReferenceError);
    });

    test('Getting previously set `test__boolean` without storage support', () => {
      expect(() => {
        this.storage.get('test__boolean');
      }).toThrow(ReferenceError);
    });
  }

  /**
   * Reports failures if any values _linger_ when wiping
   * @returns {none}
   * @this Browser_Storage_Test
   */
  resetsAndWipes() {
    test('Re-freshing class properties', () => {
      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.remove(key)).toBe(true);
        expect(this.storage.get(key)).toBe(undefined);
      });
    });

    this.forceStorageState({supports_local_storage: false});
    test('Clearing cookies', () => {
      expect(this.storage.clear()).toBe(true);
    });

    test('Than non-set key is undefined', () => {
      expect(this.storage.get('null')).toBe(undefined);
    });
  }

}


const test_storage = new Browser_Storage_Test();

test_storage.runTests();
  • Currently I'm using decodeURIComponent to JSON.parse when returning browser stored values, however, that leads to requiring a try/catch for strings which seems messy, so is there a better way of retrieving values?

Never mind that last bit, got it sorted and now things return undefined when something is undefined.

Updating JavaScript based off Jest tests and automated code review tools
Source Link
S0AndS0
  • 968
  • 1
  • 6
  • 15
class Browser_Storage {
  /**
   * Sets properties used by other methods of this class
   * @returns {none}
   * @property {boolean} supports_local_storage - What `this.supportsLocalStorage()` had to say
   * @property {boolean} supports_cookies       - What `this.supportsCookies()` had to say
   * @property {boolean} storage_available      - If either of the above is `true`
   * @this Browser_Storage
   * @copyright S0AndS0 2019 GNU AGPL version 3@class
   */
  constructor() {
    this.supports_local_storage = this.supportsLocalStorage();
    this.supports_cookies = this.supportsCookies();
    this.storage_available = (this.supports_cookies || this.supports_local_storage) ? true : false;
  }

  /**
   * Note,Copy useof `this.supports_local_storage`constructor` insteadthat withinshould testsnot throw `TypeError` when called
   * @returns {none}
   * @this Browser_Storage
   */
  constructorRefresh() {
    this.supports_local_storage = this.supportsLocalStorage();
    this.supports_cookies = this.supportsCookies();
    this.storage_available = (this.supports_cookies || this.supports_local_storage) ? true : false;
  }

  /**
   * Tests and reports `boolean` if `localStorage` is available
   * @returns {boolean}
   */
  supportsLocalStorage() {
    try {
      localStorage.setItem('test_key', true);
    } catch (e) {
      return false;
    } finally {
      localStorage.removeItem('test_key');
    }
    return true;
  }

  /**
   * Reports if `navigator.cookieEnabled` or `document.cookie` are available
   * Note, use `this.supports_cookies` instead within tests.
   * @returns {boolean}
   * @this Browser_Storage
   */
  supportsCookies() {
    if (navigator.cookieEnabled) return true;

    try {
      document.cookie = 'testcookie';
    } catch (e) {
      return false;
    }

    if (document.cookie.indexOf('testcookie') != -1) {
      this.remove('testcookie');
      return true;
    }
    return false;
  }

  /**
   * Copy of `constructor` method that shouldGets notdecoded/JSON throughvalue afor typegiven errorkey
   * @returns {none?boolean|?number|?string|?undefined}
   * @this Browser_Storage
   */
  constructorRefresh()@throws {
    this.supports_local_storage = this.supportsLocalStorage();
    this.supports_cookies = this.supportsCookies();
    this.storage_available = (this.supports_cookies || this.supports_local_storage) ? true : false;
  ReferenceError}

  /**
   * Returns decoded value for givenWhen key
no browser based *storage @returnsis {?boolean|?number|?string}available
   * @param {string|number} key - Name of key to look up value for.
   * @throws Error when no local storage options where detected
   * @this Browser_Storage
   */
  get(key) {
    const encoded_key = encodeURIComponent(key);
    let decoded_value = null;undefined;
    if (this.supports_local_storage) {
      decoded_value = decodeURIComponent(localStorage.getItem(keyencoded_key));
    } else if (this.supports_cookies) {
      let cookie_data = document.cookie.match('(^|;) ?' + keyencoded_key + '=([^;]*)(;|$)');
      decoded_value = cookie_data ? decodeURIComponent(cookie_data[2]) : null;
    }

    if (this.supports_local_storage || this.supports_cookies) {
      // Ignore SyntaxError from decoded strings
      //  and if so return decoded value instead
      try {
        return JSON.parse(decoded_value);
      } catch (e) {
        if (!(e instanceof SyntaxError)) throw e;
      }
      return decoded_value;
    }
 
    throw new ErrorReferenceError('No'Browser localstorage browserunavailable storageas optionsof available!last constructorRefresh()');
  }

  /**
   * Removes select value by key from browser storage; note for cookies, _full wipe_ will occur on nextrequire page loadrefresh
   * @returns {boolean}
   * @throws Error{ReferenceError} whenWhen no localbrowser storagebased optionsstorage whereis detectedavailable
   * @this Browser_Storage
   */
  remove(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.
      this.set(key, '', -7);
      return true;
    }
    throw new ErrorReferenceError('No'Browser localstorage browserunavailable storageas optionsof available!last constructorRefresh()');
  }

  /**
   * Stores clientencoded settingsJSON within browser, 
 _serverless_ in the* truest@returns scene{boolean}
   * @throws {ReferenceError} When no browser based storage is available
   * @param {string|number}           key - _variable name_ to store value under
   * @param {boolean|number|string} value - stored either under localStorage or as a cookie
   * @param {number}         days_to_live - how long a browser is suggested to keep cookies
   * @returns {boolean}
   * @this Browser_Storage
   */
  set(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(keyencoded_key, valueencoded_value);
      return true;
    } else if (this.supports_cookies) {
      let expires = '';
      if (days_to_live) {
        letconst date = new Date();
        if (days_to_live == 0) {
          date.setTime(date.getTime());
        } else {
          date.setTime(date.getTime() + (days_to_live * 24 * 60 * 60 * 1000));
        }
        expires = '; expires=' + date.toGMTString();
      }

      document.cookie = nameencoded_key + '=' + valueencoded_value + expires + '; path=/';
      return true;
    }
 
    returnthrow false;new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
  }

  /**
   * Clears **all** clientLists settingskeys fromthat eithermay localStoragepoint orto cookiesvalues
   * @returns {boolean}
   * @throws Error{ReferenceError} whenWhen no localbrowser based storage optionsis whereavailable
 detected  * @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]);
      });
      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) {
      let cookies = document.cookie.split(';');
      for (let i = 0; i < cookies.length; i++) {
        let forEach((cookie) ==> cookies[i];{
        letconst key = encodeURIComponent(cookie.split('=')[0].replace(/(^\s+|\s+$)/g, ''));
        if (typeof(key) == 'string' || typeof(key) == 'number') {
          this.remove(key);
        } else {
          return false;
        }
      });
      return true;
    }
 
    throw new ErrorReferenceError('No'Browser localstorage browserunavailable storageas optionsof available!last constructorRefresh()');
  }

}
class Browser_Storage {
  /**
   * @this Browser_Storage
   * @copyright S0AndS0 2019 GNU AGPL version 3
   */
  constructor() {
    this.supports_local_storage = this.supportsLocalStorage();
    this.supports_cookies = this.supportsCookies();
    this.storage_available = (this.supports_cookies || this.supports_local_storage) ? true : false;
  }

  /**
   * Note, use `this.supports_local_storage` instead within tests.
   * @returns {boolean}
   */
  supportsLocalStorage() {
    try {
      localStorage.setItem('test_key', true);
    } catch (e) {
      return false;
    } finally {
      localStorage.removeItem('test_key');
    }
    return true;
  }

  /**
   * Note, use `this.supports_cookies` instead within tests.
   * @returns {boolean}
   * @this Browser_Storage
   */
  supportsCookies() {
    if (navigator.cookieEnabled) return true;

    try {
      document.cookie = 'testcookie';
    } catch (e) {
      return false;
    }

    if (document.cookie.indexOf('testcookie') != -1) {
      this.remove('testcookie');
      return true;
    }
    return false;
  }

  /**
   * Copy of `constructor` method that should not through a type error
   * @returns {none}
   * @this Browser_Storage
   */
  constructorRefresh() {
    this.supports_local_storage = this.supportsLocalStorage();
    this.supports_cookies = this.supportsCookies();
    this.storage_available = (this.supports_cookies || this.supports_local_storage) ? true : false;
  }

  /**
   * Returns decoded value for given key
   * @returns {?boolean|?number|?string}
   * @param {string|number} key - Name of key to look up value for.
   * @throws Error when no local storage options where detected
   * @this Browser_Storage
   */
  get(key) {
    let decoded_value = null;
    if (this.supports_local_storage) {
      decoded_value = decodeURIComponent(localStorage.getItem(key));
    } else if (this.supports_cookies) {
      let cookie_data = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
      decoded_value = cookie_data ? decodeURIComponent(cookie_data[2]) : null;
    }

    if (this.supports_local_storage || this.supports_cookies) {
      // Ignore SyntaxError from decoded strings
      //  and if so return decoded value instead
      try {
        return JSON.parse(decoded_value);
      } catch (e) {
        if (!(e instanceof SyntaxError)) throw e;
      }
    }
    throw new Error('No local browser storage options available!');
  }

  /**
   * Removes select value by key from browser storage; note for cookies, _full wipe_ will occur on next page load
   * @returns {boolean}
   * @throws Error when no local storage options where detected
   * @this Browser_Storage
   */
  remove(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.
      this.set(key, '', -7);
      return true;
    }
    throw new Error('No local browser storage options available!');
  }

  /**
   * Stores client settings within browser, _serverless_ in the truest scene
   * @param {string|number}           key - _variable name_ to store value under
   * @param {boolean|number|string} value - stored either under localStorage or as a cookie
   * @param {number}         days_to_live - how long a browser is suggested to keep cookies
   * @returns {boolean}
   * @this Browser_Storage
   */
  set(key, value, days_to_live = false) {
    if (this.supports_local_storage) {
      localStorage.setItem(key, value);
      return true;
    } else if (this.supports_cookies) {
      let expires = '';
      if (days_to_live) {
        let date = new Date();
        if (days_to_live == 0) {
          date.setTime(date.getTime());
        } else {
          date.setTime(date.getTime() + (days_to_live * 24 * 60 * 60 * 1000));
        }
        expires = '; expires=' + date.toGMTString();
      }

      document.cookie = name + '=' + value + expires + '; path=/';
      return true;
    }
    return false;
  }

  /**
   * Clears **all** client settings from either localStorage or cookies
   * @returns {boolean}
   * @throws Error when no local storage options where detected
   * @this Browser_Storage
   */
  clear() {
    if (this.supports_local_storage) {
      localStorage.clear();
      return true;
    } else if (this.supports_cookies) {
      let cookies = document.cookie.split(';');
      for (let i = 0; i < cookies.length; i++) {
        let cookie = cookies[i];
        let key = encodeURIComponent(cookie.split('=')[0].replace(/(^\s+|\s+$)/g, ''));
        if (typeof(key) == 'string' || typeof(key) == 'number') {
          this.remove(key);
        } else {
          return false;
        }
      }
      return true;
    }
    throw new Error('No local browser storage options available!');
  }

}
class Browser_Storage {
  /**
   * Sets properties used by other methods of this class
   * @returns {none}
   * @property {boolean} supports_local_storage - What `this.supportsLocalStorage()` had to say
   * @property {boolean} supports_cookies       - What `this.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.supportsLocalStorage();
    this.supports_cookies = this.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.supportsLocalStorage();
    this.supports_cookies = this.supportsCookies();
    this.storage_available = (this.supports_cookies || this.supports_local_storage) ? true : false;
  }

  /**
   * Tests and reports `boolean` if `localStorage` is available
   * @returns {boolean}
   */
  supportsLocalStorage() {
    try {
      localStorage.setItem('test_key', true);
    } catch (e) {
      return false;
    } finally {
      localStorage.removeItem('test_key');
    }
    return true;
  }

  /**
   * Reports if `navigator.cookieEnabled` or `document.cookie` are available
   * Note, use `this.supports_cookies` instead within tests.
   * @returns {boolean}
   * @this Browser_Storage
   */
  supportsCookies() {
    if (navigator.cookieEnabled) return true;

    try {
      document.cookie = 'testcookie';
    } catch (e) {
      return false;
    }

    if (document.cookie.indexOf('testcookie') != -1) {
      this.remove('testcookie');
      return true;
    }
    return false;
  }

  /**
   * Gets decoded/JSON value for given key
   * @returns {?boolean|?number|?string|?undefined}
   * @throws {ReferenceError} When no browser based storage is available
   * @param {string|number} key - Name of key to look up value for.
   * @this Browser_Storage
   */
  get(key) {
    const encoded_key = encodeURIComponent(key);
    let decoded_value = undefined;
    if (this.supports_local_storage) {
      decoded_value = decodeURIComponent(localStorage.getItem(encoded_key));
    } else if (this.supports_cookies) {
      let cookie_data = document.cookie.match('(^|;) ?' + encoded_key + '=([^;]*)(;|$)');
      decoded_value = cookie_data ? decodeURIComponent(cookie_data[2]) : null;
    }

    if (this.supports_local_storage || this.supports_cookies) {
      // Ignore SyntaxError from decoded strings
      //  and if so return decoded value instead
      try {
        return JSON.parse(decoded_value);
      } catch (e) {
        if (!(e instanceof SyntaxError)) throw e;
      }
      return decoded_value;
    }
 
    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
   */
  remove(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.
      this.set(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 {boolean|number|string} value - stored either under localStorage or as a cookie
   * @param {number}         days_to_live - how long a browser is suggested to keep cookies
   * @this Browser_Storage
   */
  set(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, encoded_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 {
          date.setTime(date.getTime() + (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]);
      });
      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 key = encodeURIComponent(cookie.split('=')[0].replace(/(^\s+|\s+$)/g, ''));
        if (key) this.remove(key);
      });
      return true;
    }
 
    throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
  }

}
Updating code, now with 20% less gluten! Also updated one question because of code updates.
Source Link
S0AndS0
  • 968
  • 1
  • 6
  • 15
Loading
Clarified expectations on answers
Source Link
S0AndS0
  • 968
  • 1
  • 6
  • 15
Loading
css corrections for hiding bell on hover and spelling corrections
Source Link
S0AndS0
  • 968
  • 1
  • 6
  • 15
Loading
Source Link
S0AndS0
  • 968
  • 1
  • 6
  • 15
Loading