Commit e26c093a authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '357216-local-storage-sync-serialize-by-default' into 'master'

Serialize and deserialize by default for LocalStorageSync component

See merge request gitlab-org/gitlab!84159
parents 724e4858 d808cf4d
...@@ -57,6 +57,7 @@ export default { ...@@ -57,6 +57,7 @@ export default {
:value="sortDirection" :value="sortDirection"
:storage-key="storageKey" :storage-key="storageKey"
:persist="persistSortOrder" :persist="persistSortOrder"
as-string
@input="setDiscussionSortDirection({ direction: $event })" @input="setDiscussionSortDirection({ direction: $event })"
/> />
<gl-dropdown :text="dropdownText" class="js-dropdown-text full-width-mobile"> <gl-dropdown :text="dropdownText" class="js-dropdown-text full-width-mobile">
......
...@@ -99,7 +99,6 @@ export default { ...@@ -99,7 +99,6 @@ export default {
<local-storage-sync <local-storage-sync
storage-key="package_registry_list_sorting" storage-key="package_registry_list_sorting"
:value="sorting" :value="sorting"
as-json
@input="updateSorting" @input="updateSorting"
> >
<url-sync> <url-sync>
......
...@@ -273,6 +273,7 @@ export default { ...@@ -273,6 +273,7 @@ export default {
<local-storage-sync <local-storage-sync
:storage-key="$options.viewTypeKey" :storage-key="$options.viewTypeKey"
:value="currentViewType" :value="currentViewType"
as-string
@input="updateViewType" @input="updateViewType"
> >
<graph-view-selector <graph-view-selector
......
...@@ -144,7 +144,6 @@ export default { ...@@ -144,7 +144,6 @@ export default {
<local-storage-sync <local-storage-sync
v-model="autoDevopsEnabledAlertDismissedProjects" v-model="autoDevopsEnabledAlertDismissedProjects"
:storage-key="$options.autoDevopsEnabledAlertStorageKey" :storage-key="$options.autoDevopsEnabledAlertStorageKey"
as-json
/> />
<user-callout-dismisser <user-callout-dismisser
......
...@@ -112,7 +112,6 @@ export default { ...@@ -112,7 +112,6 @@ export default {
v-model="mergeRequestMeta" v-model="mergeRequestMeta"
:storage-key="$options.storageKey" :storage-key="$options.storageKey"
:clear="clearStorage" :clear="clearStorage"
as-json
/> />
<edit-meta-controls <edit-meta-controls
ref="editMetaControls" ref="editMetaControls"
......
...@@ -37,7 +37,7 @@ export default { ...@@ -37,7 +37,7 @@ export default {
<template> <template>
<div v-show="showAlert"> <div v-show="showAlert">
<local-storage-sync v-model="isDismissed" :storage-key="storageKey" as-json /> <local-storage-sync v-model="isDismissed" :storage-key="storageKey" />
<gl-alert v-if="showAlert" @dismiss="dismissFeedbackAlert"> <gl-alert v-if="showAlert" @dismiss="dismissFeedbackAlert">
<slot></slot> <slot></slot>
</gl-alert> </gl-alert>
......
<script> <script>
import { isEqual } from 'lodash'; import { isEqual, isString } from 'lodash';
/**
* This component will save and restore a value to and from localStorage.
* The value will be saved only when the value changes; the initial value won't be saved.
*
* By default, the value will be saved using JSON.stringify(), and retrieved back using JSON.parse().
*
* If you would like to save the raw string instead, you may set the 'asString' prop to true, though be aware that this is a
* legacy prop to maintain backwards compatibility.
*
* For new components saving data for the first time, it's recommended to not use 'asString' even if you're saving a string; it will still be
* saved and restored properly using JSON.stringify()/JSON.parse().
*/
export default { export default {
props: { props: {
storageKey: { storageKey: {
...@@ -12,7 +24,7 @@ export default { ...@@ -12,7 +24,7 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
asJson: { asString: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
...@@ -30,6 +42,8 @@ export default { ...@@ -30,6 +42,8 @@ export default {
}, },
watch: { watch: {
value(newVal) { value(newVal) {
if (!this.persist) return;
this.saveValue(this.serialize(newVal)); this.saveValue(this.serialize(newVal));
}, },
clear(newVal) { clear(newVal) {
...@@ -67,15 +81,22 @@ export default { ...@@ -67,15 +81,22 @@ export default {
} }
}, },
saveValue(val) { saveValue(val) {
if (!this.persist) return;
localStorage.setItem(this.storageKey, val); localStorage.setItem(this.storageKey, val);
}, },
serialize(val) { serialize(val) {
return this.asJson ? JSON.stringify(val) : val; if (!isString(val) && this.asString) {
// eslint-disable-next-line no-console
console.warn(
`[gitlab] LocalStorageSync is saving`,
val,
`to the key "${this.storageKey}", but it is not a string and the 'asString' prop is true. This will save and restore the stringified value rather than the original value. If this is not intended, please remove or set the 'asString' prop to false.`,
);
}
return this.asString ? val : JSON.stringify(val);
}, },
deserialize(val) { deserialize(val) {
return this.asJson ? JSON.parse(val) : val; return this.asString ? val : JSON.parse(val);
}, },
}, },
render() { render() {
......
...@@ -43,7 +43,7 @@ export default { ...@@ -43,7 +43,7 @@ export default {
</script> </script>
<template> <template>
<local-storage-sync :storage-key="storageKey" :value="selected" @input="setSelected"> <local-storage-sync :storage-key="storageKey" :value="selected" as-string @input="setSelected">
<gl-dropdown :text="dropdownText" lazy> <gl-dropdown :text="dropdownText" lazy>
<gl-dropdown-item <gl-dropdown-item
v-for="option in parsedOptions" v-for="option in parsedOptions"
......
...@@ -314,6 +314,7 @@ export default { ...@@ -314,6 +314,7 @@ export default {
<local-storage-sync <local-storage-sync
storage-key="gl-web-ide-button-selected" storage-key="gl-web-ide-button-selected"
:value="selection" :value="selection"
as-string
@input="select" @input="select"
/> />
<gl-modal <gl-modal
......
...@@ -157,8 +157,8 @@ export default { ...@@ -157,8 +157,8 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<local-storage-sync v-model="sortBy" :storage-key="$options.sortByStorageKey" as-json /> <local-storage-sync v-model="sortBy" :storage-key="$options.sortByStorageKey" />
<local-storage-sync v-model="sortDesc" :storage-key="$options.sortDescStorageKey" as-json /> <local-storage-sync v-model="sortDesc" :storage-key="$options.sortDescStorageKey" />
<h4>{{ tableHeader }}</h4> <h4>{{ tableHeader }}</h4>
<gl-table <gl-table
:fields="tableHeaderFields" :fields="tableHeaderFields"
......
...@@ -140,8 +140,8 @@ export default { ...@@ -140,8 +140,8 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<local-storage-sync v-model="sortBy" :storage-key="$options.sortByStorageKey" as-json /> <local-storage-sync v-model="sortBy" :storage-key="$options.sortByStorageKey" />
<local-storage-sync v-model="sortDesc" :storage-key="$options.sortDescStorageKey" as-json /> <local-storage-sync v-model="sortDesc" :storage-key="$options.sortDescStorageKey" />
<gl-table <gl-table
:fields="tableHeaderFields" :fields="tableHeaderFields"
:items="enabledNamespaces" :items="enabledNamespaces"
......
...@@ -74,7 +74,6 @@ export default { ...@@ -74,7 +74,6 @@ export default {
<local-storage-sync <local-storage-sync
v-model="userManuallyCollapsed" v-model="userManuallyCollapsed"
:storage-key="$options.MR_APPROVALS_PROMO_DISMISSED" :storage-key="$options.MR_APPROVALS_PROMO_DISMISSED"
as-json
/> />
<template v-if="isReady"> <template v-if="isReady">
<p class="gl-mb-0 gl-text-gray-500"> <p class="gl-mb-0 gl-text-gray-500">
......
<script> <script>
import { GlToggle } from '@gitlab/ui'; import { GlToggle } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
export default { export default {
...@@ -20,9 +19,6 @@ export default { ...@@ -20,9 +19,6 @@ export default {
onToggle(val) { onToggle(val) {
this.setShowLabels(val); this.setShowLabels(val);
}, },
onStorageUpdate(val) {
this.setShowLabels(parseBoolean(val));
},
}, },
}; };
</script> </script>
...@@ -30,9 +26,9 @@ export default { ...@@ -30,9 +26,9 @@ export default {
<template> <template>
<div class="board-labels-toggle-wrapper gl-display-flex gl-align-items-center gl-ml-3"> <div class="board-labels-toggle-wrapper gl-display-flex gl-align-items-center gl-ml-3">
<local-storage-sync <local-storage-sync
:value="isShowingLabels"
storage-key="gl-show-board-labels" storage-key="gl-show-board-labels"
:value="JSON.stringify(isShowingLabels)" @input="setShowLabels"
@input="onStorageUpdate"
/> />
<gl-toggle <gl-toggle
:value="isShowingLabels" :value="isShowingLabels"
......
...@@ -324,7 +324,6 @@ export default { ...@@ -324,7 +324,6 @@ export default {
<gl-form novalidate @submit.prevent="onSubmit()"> <gl-form novalidate @submit.prevent="onSubmit()">
<local-storage-sync <local-storage-sync
v-if="!isEdit" v-if="!isEdit"
as-json
:storage-key="storageKey" :storage-key="storageKey"
:clear="clearStorage" :clear="clearStorage"
:value="formFieldValues" :value="formFieldValues"
......
<script> <script>
import { difference } from 'lodash'; import { difference } from 'lodash';
import { getCookie, setCookie, parseBoolean } from '~/lib/utils/common_utils'; import { getCookie, setCookie } from '~/lib/utils/common_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { translateScannerNames } from '~/security_configuration/utils'; import { translateScannerNames } from '~/security_configuration/utils';
...@@ -86,8 +86,8 @@ export default { ...@@ -86,8 +86,8 @@ export default {
setCookie(this.$options.autoFixUserCalloutCookieName, 'true'); setCookie(this.$options.autoFixUserCalloutCookieName, 'true');
this.shouldShowAutoFixUserCallout = false; this.shouldShowAutoFixUserCallout = false;
}, },
setScannerAlertDismissed(value) { dismissScannerAlert() {
this.scannerAlertDismissed = parseBoolean(value); this.scannerAlertDismissed = true;
}, },
}, },
projectVulnerabilitiesQuery, projectVulnerabilitiesQuery,
...@@ -101,15 +101,14 @@ export default { ...@@ -101,15 +101,14 @@ export default {
<div v-else> <div v-else>
<local-storage-sync <local-storage-sync
:value="String(scannerAlertDismissed)" v-model="scannerAlertDismissed"
:storage-key="$options.SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY" :storage-key="$options.SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY"
@input="setScannerAlertDismissed"
/> />
<security-scanner-alert <security-scanner-alert
v-if="shouldShowScannersAlert" v-if="shouldShowScannersAlert"
:not-enabled-scanners="notEnabledSecurityScanners" :not-enabled-scanners="notEnabledSecurityScanners"
:no-pipeline-run-scanners="noPipelineRunSecurityScanners" :no-pipeline-run-scanners="noPipelineRunSecurityScanners"
@dismiss="setScannerAlertDismissed('true')" @dismiss="dismissScannerAlert"
/> />
<auto-fix-user-callout <auto-fix-user-callout
......
...@@ -262,7 +262,6 @@ export default { ...@@ -262,7 +262,6 @@ export default {
<local-storage-sync <local-storage-sync
v-if="shouldShowPageSizeSelector" v-if="shouldShowPageSizeSelector"
v-model="pageSize" v-model="pageSize"
as-json
:storage-key="$options.PAGE_SIZE_STORAGE_KEY" :storage-key="$options.PAGE_SIZE_STORAGE_KEY"
> >
<page-size-selector v-model="pageSize" class="gl-absolute gl-right-0" /> <page-size-selector v-model="pageSize" class="gl-absolute gl-right-0" />
......
...@@ -83,7 +83,7 @@ export default { ...@@ -83,7 +83,7 @@ export default {
</script> </script>
<template> <template>
<local-storage-sync v-model="surveyShowDate" :storage-key="storageKey"> <local-storage-sync v-model="surveyShowDate" :storage-key="storageKey" as-string>
<gl-banner <gl-banner
v-if="shouldShowSurvey" v-if="shouldShowSurvey"
:title="title" :title="title"
......
...@@ -373,10 +373,7 @@ describe('Vulnerability list GraphQL component', () => { ...@@ -373,10 +373,7 @@ describe('Vulnerability list GraphQL component', () => {
findPageSizeSelector().vm.$emit('input', pageSize); findPageSizeSelector().vm.$emit('input', pageSize);
await nextTick(); await nextTick();
expect(findLocalStorageSync().props()).toMatchObject({ expect(findLocalStorageSync().props('value')).toBe(pageSize);
asJson: true,
value: pageSize,
});
}); });
}); });
}); });
......
...@@ -15,6 +15,7 @@ describe('Shared Survey Banner component', () => { ...@@ -15,6 +15,7 @@ describe('Shared Survey Banner component', () => {
let wrapper; let wrapper;
const findGlBanner = () => wrapper.findComponent(GlBanner); const findGlBanner = () => wrapper.findComponent(GlBanner);
const findAskLaterButton = () => wrapper.findByTestId('ask-later-button'); const findAskLaterButton = () => wrapper.findByTestId('ask-later-button');
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const getOffsetDateString = (days) => { const getOffsetDateString = (days) => {
const date = new Date(); const date = new Date();
...@@ -60,6 +61,7 @@ describe('Shared Survey Banner component', () => { ...@@ -60,6 +61,7 @@ describe('Shared Survey Banner component', () => {
expect(findGlBanner().html()).toContain(description); expect(findGlBanner().html()).toContain(description);
expect(findAskLaterButton().exists()).toBe(true); expect(findAskLaterButton().exists()).toBe(true);
expect(findLocalStorageSync().props('asString')).toBe(true);
expect(findGlBanner().props()).toMatchObject({ expect(findGlBanner().props()).toMatchObject({
title, title,
buttonText, buttonText,
......
...@@ -38,8 +38,8 @@ describe('Sort Discussion component', () => { ...@@ -38,8 +38,8 @@ describe('Sort Discussion component', () => {
createComponent(); createComponent();
}); });
it('has local storage sync', () => { it('has local storage sync with the correct props', () => {
expect(findLocalStorageSync().exists()).toBe(true); expect(findLocalStorageSync().props('asString')).toBe(true);
}); });
it('calls setDiscussionSortDirection when update is emitted', () => { it('calls setDiscussionSortDirection when update is emitted', () => {
......
...@@ -73,7 +73,6 @@ describe('Package Search', () => { ...@@ -73,7 +73,6 @@ describe('Package Search', () => {
mountComponent(); mountComponent();
expect(findLocalStorageSync().props()).toMatchObject({ expect(findLocalStorageSync().props()).toMatchObject({
asJson: true,
storageKey: 'package_registry_list_sorting', storageKey: 'package_registry_list_sorting',
value: { value: {
orderBy: LIST_KEY_CREATED_AT, orderBy: LIST_KEY_CREATED_AT,
......
...@@ -30,6 +30,7 @@ import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue'; ...@@ -30,6 +30,7 @@ import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils'; import * as parsingUtils from '~/pipelines/components/parsing_utils';
import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql'; import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
import * as sentryUtils from '~/pipelines/utils'; import * as sentryUtils from '~/pipelines/utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { mockRunningPipelineHeaderData } from '../mock_data'; import { mockRunningPipelineHeaderData } from '../mock_data';
import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data'; import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data';
...@@ -55,6 +56,7 @@ describe('Pipeline graph wrapper', () => { ...@@ -55,6 +56,7 @@ describe('Pipeline graph wrapper', () => {
wrapper.find(StageColumnComponent).findAll('[data-testid="stage-column-group"]'); wrapper.find(StageColumnComponent).findAll('[data-testid="stage-column-group"]');
const getViewSelector = () => wrapper.find(GraphViewSelector); const getViewSelector = () => wrapper.find(GraphViewSelector);
const getViewSelectorTrip = () => getViewSelector().findComponent(GlAlert); const getViewSelectorTrip = () => getViewSelector().findComponent(GlAlert);
const getLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const createComponent = ({ const createComponent = ({
apolloProvider, apolloProvider,
...@@ -376,6 +378,10 @@ describe('Pipeline graph wrapper', () => { ...@@ -376,6 +378,10 @@ describe('Pipeline graph wrapper', () => {
localStorage.clear(); localStorage.clear();
}); });
it('sets the asString prop on the LocalStorageSync component', () => {
expect(getLocalStorageSync().props('asString')).toBe(true);
});
it('reads the view type from localStorage when available', () => { it('reads the view type from localStorage when available', () => {
const viewSelectorNeedsSegment = wrapper const viewSelectorNeedsSegment = wrapper
.find(GlButtonGroup) .find(GlButtonGroup)
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
const STORAGE_KEY = 'key';
describe('Local Storage Sync', () => { describe('Local Storage Sync', () => {
let wrapper; let wrapper;
const createComponent = ({ props = {}, slots = {} } = {}) => { const createComponent = ({ value, asString = false, slots = {} } = {}) => {
wrapper = shallowMount(LocalStorageSync, { wrapper = shallowMount(LocalStorageSync, {
propsData: props, propsData: { storageKey: STORAGE_KEY, value, asString },
slots, slots,
}); });
}; };
const setStorageValue = (value) => localStorage.setItem(STORAGE_KEY, value);
const getStorageValue = (value) => localStorage.getItem(STORAGE_KEY, value);
afterEach(() => { afterEach(() => {
if (wrapper) { wrapper.destroy();
wrapper.destroy();
}
wrapper = null;
localStorage.clear(); localStorage.clear();
}); });
it('is a renderless component', () => { it('is a renderless component', () => {
const html = '<div class="test-slot"></div>'; const html = '<div class="test-slot"></div>';
createComponent({ createComponent({
props: {
storageKey: 'key',
},
slots: { slots: {
default: html, default: html,
}, },
...@@ -35,233 +33,136 @@ describe('Local Storage Sync', () => { ...@@ -35,233 +33,136 @@ describe('Local Storage Sync', () => {
}); });
describe('localStorage empty', () => { describe('localStorage empty', () => {
const storageKey = 'issue_list_order';
it('does not emit input event', () => { it('does not emit input event', () => {
createComponent({ createComponent({ value: 'ascending' });
props: {
storageKey,
value: 'ascending',
},
});
expect(wrapper.emitted('input')).toBeFalsy();
});
it.each('foo', 3, true, ['foo', 'bar'], { foo: 'bar' })(
'saves updated value to localStorage',
async (newValue) => {
createComponent({
props: {
storageKey,
value: 'initial',
},
});
wrapper.setProps({ value: newValue });
await nextTick(); expect(wrapper.emitted('input')).toBeUndefined();
expect(localStorage.getItem(storageKey)).toBe(String(newValue)); });
},
);
it('does not save default value', () => {
const value = 'ascending';
createComponent({ it('does not save initial value if it did not change', () => {
props: { createComponent({ value: 'ascending' });
storageKey,
value,
},
});
expect(localStorage.getItem(storageKey)).toBe(null); expect(getStorageValue()).toBeNull();
}); });
}); });
describe('localStorage has saved value', () => { describe('localStorage has saved value', () => {
const storageKey = 'issue_list_order_by';
const savedValue = 'last_updated'; const savedValue = 'last_updated';
beforeEach(() => { beforeEach(() => {
localStorage.setItem(storageKey, savedValue); setStorageValue(savedValue);
createComponent({ asString: true });
}); });
it('emits input event with saved value', () => { it('emits input event with saved value', () => {
createComponent({
props: {
storageKey,
value: 'ascending',
},
});
expect(wrapper.emitted('input')[0][0]).toBe(savedValue); expect(wrapper.emitted('input')[0][0]).toBe(savedValue);
}); });
it('does not overwrite localStorage with prop value', () => { it('does not overwrite localStorage with initial prop value', () => {
createComponent({ expect(getStorageValue()).toBe(savedValue);
props: {
storageKey,
value: 'created',
},
});
expect(localStorage.getItem(storageKey)).toBe(savedValue);
}); });
it('updating the value updates localStorage', async () => { it('updating the value updates localStorage', async () => {
createComponent({
props: {
storageKey,
value: 'created',
},
});
const newValue = 'last_updated'; const newValue = 'last_updated';
wrapper.setProps({ await wrapper.setProps({ value: newValue });
value: newValue,
});
await nextTick(); expect(getStorageValue()).toBe(newValue);
expect(localStorage.getItem(storageKey)).toBe(newValue);
}); });
});
describe('persist prop', () => {
it('persists the value by default', async () => { it('persists the value by default', async () => {
const persistedValue = 'persisted'; const persistedValue = 'persisted';
createComponent({ asString: true });
// Sanity check to make sure we start with nothing saved.
expect(getStorageValue()).toBeNull();
createComponent({ await wrapper.setProps({ value: persistedValue });
props: {
storageKey,
},
});
wrapper.setProps({ value: persistedValue }); expect(getStorageValue()).toBe(persistedValue);
await nextTick();
expect(localStorage.getItem(storageKey)).toBe(persistedValue);
}); });
it('does not save a value if persist is set to false', async () => { it('does not save a value if persist is set to false', async () => {
const value = 'saved';
const notPersistedValue = 'notPersisted'; const notPersistedValue = 'notPersisted';
createComponent({ asString: true });
// Save some value so we can test that it's not overwritten.
await wrapper.setProps({ value });
createComponent({ expect(getStorageValue()).toBe(value);
props: {
storageKey,
},
});
wrapper.setProps({ persist: false, value: notPersistedValue }); await wrapper.setProps({ persist: false, value: notPersistedValue });
await nextTick();
expect(localStorage.getItem(storageKey)).not.toBe(notPersistedValue); expect(getStorageValue()).toBe(value);
}); });
}); });
describe('with "asJson" prop set to "true"', () => { describe('saving and restoring', () => {
const storageKey = 'testStorageKey'; it.each`
value | asString
describe.each` ${'foo'} | ${true}
value | serializedValue ${'foo'} | ${false}
${null} | ${'null'} ${'{ a: 1 }'} | ${true}
${''} | ${'""'} ${'{ a: 1 }'} | ${false}
${true} | ${'true'} ${3} | ${false}
${false} | ${'false'} ${['foo', 'bar']} | ${false}
${42} | ${'42'} ${{ foo: 'bar' }} | ${false}
${'42'} | ${'"42"'} ${null} | ${false}
${'{ foo: '} | ${'"{ foo: "'} ${' '} | ${false}
${['test']} | ${'["test"]'} ${true} | ${false}
${{ foo: 'bar' }} | ${'{"foo":"bar"}'} ${false} | ${false}
`('given $value', ({ value, serializedValue }) => { ${42} | ${false}
describe('is a new value', () => { ${'42'} | ${false}
beforeEach(async () => { ${'{ foo: '} | ${false}
createComponent({ `('saves and restores the same value', async ({ value, asString }) => {
props: { // Create an initial component to save the value.
storageKey, createComponent({ asString });
value: 'initial', await wrapper.setProps({ value });
asJson: true, wrapper.destroy();
}, // Create a second component to restore the value. Restore is only done once, when the
}); // component is first mounted.
createComponent({ asString });
wrapper.setProps({ value });
expect(wrapper.emitted('input')[0][0]).toEqual(value);
await 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', () => { it('shows a warning when trying to save a non-string value when asString prop is true', async () => {
const badJSON = '{ badJSON'; const spy = jest.spyOn(console, 'warn').mockImplementation();
createComponent({ asString: true });
beforeEach(() => { await wrapper.setProps({ value: [] });
jest.spyOn(console, 'warn').mockImplementation();
localStorage.setItem(storageKey, badJSON); expect(spy).toHaveBeenCalled();
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();
});
}); });
}); });
it('clears localStorage when clear property is true', async () => { describe('with bad JSON in storage', () => {
const storageKey = 'key'; const badJSON = '{ badJSON';
const value = 'initial'; let spy;
createComponent({ beforeEach(() => {
props: { spy = jest.spyOn(console, 'warn').mockImplementation();
storageKey, setStorageValue(badJSON);
}, createComponent();
}); });
wrapper.setProps({
value, it('should console warn', () => {
expect(spy).toHaveBeenCalled();
}); });
await nextTick(); it('should not emit an input event', () => {
expect(wrapper.emitted('input')).toBeUndefined();
});
});
expect(localStorage.getItem(storageKey)).toBe(value); it('clears localStorage when clear property is true', async () => {
const value = 'initial';
createComponent({ asString: true });
await wrapper.setProps({ value });
wrapper.setProps({ expect(getStorageValue()).toBe(value);
clear: true,
});
await nextTick(); await wrapper.setProps({ clear: true });
expect(localStorage.getItem(storageKey)).toBe(null); expect(getStorageValue()).toBeNull();
}); });
}); });
...@@ -36,10 +36,10 @@ describe('Persisted dropdown selection', () => { ...@@ -36,10 +36,10 @@ describe('Persisted dropdown selection', () => {
}); });
describe('local storage sync', () => { describe('local storage sync', () => {
it('uses the local storage sync component', () => { it('uses the local storage sync component with the correct props', () => {
createComponent(); createComponent();
expect(findLocalStorageSync().exists()).toBe(true); expect(findLocalStorageSync().props('asString')).toBe(true);
}); });
it('passes the right props', () => { it('passes the right props', () => {
......
...@@ -261,7 +261,10 @@ describe('Web IDE link component', () => { ...@@ -261,7 +261,10 @@ describe('Web IDE link component', () => {
}); });
it('should update local storage when selection changes', async () => { it('should update local storage when selection changes', async () => {
expect(findLocalStorageSync().props('value')).toBe(ACTION_WEB_IDE.key); expect(findLocalStorageSync().props()).toMatchObject({
asString: true,
value: ACTION_WEB_IDE.key,
});
findActionsButton().vm.$emit('select', ACTION_GITPOD.key); findActionsButton().vm.$emit('select', ACTION_GITPOD.key);
......
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