Commit 4c7a794a authored by Phil Hughes's avatar Phil Hughes

Merge branch 'fallback-localstorage-cases' into 'master'

Fallback localstorage cases

Closes #30179 and #25788

See merge request !10937
parents b8153535 bef42d9a
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */
import AccessorUtilities from './lib/utils/accessor';
window.Autosave = (function() { window.Autosave = (function() {
function Autosave(field, key) { function Autosave(field, key) {
this.field = field; this.field = field;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
if (key.join != null) { if (key.join != null) {
key = key.join("/"); key = key.join("/");
} }
...@@ -17,16 +20,12 @@ window.Autosave = (function() { ...@@ -17,16 +20,12 @@ window.Autosave = (function() {
} }
Autosave.prototype.restore = function() { Autosave.prototype.restore = function() {
var e, text; var text;
if (window.localStorage == null) {
return; if (!this.isLocalStorageAvailable) return;
}
try { text = window.localStorage.getItem(this.key);
text = window.localStorage.getItem(this.key);
} catch (error) {
e = error;
return;
}
if ((text != null ? text.length : void 0) > 0) { if ((text != null ? text.length : void 0) > 0) {
this.field.val(text); this.field.val(text);
} }
...@@ -35,27 +34,22 @@ window.Autosave = (function() { ...@@ -35,27 +34,22 @@ window.Autosave = (function() {
Autosave.prototype.save = function() { Autosave.prototype.save = function() {
var text; var text;
if (window.localStorage == null) {
return;
}
text = this.field.val(); text = this.field.val();
if ((text != null ? text.length : void 0) > 0) {
try { if (this.isLocalStorageAvailable && (text != null ? text.length : void 0) > 0) {
return window.localStorage.setItem(this.key, text); return window.localStorage.setItem(this.key, text);
} catch (error) {}
} else {
return this.reset();
} }
return this.reset();
}; };
Autosave.prototype.reset = function() { Autosave.prototype.reset = function() {
if (window.localStorage == null) { if (!this.isLocalStorageAvailable) return;
return;
} return window.localStorage.removeItem(this.key);
try {
return window.localStorage.removeItem(this.key);
} catch (error) {}
}; };
return Autosave; return Autosave;
})(); })();
export default window.Autosave;
import AccessorUtilities from '../../lib/utils/accessor';
const unicodeSupportTestMap = { const unicodeSupportTestMap = {
// man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/ // man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// occupationZwj: '\u{1F468}\u{200D}\u{1F393}', // occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
...@@ -140,16 +142,25 @@ function generateUnicodeSupportMap(testMap) { ...@@ -140,16 +142,25 @@ function generateUnicodeSupportMap(testMap) {
function getUnicodeSupportMap() { function getUnicodeSupportMap() {
let unicodeSupportMap; let unicodeSupportMap;
const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent'); let userAgentFromCache;
const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
if (isLocalStorageAvailable) userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
try { try {
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map')); unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
} catch (err) { } catch (err) {
// swallow // swallow
} }
if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) { if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap); unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
}
} }
return unicodeSupportMap; return unicodeSupportMap;
......
...@@ -8,6 +8,11 @@ export default { ...@@ -8,6 +8,11 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
isLocalStorageAvailable: {
type: Boolean,
required: false,
default: true,
},
}, },
computed: { computed: {
...@@ -47,7 +52,12 @@ export default { ...@@ -47,7 +52,12 @@ export default {
template: ` template: `
<div> <div>
<ul v-if="hasItems"> <div
v-if="!isLocalStorageAvailable"
class="dropdown-info-note">
This feature requires local storage to be enabled
</div>
<ul v-else-if="hasItems">
<li <li
v-for="(item, index) in processedItems" v-for="(item, index) in processedItems"
:key="index"> :key="index">
......
/* global Flash */
import FilteredSearchContainer from './container'; import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root'; import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store'; import RecentSearchesStore from './stores/recent_searches_store';
...@@ -15,7 +13,9 @@ class FilteredSearchManager { ...@@ -15,7 +13,9 @@ class FilteredSearchManager {
this.tokensContainer = this.container.querySelector('.tokens-container'); this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
this.recentSearchesStore = new RecentSearchesStore(); this.recentSearchesStore = new RecentSearchesStore({
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
});
let recentSearchesKey = 'issue-recent-searches'; let recentSearchesKey = 'issue-recent-searches';
if (page === 'merge_requests') { if (page === 'merge_requests') {
recentSearchesKey = 'merge-request-recent-searches'; recentSearchesKey = 'merge-request-recent-searches';
...@@ -24,9 +24,10 @@ class FilteredSearchManager { ...@@ -24,9 +24,10 @@ class FilteredSearchManager {
// Fetch recent searches from localStorage // Fetch recent searches from localStorage
this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch() this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
.catch(() => { .catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Flash('An error occured while parsing recent searches'); new window.Flash('An error occured while parsing recent searches');
// Gracefully fail to empty array // Gracefully fail to empty array
return []; return [];
}) })
......
...@@ -183,6 +183,9 @@ class FilteredSearchVisualTokens { ...@@ -183,6 +183,9 @@ class FilteredSearchVisualTokens {
static moveInputToTheRight() { static moveInputToTheRight() {
const input = FilteredSearchContainer.container.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
if (!input) return;
const inputLi = input.parentElement; const inputLi = input.parentElement;
const tokenContainer = FilteredSearchContainer.container.querySelector('.tokens-container'); const tokenContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
......
...@@ -29,12 +29,15 @@ class RecentSearchesRoot { ...@@ -29,12 +29,15 @@ class RecentSearchesRoot {
} }
render() { render() {
const state = this.store.state;
this.vm = new Vue({ this.vm = new Vue({
el: this.wrapperElement, el: this.wrapperElement,
data: this.store.state, data() { return state; },
template: ` template: `
<recent-searches-dropdown-content <recent-searches-dropdown-content
:items="recentSearches" /> :items="recentSearches"
:is-local-storage-available="isLocalStorageAvailable"
/>
`, `,
components: { components: {
'recent-searches-dropdown-content': RecentSearchesDropdownContent, 'recent-searches-dropdown-content': RecentSearchesDropdownContent,
......
import RecentSearchesServiceError from './recent_searches_service_error';
import AccessorUtilities from '../../lib/utils/accessor';
class RecentSearchesService { class RecentSearchesService {
constructor(localStorageKey = 'issuable-recent-searches') { constructor(localStorageKey = 'issuable-recent-searches') {
this.localStorageKey = localStorageKey; this.localStorageKey = localStorageKey;
} }
fetch() { fetch() {
if (!RecentSearchesService.isAvailable()) {
const error = new RecentSearchesServiceError();
return Promise.reject(error);
}
const input = window.localStorage.getItem(this.localStorageKey); const input = window.localStorage.getItem(this.localStorageKey);
let searches = []; let searches = [];
...@@ -19,8 +27,14 @@ class RecentSearchesService { ...@@ -19,8 +27,14 @@ class RecentSearchesService {
} }
save(searches = []) { save(searches = []) {
if (!RecentSearchesService.isAvailable()) return;
window.localStorage.setItem(this.localStorageKey, JSON.stringify(searches)); window.localStorage.setItem(this.localStorageKey, JSON.stringify(searches));
} }
static isAvailable() {
return AccessorUtilities.isLocalStorageAccessSafe();
}
} }
export default RecentSearchesService; export default RecentSearchesService;
class RecentSearchesServiceError {
constructor(message) {
this.name = 'RecentSearchesServiceError';
this.message = message || 'Recent Searches Service is unavailable';
}
}
// Can't use `extends` for builtin prototypes and get true inheritance yet
RecentSearchesServiceError.prototype = Error.prototype;
export default RecentSearchesServiceError;
function isPropertyAccessSafe(base, property) {
let safe;
try {
safe = !!base[property];
} catch (error) {
safe = false;
}
return safe;
}
function isFunctionCallSafe(base, functionName, ...args) {
let safe = true;
try {
base[functionName](...args);
} catch (error) {
safe = false;
}
return safe;
}
function isLocalStorageAccessSafe() {
let safe;
const TEST_KEY = 'isLocalStorageAccessSafe';
const TEST_VALUE = 'true';
safe = isPropertyAccessSafe(window, 'localStorage');
if (!safe) return safe;
safe = isFunctionCallSafe(window.localStorage, 'setItem', TEST_KEY, TEST_VALUE);
if (safe) window.localStorage.removeItem(TEST_KEY);
return safe;
}
const AccessorUtilities = {
isPropertyAccessSafe,
isFunctionCallSafe,
isLocalStorageAccessSafe,
};
export default AccessorUtilities;
/* eslint no-param-reassign: ["error", { "props": false }]*/ /* eslint no-param-reassign: ["error", { "props": false }]*/
/* eslint no-new: "off" */ /* eslint no-new: "off" */
import AccessorUtilities from './lib/utils/accessor';
((global) => { ((global) => {
/** /**
* Memorize the last selected tab after reloading a page. * Memorize the last selected tab after reloading a page.
...@@ -9,6 +11,8 @@ ...@@ -9,6 +11,8 @@
constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) { constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
this.currentTabKey = currentTabKey; this.currentTabKey = currentTabKey;
this.tabSelector = tabSelector; this.tabSelector = tabSelector;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.bootstrap(); this.bootstrap();
} }
...@@ -37,11 +41,15 @@ ...@@ -37,11 +41,15 @@
} }
saveData(val) { saveData(val) {
localStorage.setItem(this.currentTabKey, val); if (!this.isLocalStorageAvailable) return undefined;
return window.localStorage.setItem(this.currentTabKey, val);
} }
readData() { readData() {
return localStorage.getItem(this.currentTabKey); if (!this.isLocalStorageAvailable) return null;
return window.localStorage.getItem(this.currentTabKey);
} }
} }
......
import Autosave from '~/autosave';
import AccessorUtilities from '~/lib/utils/accessor';
describe('Autosave', () => {
let autosave;
describe('class constructor', () => {
const key = 'key';
const field = jasmine.createSpyObj('field', ['data', 'on']);
beforeEach(() => {
spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(true);
spyOn(Autosave.prototype, 'restore');
autosave = new Autosave(field, key);
});
it('should set .isLocalStorageAvailable', () => {
expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
expect(autosave.isLocalStorageAvailable).toBe(true);
});
});
describe('restore', () => {
const key = 'key';
const field = jasmine.createSpyObj('field', ['trigger']);
beforeEach(() => {
autosave = {
field,
key,
};
spyOn(window.localStorage, 'getItem');
});
describe('if .isLocalStorageAvailable is `false`', () => {
beforeEach(() => {
autosave.isLocalStorageAvailable = false;
Autosave.prototype.restore.call(autosave);
});
it('should not call .getItem', () => {
expect(window.localStorage.getItem).not.toHaveBeenCalled();
});
});
describe('if .isLocalStorageAvailable is `true`', () => {
beforeEach(() => {
autosave.isLocalStorageAvailable = true;
Autosave.prototype.restore.call(autosave);
});
it('should call .getItem', () => {
expect(window.localStorage.getItem).toHaveBeenCalledWith(key);
});
});
});
describe('save', () => {
const field = jasmine.createSpyObj('field', ['val']);
beforeEach(() => {
autosave = jasmine.createSpyObj('autosave', ['reset']);
autosave.field = field;
field.val.and.returnValue('value');
spyOn(window.localStorage, 'setItem');
});
describe('if .isLocalStorageAvailable is `false`', () => {
beforeEach(() => {
autosave.isLocalStorageAvailable = false;
Autosave.prototype.save.call(autosave);
});
it('should not call .setItem', () => {
expect(window.localStorage.setItem).not.toHaveBeenCalled();
});
});
describe('if .isLocalStorageAvailable is `true`', () => {
beforeEach(() => {
autosave.isLocalStorageAvailable = true;
Autosave.prototype.save.call(autosave);
});
it('should call .setItem', () => {
expect(window.localStorage.setItem).toHaveBeenCalled();
});
});
});
describe('reset', () => {
const key = 'key';
beforeEach(() => {
autosave = {
key,
};
spyOn(window.localStorage, 'removeItem');
});
describe('if .isLocalStorageAvailable is `false`', () => {
beforeEach(() => {
autosave.isLocalStorageAvailable = false;
Autosave.prototype.reset.call(autosave);
});
it('should not call .removeItem', () => {
expect(window.localStorage.removeItem).not.toHaveBeenCalled();
});
});
describe('if .isLocalStorageAvailable is `true`', () => {
beforeEach(() => {
autosave.isLocalStorageAvailable = true;
Autosave.prototype.reset.call(autosave);
});
it('should call .removeItem', () => {
expect(window.localStorage.removeItem).toHaveBeenCalledWith(key);
});
});
});
});
import { getUnicodeSupportMap } from '~/behaviors/gl_emoji/unicode_support_map';
import AccessorUtilities from '~/lib/utils/accessor';
describe('Unicode Support Map', () => {
describe('getUnicodeSupportMap', () => {
const stringSupportMap = 'stringSupportMap';
beforeEach(() => {
spyOn(AccessorUtilities, 'isLocalStorageAccessSafe');
spyOn(window.localStorage, 'getItem');
spyOn(window.localStorage, 'setItem');
spyOn(JSON, 'parse');
spyOn(JSON, 'stringify').and.returnValue(stringSupportMap);
});
describe('if isLocalStorageAvailable is `true`', function () {
beforeEach(() => {
AccessorUtilities.isLocalStorageAccessSafe.and.returnValue(true);
getUnicodeSupportMap();
});
it('should call .getItem and .setItem', () => {
const allArgs = window.localStorage.setItem.calls.allArgs();
expect(window.localStorage.getItem).toHaveBeenCalledWith('gl-emoji-user-agent');
expect(allArgs[0][0]).toBe('gl-emoji-user-agent');
expect(allArgs[0][1]).toBe(navigator.userAgent);
expect(allArgs[1][0]).toBe('gl-emoji-unicode-support-map');
expect(allArgs[1][1]).toBe(stringSupportMap);
});
});
describe('if isLocalStorageAvailable is `false`', function () {
beforeEach(() => {
AccessorUtilities.isLocalStorageAccessSafe.and.returnValue(false);
getUnicodeSupportMap();
});
it('should not call .getItem or .setItem', () => {
expect(window.localStorage.getItem.calls.count()).toBe(1);
expect(window.localStorage.setItem).not.toHaveBeenCalled();
});
});
});
});
...@@ -76,6 +76,26 @@ describe('RecentSearchesDropdownContent', () => { ...@@ -76,6 +76,26 @@ describe('RecentSearchesDropdownContent', () => {
}); });
}); });
describe('if isLocalStorageAvailable is `false`', () => {
let el;
beforeEach(() => {
const props = Object.assign({ isLocalStorageAvailable: false }, propsDataWithItems);
vm = createComponent(props);
el = vm.$el;
});
it('should render an info note', () => {
const note = el.querySelector('.dropdown-info-note');
const items = el.querySelectorAll('.filtered-search-history-dropdown-item');
expect(note).toBeDefined();
expect(note.innerText.trim()).toBe('This feature requires local storage to be enabled');
expect(items.length).toEqual(propsDataWithoutItems.items.length);
});
});
describe('computed', () => { describe('computed', () => {
describe('processedItems', () => { describe('processedItems', () => {
it('with items', () => { it('with items', () => {
......
import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
require('~/lib/utils/url_utility'); require('~/lib/utils/url_utility');
require('~/lib/utils/common_utils'); require('~/lib/utils/common_utils');
require('~/filtered_search/filtered_search_token_keys'); require('~/filtered_search/filtered_search_token_keys');
...@@ -60,6 +64,36 @@ describe('Filtered Search Manager', () => { ...@@ -60,6 +64,36 @@ describe('Filtered Search Manager', () => {
manager.cleanup(); manager.cleanup();
}); });
describe('class constructor', () => {
const isLocalStorageAvailable = 'isLocalStorageAvailable';
let filteredSearchManager;
beforeEach(() => {
spyOn(RecentSearchesService, 'isAvailable').and.returnValue(isLocalStorageAvailable);
spyOn(recentSearchesStoreSrc, 'default');
filteredSearchManager = new gl.FilteredSearchManager();
return filteredSearchManager;
});
it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => {
expect(RecentSearchesService.isAvailable).toHaveBeenCalled();
expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({
isLocalStorageAvailable,
});
});
it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => {
spyOn(RecentSearchesService.prototype, 'fetch').and.callFake(() => Promise.reject(new RecentSearchesServiceError()));
spyOn(window, 'Flash');
filteredSearchManager = new gl.FilteredSearchManager();
expect(window.Flash).not.toHaveBeenCalled();
});
});
describe('search', () => { describe('search', () => {
const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened'; const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
......
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
import * as vueSrc from 'vue';
describe('RecentSearchesRoot', () => {
describe('render', () => {
let recentSearchesRoot;
let data;
let template;
beforeEach(() => {
recentSearchesRoot = {
store: {
state: 'state',
},
};
spyOn(vueSrc, 'default').and.callFake((options) => {
data = options.data;
template = options.template;
});
RecentSearchesRoot.prototype.render.call(recentSearchesRoot);
});
it('should instantiate Vue', () => {
expect(vueSrc.default).toHaveBeenCalled();
expect(data()).toBe(recentSearchesRoot.store.state);
expect(template).toContain(':is-local-storage-available="isLocalStorageAvailable"');
});
});
});
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
describe('RecentSearchesServiceError', () => {
let recentSearchesServiceError;
beforeEach(() => {
recentSearchesServiceError = new RecentSearchesServiceError();
});
it('instantiates an instance of RecentSearchesServiceError and not an Error', () => {
expect(recentSearchesServiceError).toEqual(jasmine.any(RecentSearchesServiceError));
expect(recentSearchesServiceError.name).toBe('RecentSearchesServiceError');
});
it('should set a default message', () => {
expect(recentSearchesServiceError.message).toBe('Recent Searches Service is unavailable');
});
});
/* eslint-disable promise/catch-or-return */ /* eslint-disable promise/catch-or-return */
import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import AccessorUtilities from '~/lib/utils/accessor';
describe('RecentSearchesService', () => { describe('RecentSearchesService', () => {
let service; let service;
...@@ -11,6 +12,10 @@ describe('RecentSearchesService', () => { ...@@ -11,6 +12,10 @@ describe('RecentSearchesService', () => {
}); });
describe('fetch', () => { describe('fetch', () => {
beforeEach(() => {
spyOn(RecentSearchesService, 'isAvailable').and.returnValue(true);
});
it('should default to empty array', (done) => { it('should default to empty array', (done) => {
const fetchItemsPromise = service.fetch(); const fetchItemsPromise = service.fetch();
...@@ -29,11 +34,21 @@ describe('RecentSearchesService', () => { ...@@ -29,11 +34,21 @@ describe('RecentSearchesService', () => {
const fetchItemsPromise = service.fetch(); const fetchItemsPromise = service.fetch();
fetchItemsPromise fetchItemsPromise
.catch(() => { .catch((error) => {
expect(error).toEqual(jasmine.any(SyntaxError));
done(); done();
}); });
}); });
it('should reject when service is unavailable', (done) => {
RecentSearchesService.isAvailable.and.returnValue(false);
service.fetch().catch((error) => {
expect(error).toEqual(jasmine.any(Error));
done();
});
});
it('should return items from localStorage', (done) => { it('should return items from localStorage', (done) => {
window.localStorage.setItem(service.localStorageKey, '["foo", "bar"]'); window.localStorage.setItem(service.localStorageKey, '["foo", "bar"]');
const fetchItemsPromise = service.fetch(); const fetchItemsPromise = service.fetch();
...@@ -44,15 +59,89 @@ describe('RecentSearchesService', () => { ...@@ -44,15 +59,89 @@ describe('RecentSearchesService', () => {
done(); done();
}); });
}); });
describe('if .isAvailable returns `false`', () => {
beforeEach(() => {
RecentSearchesService.isAvailable.and.returnValue(false);
spyOn(window.localStorage, 'getItem');
RecentSearchesService.prototype.fetch();
});
it('should not call .getItem', () => {
expect(window.localStorage.getItem).not.toHaveBeenCalled();
});
});
}); });
describe('setRecentSearches', () => { describe('setRecentSearches', () => {
beforeEach(() => {
spyOn(RecentSearchesService, 'isAvailable').and.returnValue(true);
});
it('should save things in localStorage', () => { it('should save things in localStorage', () => {
const items = ['foo', 'bar']; const items = ['foo', 'bar'];
service.save(items); service.save(items);
const newLocalStorageValue = const newLocalStorageValue = window.localStorage.getItem(service.localStorageKey);
window.localStorage.getItem(service.localStorageKey);
expect(JSON.parse(newLocalStorageValue)).toEqual(items); expect(JSON.parse(newLocalStorageValue)).toEqual(items);
}); });
}); });
describe('save', () => {
beforeEach(() => {
spyOn(window.localStorage, 'setItem');
spyOn(RecentSearchesService, 'isAvailable');
});
describe('if .isAvailable returns `true`', () => {
const searchesString = 'searchesString';
const localStorageKey = 'localStorageKey';
const recentSearchesService = {
localStorageKey,
};
beforeEach(() => {
RecentSearchesService.isAvailable.and.returnValue(true);
spyOn(JSON, 'stringify').and.returnValue(searchesString);
RecentSearchesService.prototype.save.call(recentSearchesService);
});
it('should call .setItem', () => {
expect(window.localStorage.setItem).toHaveBeenCalledWith(localStorageKey, searchesString);
});
});
describe('if .isAvailable returns `false`', () => {
beforeEach(() => {
RecentSearchesService.isAvailable.and.returnValue(false);
RecentSearchesService.prototype.save();
});
it('should not call .setItem', () => {
expect(window.localStorage.setItem).not.toHaveBeenCalled();
});
});
});
describe('isAvailable', () => {
let isAvailable;
beforeEach(() => {
spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.callThrough();
isAvailable = RecentSearchesService.isAvailable();
});
it('should call .isLocalStorageAccessSafe', () => {
expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
});
it('should return a boolean', () => {
expect(typeof isAvailable).toBe('boolean');
});
});
}); });
import AccessorUtilities from '~/lib/utils/accessor';
describe('AccessorUtilities', () => {
const testError = new Error('test error');
describe('isPropertyAccessSafe', () => {
let base;
it('should return `true` if access is safe', () => {
base = { testProp: 'testProp' };
expect(AccessorUtilities.isPropertyAccessSafe(base, 'testProp')).toBe(true);
});
it('should return `false` if access throws an error', () => {
base = { get testProp() { throw testError; } };
expect(AccessorUtilities.isPropertyAccessSafe(base, 'testProp')).toBe(false);
});
it('should return `false` if property is undefined', () => {
base = {};
expect(AccessorUtilities.isPropertyAccessSafe(base, 'testProp')).toBe(false);
});
});
describe('isFunctionCallSafe', () => {
const base = {};
it('should return `true` if calling is safe', () => {
base.func = () => {};
expect(AccessorUtilities.isFunctionCallSafe(base, 'func')).toBe(true);
});
it('should return `false` if calling throws an error', () => {
base.func = () => { throw new Error('test error'); };
expect(AccessorUtilities.isFunctionCallSafe(base, 'func')).toBe(false);
});
it('should return `false` if function is undefined', () => {
base.func = undefined;
expect(AccessorUtilities.isFunctionCallSafe(base, 'func')).toBe(false);
});
});
describe('isLocalStorageAccessSafe', () => {
beforeEach(() => {
spyOn(window.localStorage, 'setItem');
spyOn(window.localStorage, 'removeItem');
});
it('should return `true` if access is safe', () => {
expect(AccessorUtilities.isLocalStorageAccessSafe()).toBe(true);
});
it('should return `false` if access to .setItem isnt safe', () => {
window.localStorage.setItem.and.callFake(() => { throw testError; });
expect(AccessorUtilities.isLocalStorageAccessSafe()).toBe(false);
});
it('should set a test item if access is safe', () => {
AccessorUtilities.isLocalStorageAccessSafe();
expect(window.localStorage.setItem).toHaveBeenCalledWith('isLocalStorageAccessSafe', 'true');
});
it('should remove the test item if access is safe', () => {
AccessorUtilities.isLocalStorageAccessSafe();
expect(window.localStorage.removeItem).toHaveBeenCalledWith('isLocalStorageAccessSafe');
});
});
});
import AccessorUtilities from '~/lib/utils/accessor';
require('~/signin_tabs_memoizer'); require('~/signin_tabs_memoizer');
((global) => { ((global) => {
...@@ -19,6 +21,8 @@ require('~/signin_tabs_memoizer'); ...@@ -19,6 +21,8 @@ require('~/signin_tabs_memoizer');
beforeEach(() => { beforeEach(() => {
loadFixtures(fixtureTemplate); loadFixtures(fixtureTemplate);
spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(true);
}); });
it('does nothing if no tab was previously selected', () => { it('does nothing if no tab was previously selected', () => {
...@@ -49,5 +53,91 @@ require('~/signin_tabs_memoizer'); ...@@ -49,5 +53,91 @@ require('~/signin_tabs_memoizer');
expect(memo.readData()).toEqual('#standard'); expect(memo.readData()).toEqual('#standard');
}); });
describe('class constructor', () => {
beforeEach(() => {
memo = createMemoizer();
});
it('should set .isLocalStorageAvailable', () => {
expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
expect(memo.isLocalStorageAvailable).toBe(true);
});
});
describe('saveData', () => {
beforeEach(() => {
memo = {
currentTabKey,
};
spyOn(localStorage, 'setItem');
});
describe('if .isLocalStorageAvailable is `false`', () => {
beforeEach(function () {
memo.isLocalStorageAvailable = false;
global.ActiveTabMemoizer.prototype.saveData.call(memo);
});
it('should not call .setItem', () => {
expect(localStorage.setItem).not.toHaveBeenCalled();
});
});
describe('if .isLocalStorageAvailable is `true`', () => {
const value = 'value';
beforeEach(function () {
memo.isLocalStorageAvailable = true;
global.ActiveTabMemoizer.prototype.saveData.call(memo, value);
});
it('should call .setItem', () => {
expect(localStorage.setItem).toHaveBeenCalledWith(currentTabKey, value);
});
});
});
describe('readData', () => {
const itemValue = 'itemValue';
let readData;
beforeEach(() => {
memo = {
currentTabKey,
};
spyOn(localStorage, 'getItem').and.returnValue(itemValue);
});
describe('if .isLocalStorageAvailable is `false`', () => {
beforeEach(function () {
memo.isLocalStorageAvailable = false;
readData = global.ActiveTabMemoizer.prototype.readData.call(memo);
});
it('should not call .getItem and should return `null`', () => {
expect(localStorage.getItem).not.toHaveBeenCalled();
expect(readData).toBe(null);
});
});
describe('if .isLocalStorageAvailable is `true`', () => {
beforeEach(function () {
memo.isLocalStorageAvailable = true;
readData = global.ActiveTabMemoizer.prototype.readData.call(memo);
});
it('should call .getItem and return the localStorage value', () => {
expect(window.localStorage.getItem).toHaveBeenCalledWith(currentTabKey);
expect(readData).toBe(itemValue);
});
});
});
}); });
})(window); })(window);
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment