From 55a82be4a9f2230df3e27774d022015dd36c3e8a Mon Sep 17 00:00:00 2001 From: Ivan Kalachikov Date: Thu, 12 Dec 2024 17:26:56 +0100 Subject: [PATCH 01/10] fix: vc-add-to-cart unit tests (#1493) --- client-app/core/utilities/tests/index.ts | 63 ++++++ .../shared/cart/components/add-to-cart.vue | 1 + .../add-to-cart/vc-add-to-cart.test.ts | 212 ++++++++++++++++++ .../organisms/add-to-cart/vc-add-to-cart.vue | 3 +- 4 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 client-app/core/utilities/tests/index.ts create mode 100644 client-app/ui-kit/components/organisms/add-to-cart/vc-add-to-cart.test.ts diff --git a/client-app/core/utilities/tests/index.ts b/client-app/core/utilities/tests/index.ts new file mode 100644 index 000000000..1fb70021e --- /dev/null +++ b/client-app/core/utilities/tests/index.ts @@ -0,0 +1,63 @@ +import merge from "lodash/merge"; +import { createI18n } from "vue-i18n"; +import type { Component } from "vue"; + +const i18n = createI18n({ + locale: "en", + legacy: false, + messages: {}, + missingWarn: false, +}); + +const defaults = { + global: { + mocks: { + $t: (key: string, ...args: unknown[]) => `${key} ${args.join(", ")}`, + $n: (count: number) => count, + $d: (date: unknown) => date, + $route: { + path: "/", + name: "home", + params: {}, + query: {}, + }, + $router: { + push: () => {}, + replace: () => {}, + }, + }, + stubs: { + "router-link": true, + "router-view": true, + transition: true, + "transition-group": true, + }, + directives: { + "html-safe": true, + }, + plugins: [i18n], + renderStubDefaultSlot: true, + }, +}; + +export function createWrapperFactory( + mount: typeof import("@vue/test-utils").mount, + component: T, + globalOverrides: Parameters[1] = {}, +) { + return (overrides: Parameters>[1] = {}) => + mount(component, { + ...merge({}, defaults, globalOverrides, overrides), + }); +} + +export function createShallowWrapperFactory( + shallowMount: typeof import("@vue/test-utils").shallowMount, + component: T, + globalOverrides: Parameters[1] = {}, +) { + return (overrides: Parameters>[1] = {}) => + shallowMount(component, { + ...merge({}, defaults, globalOverrides, overrides), + }); +} diff --git a/client-app/shared/cart/components/add-to-cart.vue b/client-app/shared/cart/components/add-to-cart.vue index 8d95aa96f..4a3dbb78e 100644 --- a/client-app/shared/cart/components/add-to-cart.vue +++ b/client-app/shared/cart/components/add-to-cart.vue @@ -6,6 +6,7 @@ :min-quantity="product.minQuantity" :max-quantity="maxQty" :pack-size="product.packSize" + :is-active="product.availabilityData?.isActive" :is-available="product.availabilityData?.isAvailable" :is-buyable="product.availabilityData?.isBuyable" :is-in-stock="product.availabilityData?.isInStock" diff --git a/client-app/ui-kit/components/organisms/add-to-cart/vc-add-to-cart.test.ts b/client-app/ui-kit/components/organisms/add-to-cart/vc-add-to-cart.test.ts new file mode 100644 index 000000000..ca0023176 --- /dev/null +++ b/client-app/ui-kit/components/organisms/add-to-cart/vc-add-to-cart.test.ts @@ -0,0 +1,212 @@ +import { mount } from "@vue/test-utils"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { nextTick } from "vue"; +import { LINE_ITEM_QUANTITY_LIMIT } from "@/core/constants"; +import { createWrapperFactory } from "@/core/utilities/tests"; +import { VcInputDetails, VcPopover, VcTooltip } from "@/ui-kit/components/atoms"; +import { VcButton, VcInput } from "@/ui-kit/components/molecules"; +import { VcAddToCart } from "@/ui-kit/components/organisms"; + +const BUTTONS_SELECTOR = ".vc-add-to-cart__icon-button, .vc-add-to-cart__text-button"; + +const createWrapper = createWrapperFactory(mount, VcAddToCart, { + global: { + components: { + VcInput: VcInput, + VcInputDetails: VcInputDetails, + VcButton: VcButton, + VcTooltip: VcTooltip, + VcPopover: VcPopover, + }, + stubs: { + VcLabel: true, + VcIcon: true, + }, + }, +}); + +describe("AddToCart", () => { + beforeAll(() => { + vi.useFakeTimers(); + }); + + afterAll(() => { + vi.restoreAllMocks(); + }); + + describe("render", () => { + it("renders with default add to cart text", () => { + const wrapper = createWrapper({ + props: { modelValue: 1 }, + }); + expect(wrapper.html()).toContain("ui_kit.buttons.add_to_cart"); + }); + + it("displays the quantity input with initial modelValue", () => { + const wrapper = createWrapper({ + props: { modelValue: 3 }, + }); + const input = wrapper.find("input[type='number']"); + expect(input.exists()).toBe(true); + expect((input.element as HTMLInputElement).value).toBe("3"); + }); + + it("displays updated quantity when modelValue changes", async () => { + const wrapper = createWrapper({ + props: { modelValue: 2 }, + }); + const input = wrapper.find("input[type='number']"); + await wrapper.setProps({ modelValue: 5 }); + expect((input.element as HTMLInputElement).value).toBe("5"); + }); + + it("does not display button if hideButton is true", () => { + const wrapper = createWrapper({ + props: { modelValue: 2, hideButton: true }, + }); + expect(wrapper.find(BUTTONS_SELECTOR).exists()).toBe(false); + }); + + it("displays message if provided via props", async () => { + const wrapper = createWrapper({ props: { modelValue: 1, message: "Stock limited" } }); + await nextTick(); + expect(wrapper.html()).toContain("Stock limited"); + }); + }); + + describe("on input", () => { + it("emits 'update:modelValue' when quantity input changes with valid data", async () => { + const wrapper = createWrapper({ + props: { modelValue: 1 }, + }); + const input = wrapper.find("input[type='number']"); + await input.setValue("4"); + await vi.advanceTimersToNextTimerAsync(); + const emitted = wrapper.emitted("update:modelValue"); + expect(emitted).toBeTruthy(); + expect(emitted?.[0]).toEqual([4]); + }); + + it("resets quantity input to modelValue if invalid value is provided and loses focus", async () => { + const wrapper = createWrapper({ props: { modelValue: 2 } }); + const input = wrapper.find("input[type='number']"); + await input.setValue("0"); + await input.trigger("blur"); + expect((input.element as HTMLInputElement).value).toBe("2"); + }); + + it("does not allow quantity beyond LINE_ITEM_QUANTITY_LIMIT and truncates input", async () => { + const wrapper = createWrapper({ props: { modelValue: 1 } }); + const input = wrapper.find("input[type='number']"); + const largeNumber = (LINE_ITEM_QUANTITY_LIMIT * 10).toString(); + await input.setValue(largeNumber); + await vi.advanceTimersToNextTimerAsync(); + const trimmedValue = (input.element as HTMLInputElement).value; + expect(Number(trimmedValue)).toBe(LINE_ITEM_QUANTITY_LIMIT); + }); + + it("does not emit 'update:modelValue' if new quantity is the same as the old one", async () => { + const wrapper = createWrapper({ props: { modelValue: 5 } }); + const input = wrapper.find("input[type='number']"); + await input.setValue("5"); + await vi.advanceTimersToNextTimerAsync(); + expect(wrapper.emitted("update:modelValue")).toBeFalsy(); + }); + }); + + describe("on button click", () => { + it("emits 'update:cartItemQuantity' event with current quantity when button is clicked", async () => { + const wrapper = createWrapper({ + props: { modelValue: 2, isActive: true, isAvailable: true, isBuyable: true, isInStock: true }, + }); + const button = wrapper.find(BUTTONS_SELECTOR); + await button.trigger("click"); + const emitted = wrapper.emitted("update:cartItemQuantity"); + expect(emitted).toBeTruthy(); + expect(emitted?.[0]).toEqual([2]); + }); + + it("does not emit 'update:cartItemQuantity event on button click if the button is disabled", async () => { + const wrapper = createWrapper({ + props: { modelValue: 2, disabled: true }, + }); + const button = wrapper.find(BUTTONS_SELECTOR); + await button.trigger("click"); + expect(wrapper.emitted("update:cartItemQuantity")).toBeFalsy(); + }); + }); + + describe("validation", () => { + it("emits 'update:validation' event with true if validation is successful", async () => { + const wrapper = createWrapper({ props: { isInStock: true, modelValue: 1 } }); + await wrapper.find("input[type='number']").setValue("2"); + await vi.advanceTimersToNextTimerAsync(); + expect(wrapper.emitted("update:validation")).toBeTruthy(); + expect(wrapper.emitted("update:validation")?.[0]).toEqual([{ isValid: true }]); + }); + + it("emits 'update:validation' event with false if validation fails", async () => { + const wrapper = createWrapper({ props: { isInStock: true, modelValue: 1, maxQuantity: 9 } }); + await wrapper.find("input[type='number']").setValue("10"); + await vi.advanceTimersToNextTimerAsync(); + expect(wrapper.emitted("update:validation")).toBeTruthy(); + expect(wrapper.emitted("update:validation")?.[1]).toEqual([ + { errorMessage: "ui_kit.add_to_cart.errors.max", isValid: false }, + ]); + }); + }); + + describe("disabled", () => { + it("disables button if disabled prop is set", () => { + const wrapper = createWrapper({ + props: { modelValue: 1, disabled: true }, + }); + const button = wrapper.find("button"); + expect(button.attributes("disabled")).toBeDefined(); + }); + + it("disables button if isActive is false", () => { + const wrapper = createWrapper({ + props: { + modelValue: 1, + isActive: false, + isAvailable: true, + isBuyable: true, + isInStock: true, + }, + }); + const button = wrapper.find("button"); + expect(button.attributes("disabled")).toBeDefined(); + }); + + it("disables button if isAvailable is false", () => { + const wrapper = createWrapper({ + props: { + modelValue: 1, + isActive: true, + isAvailable: false, + isBuyable: true, + isInStock: true, + }, + }); + const button = wrapper.find("button"); + expect(button.attributes("disabled")).toBeDefined(); + }); + + it("disables button if isBuyable is false", () => { + const wrapper = createWrapper({ + props: { modelValue: 1, isActive: true, isAvailable: true, isBuyable: false, isInStock: true }, + }); + const button = wrapper.find("button"); + expect(button.attributes("disabled")).toBeDefined(); + }); + + it("disables button if isInStock is false", () => { + const wrapper = createWrapper({ + props: { modelValue: 1, isActive: true, isAvailable: true, isBuyable: true, isInStock: false }, + }); + const button = wrapper.find("button"); + expect(button.attributes("disabled")).toBeDefined(); + }); + }); +}); diff --git a/client-app/ui-kit/components/organisms/add-to-cart/vc-add-to-cart.vue b/client-app/ui-kit/components/organisms/add-to-cart/vc-add-to-cart.vue index b648a2e11..e827d4813 100644 --- a/client-app/ui-kit/components/organisms/add-to-cart/vc-add-to-cart.vue +++ b/client-app/ui-kit/components/organisms/add-to-cart/vc-add-to-cart.vue @@ -141,7 +141,8 @@ const { quantitySchema } = useQuantityValidationSchema({ const rules = computed(() => toTypedSchema(quantitySchema.value)); const isDisabled = computed( - () => !isValid.value || disabled.value || !isActive || !isAvailable.value || !isBuyable.value || !isInStock.value, + () => + !isValid.value || disabled.value || !isActive.value || !isAvailable.value || !isBuyable.value || !isInStock.value, ); const { From b5f49c15d71168268904c229e800c5fec97e62ac Mon Sep 17 00:00:00 2001 From: Maiia Diachkovskaia Date: Fri, 13 Dec 2024 13:30:26 +0900 Subject: [PATCH 02/10] feat: implement new footer design (#1494) ## Description ## References ### Jira-link: https://virtocommerce.atlassian.net/browse/VCST-1937 ### Artifact URL: https://vc3prerelease.blob.core.windows.net/packages/vc-theme-b2b-vue-2.12.0-pr-1494-0abd-0abd0928.zip --- client-app/pages/cart.vue | 2 +- .../footer/_internal/footer-link.vue | 2 +- .../footer/_internal/footer-links.vue | 80 +++++++++++++++---- .../layout/components/footer/vc-footer.vue | 14 ++-- .../components/main-layout/main-layout.vue | 2 +- .../molecules/widget/vc-widget.stories.ts | 7 +- .../components/molecules/widget/vc-widget.vue | 19 ++++- 7 files changed, 95 insertions(+), 31 deletions(-) diff --git a/client-app/pages/cart.vue b/client-app/pages/cart.vue index e38766e2b..716f13e53 100644 --- a/client-app/pages/cart.vue +++ b/client-app/pages/cart.vue @@ -129,7 +129,7 @@
{{ $t("common.labels.total") }}: diff --git a/client-app/shared/layout/components/footer/_internal/footer-link.vue b/client-app/shared/layout/components/footer/_internal/footer-link.vue index 412e46f81..4c1e7c912 100644 --- a/client-app/shared/layout/components/footer/_internal/footer-link.vue +++ b/client-app/shared/layout/components/footer/_internal/footer-link.vue @@ -29,7 +29,7 @@ const isExternalLink = computed(() => { diff --git a/client-app/shared/layout/components/footer/vc-footer.vue b/client-app/shared/layout/components/footer/vc-footer.vue index 004342574..22cfee0dd 100644 --- a/client-app/shared/layout/components/footer/vc-footer.vue +++ b/client-app/shared/layout/components/footer/vc-footer.vue @@ -3,14 +3,14 @@