Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Totals localstorage sync #5352

Merged
merged 7 commits into from
Dec 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added states.json in core/i18n/resource (#4531)
- Added phone validation helper (#4980)
- Configurable enabling min & max price aggregations
- Storing totals in localStorage to sync it between tabs ([#4733](https://github.com/vuestorefront/vue-storefront/issues/4733))

### Fixed

Expand Down
52 changes: 25 additions & 27 deletions core/modules/cart/helpers/cartCacheHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,31 @@ import { Logger } from '@vue-storefront/core/lib/logger'

import { StorageManager } from '@vue-storefront/core/lib/storage-manager'

export function cartCacheHandlerFactory (Vue) {
return (mutation, state) => {
const type = mutation.type;
export const cartCacheHandlerPlugin = (mutation, state) => {
const type = mutation.type;

if (
type.endsWith(types.CART_LOAD_CART) ||
type.endsWith(types.CART_ADD_ITEM) ||
type.endsWith(types.CART_DEL_ITEM) ||
type.endsWith(types.CART_UPD_ITEM) ||
type.endsWith(types.CART_DEL_NON_CONFIRMED_ITEM) ||
type.endsWith(types.CART_UPD_ITEM_PROPS)
) {
return StorageManager.get('cart').setItem('current-cart', state.cart.cartItems).catch((reason) => {
Logger.error(reason)() // it doesn't work on SSR
}) // populate cache
} else if (
type.endsWith(types.CART_LOAD_CART_SERVER_TOKEN)
) {
return StorageManager.get('cart').setItem('current-cart-token', state.cart.cartServerToken).catch((reason) => {
Logger.error(reason)()
})
} else if (
type.endsWith(types.CART_SET_ITEMS_HASH)
) {
return StorageManager.get('cart').setItem('current-cart-hash', state.cart.cartItemsHash).catch((reason) => {
Logger.error(reason)()
})
}
if (
type.endsWith(types.CART_LOAD_CART) ||
type.endsWith(types.CART_ADD_ITEM) ||
type.endsWith(types.CART_DEL_ITEM) ||
type.endsWith(types.CART_UPD_ITEM) ||
type.endsWith(types.CART_DEL_NON_CONFIRMED_ITEM) ||
type.endsWith(types.CART_UPD_ITEM_PROPS)
) {
return StorageManager.get('cart').setItem('current-cart', state.cart.cartItems).catch((reason) => {
Logger.error(reason)() // it doesn't work on SSR
}) // populate cache
} else if (
type.endsWith(types.CART_LOAD_CART_SERVER_TOKEN)
) {
return StorageManager.get('cart').setItem('current-cart-token', state.cart.cartServerToken).catch((reason) => {
Logger.error(reason)()
})
} else if (
type.endsWith(types.CART_SET_ITEMS_HASH)
) {
return StorageManager.get('cart').setItem('current-cart-hash', state.cart.cartItemsHash).catch((reason) => {
Logger.error(reason)()
})
}
}
6 changes: 4 additions & 2 deletions core/modules/cart/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cartCacheHandlerFactory } from './cartCacheHandler'
import { cartCacheHandlerPlugin } from './cartCacheHandler'
import { totalsCacheHandlerPlugin } from './totalsCacheHandler'
import optimizeProduct from './optimizeProduct'
import prepareProductsToAdd from './prepareProductsToAdd'
import productChecksum from './productChecksum'
Expand All @@ -18,7 +19,8 @@ import createShippingInfoData from './createShippingInfoData'
import * as syncCartWhenLocalStorageChange from './syncCartWhenLocalStorageChange'

export {
cartCacheHandlerFactory,
cartCacheHandlerPlugin,
totalsCacheHandlerPlugin,
optimizeProduct,
prepareProductsToAdd,
productChecksum,
Expand Down
6 changes: 4 additions & 2 deletions core/modules/cart/helpers/syncCartWhenLocalStorageChange.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import rootStore from '@vue-storefront/core/store';

function getItemsFromStorage ({ key }) {
const value = JSON.parse(localStorage[key])
if (key === 'shop/cart/current-cart') {
const storedItems = JSON.parse(localStorage[key])
rootStore.dispatch('cart/syncCartWhenLocalStorageChange', { items: storedItems })
rootStore.dispatch('cart/updateCart', { items: value })
} else if (key === 'shop/cart/current-totals') {
rootStore.dispatch('cart/updateTotals', value)
}
}

Expand Down
17 changes: 17 additions & 0 deletions core/modules/cart/helpers/totalsCacheHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as types from '../store/mutation-types'
import { Logger } from '@vue-storefront/core/lib/logger'

import { StorageManager } from '@vue-storefront/core/lib/storage-manager'

export const totalsCacheHandlerPlugin = ({ type }, state) => {
if (
type.endsWith(types.CART_UPD_TOTALS)
) {
return StorageManager.get('cart').setItem('current-totals', {
platformTotalSegments: state.cart.platformTotalSegments,
platformTotals: state.cart.platformTotals
}).catch((reason) => {
Logger.error(reason)()
})
}
}
6 changes: 3 additions & 3 deletions core/modules/cart/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { StorefrontModule } from '@vue-storefront/core/lib/modules'
import { cartStore } from './store'
import { cartCacheHandlerFactory } from './helpers';
import { cartCacheHandlerPlugin, totalsCacheHandlerPlugin } from './helpers';
import { isServer } from '@vue-storefront/core/helpers'
import Vue from 'vue'
import { StorageManager } from '@vue-storefront/core/lib/storage-manager'

export const CartModule: StorefrontModule = function ({ store }) {
Expand All @@ -11,5 +10,6 @@ export const CartModule: StorefrontModule = function ({ store }) {
store.registerModule('cart', cartStore)

if (!isServer) store.dispatch('cart/load')
store.subscribe(cartCacheHandlerFactory(Vue))
store.subscribe(cartCacheHandlerPlugin);
store.subscribe(totalsCacheHandlerPlugin);
}
2 changes: 1 addition & 1 deletion core/modules/cart/store/actions/synchronizeActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const synchronizeActions = {

cartHooksExecutors.afterLoad(storedItems)
},
syncCartWhenLocalStorageChange ({ commit }, { items }) {
updateCart ({ commit }, { items }) {
commit(types.CART_LOAD_CART, items)
},
async synchronizeCart ({ commit, dispatch }, { forceClientState }) {
Expand Down
3 changes: 3 additions & 0 deletions core/modules/cart/store/actions/totalsActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {
import EventBus from '@vue-storefront/core/compatibility/plugins/event-bus'

const totalsActions = {
async updateTotals ({ commit }, payload) {
commit(types.CART_UPD_TOTALS, payload)
},
async getTotals (context, { addressInformation, hasShippingInformation }) {
if (hasShippingInformation) {
return CartService.setShippingInfo(addressInformation)
Expand Down
14 changes: 7 additions & 7 deletions core/modules/cart/test/unit/helpers/cartCacheHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ const StorageManager = {
return this[key]
},
clear () {
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
resolve()
})
}
};
const cartCacheHandlerFactory = require('../../../helpers/cartCacheHandler').cartCacheHandlerFactory
const cartCacheHandlerPlugin = require('../../../helpers/cartCacheHandler').cartCacheHandlerPlugin

jest.mock('@vue-storefront/core/lib/storage-manager', () => ({ StorageManager }))
jest.mock('@vue-storefront/core/helpers', () => ({
Expand Down Expand Up @@ -54,7 +54,7 @@ describe('Cart afterRegistration', () => {

StorageManager.get('cart').setItem.mockImplementationOnce(() => Promise.resolve('foo'));

await cartCacheHandlerFactory(Vue)({ type: mutationType }, stateMock);
await cartCacheHandlerPlugin({ type: mutationType }, stateMock);

expect(StorageManager.get('cart').setItem)
.toBeCalledWith('current-cart', stateMock.cart.cartItems);
Expand All @@ -71,7 +71,7 @@ describe('Cart afterRegistration', () => {

StorageManager.get('cart').setItem.mockImplementationOnce(() => Promise.reject('foo'));

await cartCacheHandlerFactory(Vue)({ type: types.CART_LOAD_CART }, stateMock);
await cartCacheHandlerPlugin({ type: types.CART_LOAD_CART }, stateMock);

expect(consoleErrorSpy).toBeCalled();
});
Expand All @@ -85,7 +85,7 @@ describe('Cart afterRegistration', () => {

StorageManager.get('cart').setItem.mockImplementationOnce(() => Promise.resolve('foo'));

await cartCacheHandlerFactory(Vue)({ type: types.CART_LOAD_CART_SERVER_TOKEN }, stateMock);
await cartCacheHandlerPlugin({ type: types.CART_LOAD_CART_SERVER_TOKEN }, stateMock);

expect(StorageManager.get('cart').setItem)
.toBeCalledWith('current-cart-token', stateMock.cart.cartServerToken);
Expand All @@ -102,7 +102,7 @@ describe('Cart afterRegistration', () => {

StorageManager.get('cart').setItem.mockImplementationOnce(() => Promise.reject('foo'));

await cartCacheHandlerFactory(Vue)({ type: types.CART_LOAD_CART_SERVER_TOKEN }, stateMock);
await cartCacheHandlerPlugin({ type: types.CART_LOAD_CART_SERVER_TOKEN }, stateMock);

expect(consoleErrorSpy).toBeCalled();
});
Expand All @@ -118,7 +118,7 @@ describe('Cart afterRegistration', () => {

StorageManager.get('cart').setItem.mockImplementationOnce(() => Promise.reject('foo'));

await cartCacheHandlerFactory(Vue)({ type: 'bar' }, stateMock);
await cartCacheHandlerPlugin({ type: 'bar' }, stateMock);

expect(consoleErrorSpy).not.toBeCalled();
});
Expand Down
92 changes: 92 additions & 0 deletions core/modules/cart/test/unit/helpers/totalsCacheHandler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import Vue from 'vue'
import Vuex from 'vuex'

import * as types from '../../../store/mutation-types'
import { Logger } from '@vue-storefront/core/lib/logger'

const StorageManager = {
cart: {
setItem: jest.fn()
},
get (key) {
return this[key]
},
clear () {
return new Promise<void>((resolve, reject) => {
resolve()
})
}
};
const totalsCacheHandlerPlugin = require('../../../helpers/totalsCacheHandler').totalsCacheHandlerPlugin

jest.mock('@vue-storefront/core/lib/storage-manager', () => ({ StorageManager }))
jest.mock('@vue-storefront/core/helpers', () => ({
isServer: () => false
}));
jest.mock('@vue-storefront/core/app', () => ({ createApp: jest.fn() }))
jest.mock('@vue-storefront/i18n', () => ({ loadLanguageAsync: jest.fn() }))

jest.mock('@vue-storefront/core/lib/logger', () => ({
Logger: {
error: () => () => {}
}
}))

Vue.use(Vuex);

describe('Cart afterRegistration', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('handler populates cart cache on mutation CART_UPD_TOTALS that modifies totals', async () => {
const stateMock = {
cart: {
platformTotalSegments: 1,
platformTotals: 2
}
};

StorageManager.get('cart').setItem.mockImplementationOnce(() => Promise.resolve('foo'));

await totalsCacheHandlerPlugin({ type: types.CART_UPD_TOTALS }, stateMock);

expect(StorageManager.get('cart').setItem)
.toBeCalledWith('current-totals', {
platformTotalSegments: 1,
platformTotals: 2
});
});

it('handler logs error when populating cart cache with items fails', async () => {
const stateMock = {
cart: {
cartItems: [{}]
}
};

const consoleErrorSpy = jest.spyOn(Logger, 'error');

StorageManager.get('cart').setItem.mockImplementationOnce(() => Promise.reject('foo'));

await totalsCacheHandlerPlugin({ type: types.CART_UPD_TOTALS }, stateMock);

expect(consoleErrorSpy).toBeCalled();
});

it('nothing happens for mutation different than CART_UPD_TOTALS', async () => {
const stateMock = {
cart: {
cartItems: [{}]
}
};

const consoleErrorSpy = jest.spyOn(Logger, 'error');
const storageManagerSpy = jest.spyOn(StorageManager.get('cart'), 'setItem');

await totalsCacheHandlerPlugin({ type: 'abc' }, stateMock);

expect(consoleErrorSpy).not.toBeCalled();
expect(storageManagerSpy).not.toBeCalled();
});
});