Commit 16a3b700 authored by David Pisek's avatar David Pisek Committed by Paul Slaughter

Add JSON support to localStorageSync component

https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42938
parent a9dc7769
<script> <script>
import { isEqual } from 'lodash';
export default { export default {
props: { props: {
storageKey: { storageKey: {
...@@ -6,31 +8,58 @@ export default { ...@@ -6,31 +8,58 @@ export default {
required: true, required: true,
}, },
value: { value: {
type: String, type: [String, Number, Boolean, Array, Object],
required: false, required: false,
default: '', default: '',
}, },
asJson: {
type: Boolean,
required: false,
default: false,
},
}, },
watch: { watch: {
value(newVal) { value(newVal) {
this.saveValue(newVal); this.saveValue(this.serialize(newVal));
}, },
}, },
mounted() { mounted() {
// On mount, trigger update if we actually have a localStorageValue // On mount, trigger update if we actually have a localStorageValue
const value = this.getValue(); const { exists, value } = this.getStorageValue();
if (value && this.value !== value) { if (exists && !isEqual(value, this.value)) {
this.$emit('input', value); this.$emit('input', value);
} }
}, },
methods: { methods: {
getValue() { getStorageValue() {
return localStorage.getItem(this.storageKey); const value = localStorage.getItem(this.storageKey);
if (value === null) {
return { exists: false };
}
try {
return { exists: true, value: this.deserialize(value) };
} catch {
// eslint-disable-next-line no-console
console.warn(
`[gitlab] Failed to deserialize value from localStorage (key=${this.storageKey})`,
value,
);
// default to "don't use localStorage value"
return { exists: false };
}
}, },
saveValue(val) { saveValue(val) {
localStorage.setItem(this.storageKey, val); localStorage.setItem(this.storageKey, val);
}, },
serialize(val) {
return this.asJson ? JSON.stringify(val) : val;
},
deserialize(val) {
return this.asJson ? JSON.parse(val) : val;
},
}, },
render() { render() {
return this.$slots.default; return this.$slots.default;
......
...@@ -35,7 +35,7 @@ export const createLocalStorageSpy = () => { ...@@ -35,7 +35,7 @@ export const createLocalStorageSpy = () => {
clear: jest.fn(() => { clear: jest.fn(() => {
storage = {}; storage = {};
}), }),
getItem: jest.fn(key => storage[key]), getItem: jest.fn(key => (key in storage ? storage[key] : null)),
setItem: jest.fn((key, value) => { setItem: jest.fn((key, value) => {
storage[key] = value; storage[key] = value;
}), }),
......
...@@ -18,11 +18,11 @@ describe('localStorage helper', () => { ...@@ -18,11 +18,11 @@ describe('localStorage helper', () => {
localStorage.removeItem('test', 'testing'); localStorage.removeItem('test', 'testing');
expect(localStorage.getItem('test')).toBeUndefined(); expect(localStorage.getItem('test')).toBe(null);
expect(localStorage.getItem('test2')).toBe('testing'); expect(localStorage.getItem('test2')).toBe('testing');
localStorage.clear(); localStorage.clear();
expect(localStorage.getItem('test2')).toBeUndefined(); expect(localStorage.getItem('test2')).toBe(null);
}); });
}); });
...@@ -12,7 +12,9 @@ describe('Local Storage Sync', () => { ...@@ -12,7 +12,9 @@ describe('Local Storage Sync', () => {
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); if (wrapper) {
wrapper.destroy();
}
wrapper = null; wrapper = null;
localStorage.clear(); localStorage.clear();
}); });
...@@ -45,23 +47,23 @@ describe('Local Storage Sync', () => { ...@@ -45,23 +47,23 @@ describe('Local Storage Sync', () => {
expect(wrapper.emitted('input')).toBeFalsy(); expect(wrapper.emitted('input')).toBeFalsy();
}); });
it('saves updated value to localStorage', () => { it.each('foo', 3, true, ['foo', 'bar'], { foo: 'bar' })(
createComponent({ 'saves updated value to localStorage',
props: { newValue => {
storageKey, createComponent({
value: 'ascending', props: {
}, storageKey,
}); value: 'initial',
},
const newValue = 'descending'; });
wrapper.setProps({
value: newValue, wrapper.setProps({ value: newValue });
});
return wrapper.vm.$nextTick().then(() => {
return wrapper.vm.$nextTick().then(() => { expect(localStorage.getItem(storageKey)).toBe(String(newValue));
expect(localStorage.getItem(storageKey)).toBe(newValue); });
}); },
}); );
it('does not save default value', () => { it('does not save default value', () => {
const value = 'ascending'; const value = 'ascending';
...@@ -125,4 +127,88 @@ describe('Local Storage Sync', () => { ...@@ -125,4 +127,88 @@ describe('Local Storage Sync', () => {
}); });
}); });
}); });
describe('with "asJson" prop set to "true"', () => {
const storageKey = 'testStorageKey';
describe.each`
value | serializedValue
${null} | ${'null'}
${''} | ${'""'}
${true} | ${'true'}
${false} | ${'false'}
${42} | ${'42'}
${'42'} | ${'"42"'}
${'{ foo: '} | ${'"{ foo: "'}
${['test']} | ${'["test"]'}
${{ foo: 'bar' }} | ${'{"foo":"bar"}'}
`('given $value', ({ value, serializedValue }) => {
describe('is a new value', () => {
beforeEach(() => {
createComponent({
props: {
storageKey,
value: 'initial',
asJson: true,
},
});
wrapper.setProps({ value });
return wrapper.vm.$nextTick();
});
it('serializes the value correctly to localStorage', () => {
expect(localStorage.getItem(storageKey)).toBe(serializedValue);
});
});
describe('is already stored', () => {
beforeEach(() => {
localStorage.setItem(storageKey, serializedValue);
createComponent({
props: {
storageKey,
value: 'initial',
asJson: true,
},
});
});
it('emits an input event with the deserialized value', () => {
expect(wrapper.emitted('input')).toEqual([[value]]);
});
});
});
describe('with bad JSON in storage', () => {
const badJSON = '{ badJSON';
beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation();
localStorage.setItem(storageKey, badJSON);
createComponent({
props: {
storageKey,
value: 'initial',
asJson: true,
},
});
});
it('should console warn', () => {
// eslint-disable-next-line no-console
expect(console.warn).toHaveBeenCalledWith(
`[gitlab] Failed to deserialize value from localStorage (key=${storageKey})`,
badJSON,
);
});
it('should not emit an input event', () => {
expect(wrapper.emitted('input')).toBeUndefined();
});
});
});
}); });
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