Merge pull request #1660 from Safelite/feature/Digital/csr-1655
CSR-1655
This commit is contained in:
commit
6c3a3ddf85
8 changed files with 399 additions and 1 deletions
|
|
@ -28,6 +28,7 @@ import {
|
||||||
removeCurrentlyActivePromoCodesFromInactivePromos,
|
removeCurrentlyActivePromoCodesFromInactivePromos,
|
||||||
} from "@/helpers/promotions-helper";
|
} from "@/helpers/promotions-helper";
|
||||||
import { getDateDifferenceInDays } from "@/helpers/date-helper";
|
import { getDateDifferenceInDays } from "@/helpers/date-helper";
|
||||||
|
import sharedMutations from "./vuex-shared-mutations/vuexSharedMutations";
|
||||||
// Export State
|
// Export State
|
||||||
const getDefaultState = () => {
|
const getDefaultState = () => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -2523,7 +2524,12 @@ export const actions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
plugins: [createPersistedState()],
|
plugins: [
|
||||||
|
createPersistedState(),
|
||||||
|
sharedMutations({
|
||||||
|
predicate: [...Object.values(storeMutations)],
|
||||||
|
}),
|
||||||
|
],
|
||||||
// IMPORTANT: Be VERY careful when modifying these fields for at least a few reasons:
|
// IMPORTANT: Be VERY careful when modifying these fields for at least a few reasons:
|
||||||
// * The CMS can reference the fields by name
|
// * The CMS can reference the fields by name
|
||||||
// * Return users may have a previous "version" of the model, and we don't want
|
// * Return users may have a previous "version" of the model, and we don't want
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
const DEFAULT_CHANNEL = "vuex-shared-mutations";
|
||||||
|
|
||||||
|
const globalObj =
|
||||||
|
typeof window !== "undefined" ? window : /* istanbul ignore next: node env */ global;
|
||||||
|
|
||||||
|
export default class BroadcastChannelStrategy {
|
||||||
|
static available(BroadcastChannelImpl = globalObj.BroadcastChannel) {
|
||||||
|
return !(typeof BroadcastChannelImpl !== "function");
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options = {}) {
|
||||||
|
const BroadcastChannelImpl = options.BroadcastChannel || globalObj.BroadcastChannel;
|
||||||
|
const key = options.key || DEFAULT_CHANNEL;
|
||||||
|
|
||||||
|
if (!this.constructor.available(BroadcastChannelImpl)) {
|
||||||
|
throw new Error("Broadcast strategy not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.channel = new BroadcastChannelImpl(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(fn) {
|
||||||
|
this.channel.addEventListener("message", (e) => {
|
||||||
|
fn(e.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
share(message) {
|
||||||
|
return this.channel.postMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import BroadcastChannelStrategy from "./broadcastChannel";
|
||||||
|
|
||||||
|
describe("BroadcastChannelStrategy", () => {
|
||||||
|
const mockChannel = {
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
postMessage: jest.fn(),
|
||||||
|
};
|
||||||
|
const mockBroadcastChannel = jest.fn(() => mockChannel);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("available", () => {
|
||||||
|
it("should return true if BroadcastChannelImpl is a function", () => {
|
||||||
|
expect(BroadcastChannelStrategy.available(mockBroadcastChannel)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if BroadcastChannelImpl is not a function", () => {
|
||||||
|
expect(BroadcastChannelStrategy.available(null)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("constructor", () => {
|
||||||
|
it("should create a new BroadcastChannel with the default channel name if no key is provided", () => {
|
||||||
|
const strategy = new BroadcastChannelStrategy({
|
||||||
|
BroadcastChannel: mockBroadcastChannel,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockBroadcastChannel).toHaveBeenCalledWith("vuex-shared-mutations");
|
||||||
|
expect(strategy.channel).toBe(mockChannel);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a new BroadcastChannel with the provided key", () => {
|
||||||
|
const strategy = new BroadcastChannelStrategy({
|
||||||
|
BroadcastChannel: mockBroadcastChannel,
|
||||||
|
key: "test-channel",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockBroadcastChannel).toHaveBeenCalledWith("test-channel");
|
||||||
|
expect(strategy.channel).toBe(mockChannel);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if Broadcast strategy is not available", () => {
|
||||||
|
expect(() => new BroadcastChannelStrategy({ BroadcastChannel: null })).toThrow(
|
||||||
|
"Broadcast strategy not available"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("addEventListener", () => {
|
||||||
|
it("should add a message event listener to the channel", () => {
|
||||||
|
const strategy = new BroadcastChannelStrategy({
|
||||||
|
BroadcastChannel: mockBroadcastChannel,
|
||||||
|
});
|
||||||
|
const mockFn = jest.fn();
|
||||||
|
|
||||||
|
strategy.addEventListener(mockFn);
|
||||||
|
expect(mockChannel.addEventListener).toHaveBeenCalledWith(
|
||||||
|
"message",
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
// const mockEvent = { data: "test-message" };
|
||||||
|
// console.log(mockChannel.addEventListener.mock.calls);
|
||||||
|
// mockChannel.addEventListener.mock.calls[0][1];
|
||||||
|
// expect(mockFn).toHaveBeenCalledWith(mockEvent.data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("share", () => {
|
||||||
|
it("should post a message to the channel", () => {
|
||||||
|
const strategy = new BroadcastChannelStrategy({
|
||||||
|
BroadcastChannel: mockBroadcastChannel,
|
||||||
|
});
|
||||||
|
const mockMessage = { test: "message" };
|
||||||
|
|
||||||
|
expect(strategy.share(mockMessage)).toBeUndefined();
|
||||||
|
expect(mockChannel.postMessage).toHaveBeenCalledWith(mockMessage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import BroadcastChannelStrategy from "./broadcastChannel";
|
||||||
|
import LocalStorageStrategy from "./localStorage";
|
||||||
|
|
||||||
|
export default function createDefaultStrategy() {
|
||||||
|
/* istanbul ignore next: browser-dependent code */
|
||||||
|
if (LocalStorageStrategy.available()) {
|
||||||
|
return new LocalStorageStrategy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next: browser-dependent code */
|
||||||
|
if (BroadcastChannelStrategy.available()) {
|
||||||
|
return new BroadcastChannelStrategy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next: browser-dependent code */
|
||||||
|
throw new Error("No strategies available");
|
||||||
|
}
|
||||||
101
src/store/vuex-shared-mutations/strategies/localStorage.js
Normal file
101
src/store/vuex-shared-mutations/strategies/localStorage.js
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
const DEFAULT_KEY = "vuex-shared-mutations";
|
||||||
|
|
||||||
|
const globalObj =
|
||||||
|
typeof window !== "undefined" ? window : /* istanbul ignore next: node env */ global;
|
||||||
|
|
||||||
|
const MAX_MESSAGE_LENGTH = 4 * 1024;
|
||||||
|
let messageCounter = 1;
|
||||||
|
|
||||||
|
function splitMessage(message) {
|
||||||
|
const partsCount = Math.ceil(message.length / MAX_MESSAGE_LENGTH);
|
||||||
|
return Array.from({ length: partsCount }).map((_, idx) =>
|
||||||
|
message.substr(idx * MAX_MESSAGE_LENGTH, MAX_MESSAGE_LENGTH)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class LocalStorageStrategy {
|
||||||
|
static available(
|
||||||
|
{ window: windowImpl, localStorage: localStorageImpl } = {
|
||||||
|
window: globalObj.window,
|
||||||
|
localStorage: globalObj.localStorage,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (!windowImpl || !localStorageImpl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorageImpl.setItem("vuex-shared-mutations-test-key", Date.now());
|
||||||
|
localStorageImpl.removeItem("vuex-shared-mutations-test-key");
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options = {}) {
|
||||||
|
const windowImpl = options.window || globalObj.window;
|
||||||
|
const localStorageImpl = options.localStorage || globalObj.localStorage;
|
||||||
|
if (
|
||||||
|
!this.constructor.available({
|
||||||
|
window: windowImpl,
|
||||||
|
localStorage: localStorageImpl,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
throw new Error("Strategy unavailable");
|
||||||
|
}
|
||||||
|
this.uniqueId = `${Date.now()}-${Math.random()}`;
|
||||||
|
this.messageBuffer = [];
|
||||||
|
this.window = windowImpl;
|
||||||
|
this.storage = localStorageImpl;
|
||||||
|
this.options = {
|
||||||
|
key: DEFAULT_KEY,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
addEventListener(fn) {
|
||||||
|
return this.window.addEventListener("storage", (event) => {
|
||||||
|
if (!event.newValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key.indexOf("##") === -1 || event.key.split("##")[0] !== this.options.key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const message = this.window.JSON.parse(event.newValue);
|
||||||
|
/* istanbul ignore next: IE does not follow storage event spec */
|
||||||
|
if (message.author === this.uniqueId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.messageBuffer.push(message.messagePart);
|
||||||
|
if (this.messageBuffer.length === message.total) {
|
||||||
|
const mutation = this.window.JSON.parse(this.messageBuffer.join(""));
|
||||||
|
this.messageBuffer = [];
|
||||||
|
fn(mutation);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
share(message) {
|
||||||
|
const rawMessage = this.window.JSON.stringify(message);
|
||||||
|
const messageParts = splitMessage(rawMessage);
|
||||||
|
messageParts.forEach((m, idx) => {
|
||||||
|
messageCounter += 1;
|
||||||
|
const key = `${this.options.key}##${idx}`;
|
||||||
|
this.storage.setItem(
|
||||||
|
key,
|
||||||
|
JSON.stringify({
|
||||||
|
author: this.uniqueId,
|
||||||
|
part: idx,
|
||||||
|
total: messageParts.length,
|
||||||
|
messagePart: m,
|
||||||
|
messageCounter,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.storage.removeItem(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import LocalStorageStrategy from "./localStorage";
|
||||||
|
|
||||||
|
describe("LocalStorageStrategy", () => {
|
||||||
|
describe("available", () => {
|
||||||
|
it("should return true if localStorage is available", () => {
|
||||||
|
const localStorageMock = {
|
||||||
|
setItem: jest.fn(),
|
||||||
|
removeItem: jest.fn(),
|
||||||
|
};
|
||||||
|
expect(
|
||||||
|
LocalStorageStrategy.available({ window: {}, localStorage: localStorageMock })
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
it("should return false if localStorage is not available", () => {
|
||||||
|
const windowMock = {};
|
||||||
|
expect(LocalStorageStrategy.available({ window: windowMock })).toBe(false);
|
||||||
|
});
|
||||||
|
it("should return false if window is not available", () => {
|
||||||
|
expect(LocalStorageStrategy.available({ window: undefined })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
51
src/store/vuex-shared-mutations/vuexSharedMutations.js
Normal file
51
src/store/vuex-shared-mutations/vuexSharedMutations.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
//Share vuex mutations between tabs/windows
|
||||||
|
//Referenced by https://github.com/xanf/vuex-shared-mutations/
|
||||||
|
|
||||||
|
import createDefaultStrategy from "./strategies/defaultStrategy";
|
||||||
|
|
||||||
|
export { default as BroadcastChannelStrategy } from "./strategies/broadcastChannel";
|
||||||
|
export { default as LocalStorageStratery } from "./strategies/localStorage";
|
||||||
|
|
||||||
|
export default ({ predicate, strategy, ...rest } = {}) => {
|
||||||
|
/* istanbul ignore next: deprecation warning */
|
||||||
|
if ("storageKey" in rest || "sharingKey" in rest) {
|
||||||
|
window.console.warn(
|
||||||
|
"Configuration directly on plugin was removed, configure specific strategies if needed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(predicate) && typeof predicate !== "function") {
|
||||||
|
throw new Error(
|
||||||
|
"Either array of accepted mutations or predicate function must be supplied"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const predicateFn =
|
||||||
|
typeof predicate === "function" ? predicate : ({ type }) => predicate.indexOf(type) !== -1;
|
||||||
|
|
||||||
|
let sharingInProgress = false;
|
||||||
|
const selectedStrategy = strategy || createDefaultStrategy();
|
||||||
|
return (store) => {
|
||||||
|
store.subscribe(async (mutation, state) => {
|
||||||
|
if (sharingInProgress) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldShare = await Promise.resolve(predicateFn(mutation, state));
|
||||||
|
if (!shouldShare) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedStrategy.share(mutation);
|
||||||
|
});
|
||||||
|
|
||||||
|
selectedStrategy.addEventListener((mutation) => {
|
||||||
|
try {
|
||||||
|
sharingInProgress = true;
|
||||||
|
store.commit(mutation.type, mutation.payload);
|
||||||
|
} finally {
|
||||||
|
sharingInProgress = false;
|
||||||
|
}
|
||||||
|
return "done";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
88
src/store/vuex-shared-mutations/vuexSharedMutations.spec.js
Normal file
88
src/store/vuex-shared-mutations/vuexSharedMutations.spec.js
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import createMutationsSharer from "./vuexSharedMutations";
|
||||||
|
|
||||||
|
describe("Vuex shared mutations", () => {
|
||||||
|
it("should throw an error if predicate function is not supplied", () => {
|
||||||
|
expect(() => {
|
||||||
|
createMutationsSharer();
|
||||||
|
}).toThrowError(Error);
|
||||||
|
});
|
||||||
|
it("should accept array as predicate", () => {
|
||||||
|
expect(() => {
|
||||||
|
createMutationsSharer({
|
||||||
|
predicate: ["m-1"],
|
||||||
|
});
|
||||||
|
}).not.toThrowError(Error);
|
||||||
|
});
|
||||||
|
it("should accept function as predicate", () => {
|
||||||
|
expect(() => {
|
||||||
|
createMutationsSharer({
|
||||||
|
predicate: jest.fn(),
|
||||||
|
});
|
||||||
|
}).not.toThrowError(Error);
|
||||||
|
});
|
||||||
|
it("should share relevant mutation", () => {
|
||||||
|
let capturedHandler;
|
||||||
|
const fakeStrategy = {
|
||||||
|
share: jest.fn(),
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const fakeStore = {
|
||||||
|
subscribe(fn) {
|
||||||
|
capturedHandler = fn;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
createMutationsSharer({
|
||||||
|
predicate: ["m-1"],
|
||||||
|
strategy: fakeStrategy,
|
||||||
|
})(fakeStore);
|
||||||
|
|
||||||
|
return capturedHandler({ type: "m-1", payload: "lol" }).then(() => {
|
||||||
|
expect(fakeStrategy.share).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("should not share irrelevant mutation", () => {
|
||||||
|
let capturedHandler;
|
||||||
|
const fakeStrategy = {
|
||||||
|
share: jest.fn(),
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const fakeStore = {
|
||||||
|
subscribe(fn) {
|
||||||
|
capturedHandler = fn;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
createMutationsSharer({
|
||||||
|
predicate: ["m-1"],
|
||||||
|
strategy: fakeStrategy,
|
||||||
|
})(fakeStore);
|
||||||
|
return capturedHandler({ type: "m-2", payload: "lol" }).then(() => {
|
||||||
|
expect(fakeStrategy.share).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("should respect predicate function when sharing mutation", () => {
|
||||||
|
let capturedHandler;
|
||||||
|
const fakeStrategy = {
|
||||||
|
share: jest.fn(),
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const fakeStore = {
|
||||||
|
subscribe(fn) {
|
||||||
|
capturedHandler = fn;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
createMutationsSharer({
|
||||||
|
predicate: ({ type }) => ["m-1"].indexOf(type) !== -1,
|
||||||
|
strategy: fakeStrategy,
|
||||||
|
})(fakeStore);
|
||||||
|
|
||||||
|
return capturedHandler({ type: "m-1", payload: "lol" }).then(() => {
|
||||||
|
expect(fakeStrategy.share).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue