mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-08-20 14:11:11 +00:00
Fallback localstorage cases
This commit is contained in:

committed by
Phil Hughes

parent
b815353510
commit
bef42d9a36
@ -1,8 +1,11 @@
|
||||
/* 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() {
|
||||
function Autosave(field, key) {
|
||||
this.field = field;
|
||||
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
|
||||
|
||||
if (key.join != null) {
|
||||
key = key.join("/");
|
||||
}
|
||||
@ -17,16 +20,12 @@ window.Autosave = (function() {
|
||||
}
|
||||
|
||||
Autosave.prototype.restore = function() {
|
||||
var e, text;
|
||||
if (window.localStorage == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
text = window.localStorage.getItem(this.key);
|
||||
} catch (error) {
|
||||
e = error;
|
||||
return;
|
||||
}
|
||||
var text;
|
||||
|
||||
if (!this.isLocalStorageAvailable) return;
|
||||
|
||||
text = window.localStorage.getItem(this.key);
|
||||
|
||||
if ((text != null ? text.length : void 0) > 0) {
|
||||
this.field.val(text);
|
||||
}
|
||||
@ -35,27 +34,22 @@ window.Autosave = (function() {
|
||||
|
||||
Autosave.prototype.save = function() {
|
||||
var text;
|
||||
if (window.localStorage == null) {
|
||||
return;
|
||||
}
|
||||
text = this.field.val();
|
||||
if ((text != null ? text.length : void 0) > 0) {
|
||||
try {
|
||||
return window.localStorage.setItem(this.key, text);
|
||||
} catch (error) {}
|
||||
} else {
|
||||
return this.reset();
|
||||
|
||||
if (this.isLocalStorageAvailable && (text != null ? text.length : void 0) > 0) {
|
||||
return window.localStorage.setItem(this.key, text);
|
||||
}
|
||||
|
||||
return this.reset();
|
||||
};
|
||||
|
||||
Autosave.prototype.reset = function() {
|
||||
if (window.localStorage == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return window.localStorage.removeItem(this.key);
|
||||
} catch (error) {}
|
||||
if (!this.isLocalStorageAvailable) return;
|
||||
|
||||
return window.localStorage.removeItem(this.key);
|
||||
};
|
||||
|
||||
return Autosave;
|
||||
})();
|
||||
|
||||
export default window.Autosave;
|
||||
|
@ -1,3 +1,5 @@
|
||||
import AccessorUtilities from '../../lib/utils/accessor';
|
||||
|
||||
const unicodeSupportTestMap = {
|
||||
// man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
|
||||
// occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
|
||||
@ -140,16 +142,25 @@ function generateUnicodeSupportMap(testMap) {
|
||||
|
||||
function getUnicodeSupportMap() {
|
||||
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 {
|
||||
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
|
||||
} catch (err) {
|
||||
// swallow
|
||||
}
|
||||
|
||||
if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
|
||||
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;
|
||||
|
@ -8,6 +8,11 @@ export default {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
isLocalStorageAvailable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
@ -47,7 +52,12 @@ export default {
|
||||
|
||||
template: `
|
||||
<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
|
||||
v-for="(item, index) in processedItems"
|
||||
:key="index">
|
||||
|
@ -1,5 +1,3 @@
|
||||
/* global Flash */
|
||||
|
||||
import FilteredSearchContainer from './container';
|
||||
import RecentSearchesRoot from './recent_searches_root';
|
||||
import RecentSearchesStore from './stores/recent_searches_store';
|
||||
@ -15,7 +13,9 @@ class FilteredSearchManager {
|
||||
this.tokensContainer = this.container.querySelector('.tokens-container');
|
||||
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
|
||||
|
||||
this.recentSearchesStore = new RecentSearchesStore();
|
||||
this.recentSearchesStore = new RecentSearchesStore({
|
||||
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
|
||||
});
|
||||
let recentSearchesKey = 'issue-recent-searches';
|
||||
if (page === 'merge_requests') {
|
||||
recentSearchesKey = 'merge-request-recent-searches';
|
||||
@ -24,9 +24,10 @@ class FilteredSearchManager {
|
||||
|
||||
// Fetch recent searches from localStorage
|
||||
this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
|
||||
.catch(() => {
|
||||
.catch((error) => {
|
||||
if (error.name === 'RecentSearchesServiceError') return undefined;
|
||||
// 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
|
||||
return [];
|
||||
})
|
||||
|
@ -183,6 +183,9 @@ class FilteredSearchVisualTokens {
|
||||
|
||||
static moveInputToTheRight() {
|
||||
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
||||
|
||||
if (!input) return;
|
||||
|
||||
const inputLi = input.parentElement;
|
||||
const tokenContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
|
||||
|
||||
|
@ -29,12 +29,15 @@ class RecentSearchesRoot {
|
||||
}
|
||||
|
||||
render() {
|
||||
const state = this.store.state;
|
||||
this.vm = new Vue({
|
||||
el: this.wrapperElement,
|
||||
data: this.store.state,
|
||||
data() { return state; },
|
||||
template: `
|
||||
<recent-searches-dropdown-content
|
||||
:items="recentSearches" />
|
||||
:items="recentSearches"
|
||||
:is-local-storage-available="isLocalStorageAvailable"
|
||||
/>
|
||||
`,
|
||||
components: {
|
||||
'recent-searches-dropdown-content': RecentSearchesDropdownContent,
|
||||
|
@ -1,9 +1,17 @@
|
||||
import RecentSearchesServiceError from './recent_searches_service_error';
|
||||
import AccessorUtilities from '../../lib/utils/accessor';
|
||||
|
||||
class RecentSearchesService {
|
||||
constructor(localStorageKey = 'issuable-recent-searches') {
|
||||
this.localStorageKey = localStorageKey;
|
||||
}
|
||||
|
||||
fetch() {
|
||||
if (!RecentSearchesService.isAvailable()) {
|
||||
const error = new RecentSearchesServiceError();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const input = window.localStorage.getItem(this.localStorageKey);
|
||||
|
||||
let searches = [];
|
||||
@ -19,8 +27,14 @@ class RecentSearchesService {
|
||||
}
|
||||
|
||||
save(searches = []) {
|
||||
if (!RecentSearchesService.isAvailable()) return;
|
||||
|
||||
window.localStorage.setItem(this.localStorageKey, JSON.stringify(searches));
|
||||
}
|
||||
|
||||
static isAvailable() {
|
||||
return AccessorUtilities.isLocalStorageAccessSafe();
|
||||
}
|
||||
}
|
||||
|
||||
export default RecentSearchesService;
|
||||
|
@ -0,0 +1,11 @@
|
||||
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;
|
47
app/assets/javascripts/lib/utils/accessor.js
Normal file
47
app/assets/javascripts/lib/utils/accessor.js
Normal file
@ -0,0 +1,47 @@
|
||||
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;
|
@ -1,5 +1,7 @@
|
||||
/* eslint no-param-reassign: ["error", { "props": false }]*/
|
||||
/* eslint no-new: "off" */
|
||||
import AccessorUtilities from './lib/utils/accessor';
|
||||
|
||||
((global) => {
|
||||
/**
|
||||
* Memorize the last selected tab after reloading a page.
|
||||
@ -9,6 +11,8 @@
|
||||
constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
|
||||
this.currentTabKey = currentTabKey;
|
||||
this.tabSelector = tabSelector;
|
||||
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
|
||||
|
||||
this.bootstrap();
|
||||
}
|
||||
|
||||
@ -37,11 +41,15 @@
|
||||
}
|
||||
|
||||
saveData(val) {
|
||||
localStorage.setItem(this.currentTabKey, val);
|
||||
if (!this.isLocalStorageAvailable) return undefined;
|
||||
|
||||
return window.localStorage.setItem(this.currentTabKey, val);
|
||||
}
|
||||
|
||||
readData() {
|
||||
return localStorage.getItem(this.currentTabKey);
|
||||
if (!this.isLocalStorageAvailable) return null;
|
||||
|
||||
return window.localStorage.getItem(this.currentTabKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
134
spec/javascripts/autosave_spec.js
Normal file
134
spec/javascripts/autosave_spec.js
Normal file
@ -0,0 +1,134 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,47 @@
|
||||
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', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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('processedItems', () => {
|
||||
it('with items', () => {
|
||||
|
@ -1,3 +1,7 @@
|
||||
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/common_utils');
|
||||
require('~/filtered_search/filtered_search_token_keys');
|
||||
@ -60,6 +64,36 @@ describe('Filtered Search Manager', () => {
|
||||
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', () => {
|
||||
const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
|
||||
|
||||
|
@ -0,0 +1,31 @@
|
||||
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"');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,18 @@
|
||||
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');
|
||||
});
|
||||
});
|
@ -1,6 +1,7 @@
|
||||
/* eslint-disable promise/catch-or-return */
|
||||
|
||||
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
|
||||
import AccessorUtilities from '~/lib/utils/accessor';
|
||||
|
||||
describe('RecentSearchesService', () => {
|
||||
let service;
|
||||
@ -11,6 +12,10 @@ describe('RecentSearchesService', () => {
|
||||
});
|
||||
|
||||
describe('fetch', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(RecentSearchesService, 'isAvailable').and.returnValue(true);
|
||||
});
|
||||
|
||||
it('should default to empty array', (done) => {
|
||||
const fetchItemsPromise = service.fetch();
|
||||
|
||||
@ -29,11 +34,21 @@ describe('RecentSearchesService', () => {
|
||||
const fetchItemsPromise = service.fetch();
|
||||
|
||||
fetchItemsPromise
|
||||
.catch(() => {
|
||||
.catch((error) => {
|
||||
expect(error).toEqual(jasmine.any(SyntaxError));
|
||||
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) => {
|
||||
window.localStorage.setItem(service.localStorageKey, '["foo", "bar"]');
|
||||
const fetchItemsPromise = service.fetch();
|
||||
@ -44,15 +59,89 @@ describe('RecentSearchesService', () => {
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(RecentSearchesService, 'isAvailable').and.returnValue(true);
|
||||
});
|
||||
|
||||
it('should save things in localStorage', () => {
|
||||
const items = ['foo', 'bar'];
|
||||
service.save(items);
|
||||
const newLocalStorageValue =
|
||||
window.localStorage.getItem(service.localStorageKey);
|
||||
const newLocalStorageValue = window.localStorage.getItem(service.localStorageKey);
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
78
spec/javascripts/lib/utils/accessor_spec.js
Normal file
78
spec/javascripts/lib/utils/accessor_spec.js
Normal file
@ -0,0 +1,78 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,3 +1,5 @@
|
||||
import AccessorUtilities from '~/lib/utils/accessor';
|
||||
|
||||
require('~/signin_tabs_memoizer');
|
||||
|
||||
((global) => {
|
||||
@ -19,6 +21,8 @@ require('~/signin_tabs_memoizer');
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures(fixtureTemplate);
|
||||
|
||||
spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(true);
|
||||
});
|
||||
|
||||
it('does nothing if no tab was previously selected', () => {
|
||||
@ -49,5 +53,91 @@ require('~/signin_tabs_memoizer');
|
||||
|
||||
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);
|
||||
|
Reference in New Issue
Block a user