Skip to content
This repository has been archived by the owner on Oct 31, 2024. It is now read-only.

Commit

Permalink
feat: currency switch #580 (#598)
Browse files Browse the repository at this point in the history
* feat: currency switcher

* test: useCurrency

* feat(nuxt-module): price filter

* chore: fix build

* fix: problem with resolving url params for search

* feat: change route with currency

Co-authored-by: patzick <tomczyk.patryk@gmail.com>
  • Loading branch information
mkucmus and patzick authored Apr 22, 2020
1 parent d5d8c7d commit e34a9c1
Show file tree
Hide file tree
Showing 35 changed files with 5,253 additions and 297 deletions.
26 changes: 25 additions & 1 deletion api/composables.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { AddressType } from '@shopware-pwa/commons/interfaces/models/checkout/customer/CustomerAddress';
import { BillingAddress } from '@shopware-pwa/commons/interfaces/request/GuestOrderParams';
import { Country } from '@shopware-pwa/commons/interfaces/models/system/country/Country';
import { Currency } from '@shopware-pwa/commons/interfaces/models/system/currency/Currency';
import { Customer } from '@shopware-pwa/commons/interfaces/models/checkout/customer/Customer';
import { CustomerAddress } from '@shopware-pwa/commons/interfaces/models/checkout/customer/CustomerAddress';
import { CustomerAddressParam } from '@shopware-pwa/shopware-6-client';
Expand Down Expand Up @@ -133,6 +134,25 @@ export interface UseCountries {
// @alpha (undocumented)
export const useCountries: () => UseCountries;

// @alpha (undocumented)
export interface UseCurrency {
// (undocumented)
availableCurrencies: Readonly<Ref<readonly Currency[]>>;
// (undocumented)
currency: Readonly<Ref<Currency | null>>;
// (undocumented)
currencySymbol: Ref<Readonly<string>>;
// (undocumented)
loadAvailableCurrencies: (options?: {
forceReload: boolean;
}) => Promise<void>;
// (undocumented)
setCurrency: (parameter: Partial<Currency>) => Promise<void>;
}

// @alpha (undocumented)
export const useCurrency: () => UseCurrency;

// @alpha (undocumented)
export const useNavigation: () => any;

Expand Down Expand Up @@ -196,14 +216,18 @@ export const useSalutations: () => UseSalutations;

// @alpha
export interface UseSessionContext {
// (undocumented)
currency: Readonly<Ref<Currency | null>>;
// (undocumented)
refreshSessionContext: () => Promise<void>;
// (undocumented)
sessionContext: Readonly<Ref<SessionContext | null>>;
// (undocumented)
setCurrency: (currency: Partial<Currency>) => Promise<void>;
// (undocumented)
setShippingMethod: (shippingMethod: Partial<ShippingMethod>) => Promise<void>;
// (undocumented)
shippingMethod: Readonly<Ref<ShippingMethod>>;
shippingMethod: Readonly<Ref<ShippingMethod | null>>;
}

// @alpha (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CustomField } from "../../common/CustomField";
* @alpha
*/
export interface Currency {
id: string;
isoCode: string;
factor: number;
symbol: string;
Expand Down
6 changes: 2 additions & 4 deletions packages/commons/interfaces/response/SessionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PaymentMethod } from "../models/checkout/payment/PaymentMethod";
import { ShippingMethod } from "../models/checkout/shipping/ShippingMethod";
import { Country } from "../models/system/country/Country";
import { User } from "../models/system/user/User";
import { Currency } from "../models/system/currency/Currency";
import { ShippingAddress } from "../request/GuestOrderParams";

export interface ContextTokenResponse {
Expand All @@ -18,10 +19,7 @@ export interface SessionContext {
id: string;
name: string;
};
currency: {
id: string;
name: string;
};
currency: Currency;
salesChannel: {
id: string;
name: string;
Expand Down
182 changes: 182 additions & 0 deletions packages/composables/__tests__/useCurrency.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import Vue from "vue";
import VueCompostionApi from "@vue/composition-api";
import { Ref, ref, reactive, computed } from "@vue/composition-api";
import { SessionContext } from "@shopware-pwa/commons/interfaces/response/SessionContext";
import * as Composables from "@shopware-pwa/composables";
import * as shopwareClient from "@shopware-pwa/shopware-6-client";
import { useCurrency, setStore } from "@shopware-pwa/composables";
import { Currency } from "@shopware-pwa/commons/interfaces/models/system/currency/Currency";

Vue.use(VueCompostionApi);

jest.mock("@shopware-pwa/shopware-6-client");
const mockedApiClient = shopwareClient as jest.Mocked<typeof shopwareClient>;
const consoleErrorSpy = jest.spyOn(console, "error");
consoleErrorSpy.mockImplementation(() => {});

describe("Composables - useCurrency", () => {
const stateContext: Ref<Partial<SessionContext> | null> = ref(null);
const mockedCurrentCurrency: Ref<Currency | null> = ref(null);
const refreshSessionContextMock = jest.fn(async () => {});
const setCurrencyContextMock = jest.fn(async () => {});
const refreshCartMock = jest.fn(async () => {});
beforeEach(async () => {
jest.resetAllMocks();
stateContext.value = null;
mockedCurrentCurrency.value = null;
setStore({
getters: reactive({
getSessionContext: computed(() => stateContext.value),
}),
commit: (name: string, value: SessionContext) => {
stateContext.value = value;
},
});
jest.spyOn(Composables, "useSessionContext").mockImplementation(() => {
return {
refreshSessionContext: refreshSessionContextMock,
setCurrency: setCurrencyContextMock,
currency: mockedCurrentCurrency,
} as any;
});
jest.spyOn(Composables, "useCart").mockImplementation(() => {
return {
refreshCart: refreshCartMock,
} as any;
});
mockedApiClient.getAvailableCurrencies.mockResolvedValue([] as any);
});

afterEach(async () => {
// clear shared available currencies array
const { loadAvailableCurrencies } = useCurrency();
await loadAvailableCurrencies({ forceReload: true });
});

describe("computed", () => {
describe("currency", () => {
it("should return currency from useSessionContext", async () => {
mockedCurrentCurrency.value = { symbol: "$$$" } as any;
const { currency } = useCurrency();
expect(currency.value).toEqual({ symbol: "$$$" });
});
});
describe("currencySymbol", () => {
it("should return an empty string if currency symbol object is missing", async () => {
mockedCurrentCurrency.value = {} as any;
const { currencySymbol } = useCurrency();
expect(currencySymbol.value).toBe("");
});
it("should return an empty string if currency is null", async () => {
mockedCurrentCurrency.value = null;
const { currencySymbol } = useCurrency();
expect(currencySymbol.value).toBe("");
});
it("should return a symbol of current currency", async () => {
mockedCurrentCurrency.value = { symbol: "$" } as any;
const { currencySymbol } = useCurrency();
expect(currencySymbol.value).toEqual("$");
});
});
describe("availableCurrencies", () => {
it("should return empty array if there are no currencies loaded", async () => {
const { availableCurrencies } = useCurrency();
expect(availableCurrencies.value).toStrictEqual([]);
});

it("should return fetched array of currencies", async () => {
mockedApiClient.getAvailableCurrencies.mockResolvedValueOnce([
{
iso: "EUR",
},
] as any);

const { loadAvailableCurrencies, availableCurrencies } = useCurrency();
await loadAvailableCurrencies();
expect(availableCurrencies.value).toEqual([
{
iso: "EUR",
},
]);
});

it("should return array with current currenry inside, when no currencies loaded", async () => {
mockedCurrentCurrency.value = { symbol: "$$$" } as any;
const { availableCurrencies } = useCurrency();
expect(availableCurrencies.value).toEqual([{ symbol: "$$$" }]);
});
});
});
describe("methods", () => {
describe("loadAvailableCurrencies", () => {
it("should call apiClient:getAvailableCurrencies to fetch and set available currencies ", async () => {
const { loadAvailableCurrencies } = useCurrency();
await loadAvailableCurrencies();
expect(mockedApiClient.getAvailableCurrencies).toBeCalledTimes(1);
});

it("should not call apiClient:getAvailableCurrencies second time if values are fetched", async () => {
mockedApiClient.getAvailableCurrencies.mockResolvedValueOnce([
{
iso: "EUR",
},
] as any);

const { loadAvailableCurrencies } = useCurrency();
await loadAvailableCurrencies();
await loadAvailableCurrencies();
expect(mockedApiClient.getAvailableCurrencies).toBeCalledTimes(1);
});

it("should call apiClient:getAvailableCurrencies second if forceReload flag is used", async () => {
mockedApiClient.getAvailableCurrencies.mockResolvedValueOnce([
{
iso: "EUR",
},
] as any);

const { loadAvailableCurrencies } = useCurrency();
await loadAvailableCurrencies();
await loadAvailableCurrencies({ forceReload: true });
expect(mockedApiClient.getAvailableCurrencies).toBeCalledTimes(2);
});
});

describe("setCurrency", () => {
it("should call setCurrency from useSessionContext", async () => {
const { setCurrency } = useCurrency();
await setCurrency({ id: "some-currency-id" });
expect(setCurrencyContextMock).toBeCalledWith({
id: "some-currency-id",
});
});

it("should call refreshSessionContext from useSessionContext", async () => {
const { setCurrency } = useCurrency();
await setCurrency({ id: "some-currency-id" });
expect(refreshSessionContextMock).toBeCalled();
});

it("should call refreshCart from useCart", async () => {
const { setCurrency } = useCurrency();
await setCurrency({ id: "some-currency-id" });
expect(refreshCartMock).toBeCalled();
});

it("should sidplay error when one of method throws an error", async () => {
setCurrencyContextMock.mockRejectedValueOnce({
message: "Some error",
} as any);
const { setCurrency } = useCurrency();
await setCurrency({ id: "some-currency-id" });
expect(setCurrencyContextMock).toBeCalled();
expect(consoleErrorSpy).toBeCalledWith(
"[useCurrency][setCurrency] Problem with currency change",
{
message: "Some error",
}
);
});
});
});
});
98 changes: 74 additions & 24 deletions packages/composables/__tests__/useSessionContext.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,35 +61,85 @@ describe("Composables - useSessionContext", () => {
expect(shippingMethod.value).toEqual({ id: "qwe" });
});
});

describe("currency", () => {
it("should return null when session context value is null", () => {
stateContext.value = null;
const { currency } = useSessionContext();
expect(currency.value).toBeNull();
});

it("should return null when session context doesn't have currency property", () => {
stateContext.value = {};
const { currency } = useSessionContext();
expect(currency.value).toBeNull();
});
it("should return currency from context", () => {
stateContext.value = { currency: { sign: "$$" } } as any;
const { currency } = useSessionContext();
expect(currency.value).toEqual({ sign: "$$" });
});
});
});

describe("methods", () => {
describe("refreshSessionContext", () => {
it("should get context from api", async () => {
mockedApiClient.getSessionContext.mockResolvedValueOnce({
token: "qwe",
} as any);
const { sessionContext, refreshSessionContext } = useSessionContext();
await refreshSessionContext();
expect(sessionContext.value).toEqual({ token: "qwe" });
describe("setCurrency", () => {
it("should not call API client setCurrentCurrency with not argument provided", async () => {
const { setCurrency } = useSessionContext();
try {
await setCurrency(undefined as any);
} catch (e) {
expect(e.message).toBe(
"You need to provide currency id in order to set currency."
);
}
expect(mockedApiClient.setCurrentCurrency).toBeCalledTimes(0);
});
it("should not call API client setCurrentCurrency ", async () => {
const { setCurrency } = useSessionContext();
try {
await setCurrency({ id: null } as any);
} catch (e) {
expect(e.message).toBe(
"You need to provide currency id in order to set currency."
);
}
expect(mockedApiClient.setCurrentCurrency).toBeCalledTimes(0);
});
it("should call API client setCurrentCurrency ", async () => {
const { setCurrency } = useSessionContext();
await setCurrency({ id: "euro-id" } as any);

it("should not set context on api rejection and show console error", async () => {
mockedApiClient.getSessionContext.mockRejectedValueOnce({
message: "Some error",
} as any);
const { sessionContext, refreshSessionContext } = useSessionContext();
await refreshSessionContext();
expect(sessionContext.value).toBeNull();
expect(stateContext.value).toBeNull();
expect(consoleErrorSpy).toBeCalledWith(
"[UseSessionContext][refreshSessionContext]",
{
expect(mockedApiClient.setCurrentCurrency).toBeCalledTimes(1);
expect(mockedApiClient.setCurrentCurrency).toBeCalledWith("euro-id");
});
}),
describe("refreshSessionContext", () => {
it("should get context from api", async () => {
mockedApiClient.getSessionContext.mockResolvedValueOnce({
token: "qwe",
} as any);
const { sessionContext, refreshSessionContext } = useSessionContext();
await refreshSessionContext();
expect(sessionContext.value).toEqual({ token: "qwe" });
});

it("should not set context on api rejection and show console error", async () => {
mockedApiClient.getSessionContext.mockRejectedValueOnce({
message: "Some error",
}
);
} as any);
const { sessionContext, refreshSessionContext } = useSessionContext();
await refreshSessionContext();
expect(sessionContext.value).toBeNull();
expect(stateContext.value).toBeNull();
expect(consoleErrorSpy).toBeCalledWith(
"[UseSessionContext][refreshSessionContext]",
{
message: "Some error",
}
);
});
});
});

describe("setShippingMethod", () => {
it("should set shipping method", async () => {
Expand Down Expand Up @@ -118,14 +168,14 @@ describe("Composables - useSessionContext", () => {
it("should throw an error if shipping method is not provided", async () => {
const { setShippingMethod } = useSessionContext();
await expect(setShippingMethod(undefined as any)).rejects.toThrowError(
"You need to provige shipping method id in order to set shipping method."
"You need to provide shipping method id in order to set shipping method."
);
});

it("should throw an error if shipping method is empty reference", async () => {
const { setShippingMethod } = useSessionContext();
await expect(setShippingMethod(null as any)).rejects.toThrowError(
"You need to provige shipping method id in order to set shipping method."
"You need to provide shipping method id in order to set shipping method."
);
});
});
Expand Down
Loading

0 comments on commit e34a9c1

Please sign in to comment.