Commit ab9e43cb authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '247850-add-json-support-to-localstoragesync-component' into 'master'

Add JSON support to localStorageSync component

Closes #247850

See merge request gitlab-org/gitlab!42938
parents 9c5eda55 16a3b700
<script>
import { isEqual } from 'lodash';
export default {
props: {
storageKey: {
......@@ -6,31 +8,58 @@ export default {
required: true,
},
value: {
type: String,
type: [String, Number, Boolean, Array, Object],
required: false,
default: '',
},
asJson: {
type: Boolean,
required: false,
default: false,
},
},
watch: {
value(newVal) {
this.saveValue(newVal);
this.saveValue(this.serialize(newVal));
},
},
mounted() {
// 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);
}
},
methods: {
getValue() {
return localStorage.getItem(this.storageKey);
getStorageValue() {
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) {
localStorage.setItem(this.storageKey, val);
},
serialize(val) {
return this.asJson ? JSON.stringify(val) : val;
},
deserialize(val) {
return this.asJson ? JSON.parse(val) : val;
},
},
render() {
return this.$slots.default;
......
......@@ -35,7 +35,7 @@ export const createLocalStorageSpy = () => {
clear: jest.fn(() => {
storage = {};
}),
getItem: jest.fn(key => storage[key]),
getItem: jest.fn(key => (key in storage ? storage[key] : null)),
setItem: jest.fn((key, value) => {
storage[key] = value;
}),
......
......@@ -18,11 +18,11 @@ describe('localStorage helper', () => {
localStorage.removeItem('test', 'testing');
expect(localStorage.getItem('test')).toBeUndefined();
expect(localStorage.getItem('test')).toBe(null);
expect(localStorage.getItem('test2')).toBe('testing');
localStorage.clear();
expect(localStorage.getItem('test2')).toBeUndefined();
expect(localStorage.getItem('test2')).toBe(null);
});
});
......@@ -12,7 +12,9 @@ describe('Local Storage Sync', () => {
};
afterEach(() => {
wrapper.destroy();
if (wrapper) {
wrapper.destroy();
}
wrapper = null;
localStorage.clear();
});
......@@ -45,23 +47,23 @@ describe('Local Storage Sync', () => {
expect(wrapper.emitted('input')).toBeFalsy();
});
it('saves updated value to localStorage', () => {
createComponent({
props: {
storageKey,
value: 'ascending',
},
});
const newValue = 'descending';
wrapper.setProps({
value: newValue,
});
return wrapper.vm.$nextTick().then(() => {
expect(localStorage.getItem(storageKey)).toBe(newValue);
});
});
it.each('foo', 3, true, ['foo', 'bar'], { foo: 'bar' })(
'saves updated value to localStorage',
newValue => {
createComponent({
props: {
storageKey,
value: 'initial',
},
});
wrapper.setProps({ value: newValue });
return wrapper.vm.$nextTick().then(() => {
expect(localStorage.getItem(storageKey)).toBe(String(newValue));
});
},
);
it('does not save default value', () => {
const value = 'ascending';
......@@ -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