Merge pull request #1660 from Safelite/feature/Digital/csr-1655

CSR-1655
This commit is contained in:
hiteshkumar87 2023-12-22 18:41:55 +05:30 committed by GitHub
commit 6c3a3ddf85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 399 additions and 1 deletions

View file

@ -28,6 +28,7 @@ import {
removeCurrentlyActivePromoCodesFromInactivePromos,
} from "@/helpers/promotions-helper";
import { getDateDifferenceInDays } from "@/helpers/date-helper";
import sharedMutations from "./vuex-shared-mutations/vuexSharedMutations";
// Export State
const getDefaultState = () => {
return {
@ -2523,7 +2524,12 @@ export const actions = {
};
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:
// * The CMS can reference the fields by name
// * Return users may have a previous "version" of the model, and we don't want

View file

@ -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);
}
}

View file

@ -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);
});
});
});

View file

@ -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");
}

View 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);
});
}
}

View file

@ -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);
});
});
});

View 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";
});
};
};

View 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();
});
});
});