@shopbite-de/storefront 1.2.8 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { useInterval } from "../../app/composables/useInterval";
3
+ import { mount } from "@vue/test-utils";
4
+ import { defineComponent } from "vue";
5
+
6
+ describe("useInterval", () => {
7
+ beforeEach(() => {
8
+ vi.useFakeTimers();
9
+ });
10
+
11
+ afterEach(() => {
12
+ vi.restoreAllMocks();
13
+ vi.useRealTimers();
14
+ });
15
+
16
+ it("should start interval on mount and call callback", async () => {
17
+ const callback = vi.fn();
18
+ const TestComponent = defineComponent({
19
+ setup() {
20
+ useInterval(callback, 1000);
21
+ return {};
22
+ },
23
+ template: "<div></div>",
24
+ });
25
+
26
+ const wrapper = mount(TestComponent);
27
+
28
+ expect(callback).not.toHaveBeenCalled();
29
+
30
+ vi.advanceTimersByTime(1000);
31
+ expect(callback).toHaveBeenCalledTimes(1);
32
+
33
+ vi.advanceTimersByTime(1000);
34
+ expect(callback).toHaveBeenCalledTimes(2);
35
+
36
+ wrapper.unmount();
37
+ });
38
+
39
+ it("should clear interval on unmount", () => {
40
+ const callback = vi.fn();
41
+ const TestComponent = defineComponent({
42
+ setup() {
43
+ useInterval(callback, 1000);
44
+ return {};
45
+ },
46
+ template: "<div></div>",
47
+ });
48
+
49
+ const wrapper = mount(TestComponent);
50
+
51
+ vi.advanceTimersByTime(1000);
52
+ expect(callback).toHaveBeenCalledTimes(1);
53
+
54
+ wrapper.unmount();
55
+
56
+ vi.advanceTimersByTime(1000);
57
+ expect(callback).toHaveBeenCalledTimes(1); // Should not have increased
58
+ });
59
+ });
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { useProductConfigurator } from "../../app/composables/useProductConfigurator";
3
+
4
+
5
+ const {
6
+ mockInvoke,
7
+ mockConfigurator,
8
+ mockProduct,
9
+ } = vi.hoisted(() => ({
10
+ mockInvoke: vi.fn(),
11
+ mockConfigurator: { value: [] },
12
+ mockProduct: { value: { id: "p1", optionIds: [], options: [] } },
13
+ }));
14
+
15
+ // Use vi.mock for explicit imports
16
+ vi.mock("@shopware/composables", () => ({
17
+ useShopwareContext: () => ({
18
+ apiClient: {
19
+ invoke: mockInvoke,
20
+ },
21
+ }),
22
+ useProductConfigurator: () => ({
23
+ handleChange: vi.fn(),
24
+ }),
25
+ useProduct: () => ({
26
+ configurator: mockConfigurator,
27
+ product: mockProduct,
28
+ }),
29
+ }));
30
+
31
+ describe("useProductConfigurator", () => {
32
+ beforeEach(() => {
33
+ vi.clearAllMocks();
34
+ mockConfigurator.value = [];
35
+ mockProduct.value = {
36
+ id: "p1",
37
+ optionIds: [],
38
+ options: []
39
+ } as any;
40
+ });
41
+
42
+ it("should initialize with selected options from product", () => {
43
+ mockConfigurator.value = [
44
+ {
45
+ id: "g1",
46
+ name: "Size",
47
+ options: [{ id: "o1", name: "Small" }]
48
+ }
49
+ ] as any;
50
+ mockProduct.value = {
51
+ id: "p1-v1",
52
+ parentId: "p1",
53
+ optionIds: ["o1"],
54
+ options: [{ id: "o1" }]
55
+ } as any;
56
+
57
+ const { isLoadingOptions } = useProductConfigurator();
58
+ expect(isLoadingOptions.value).toBe(true);
59
+ });
60
+
61
+ it("should find variant for selected options", async () => {
62
+ mockProduct.value = { parentId: "parent-1" } as any;
63
+ mockInvoke.mockResolvedValue({
64
+ data: {
65
+ elements: [{ id: "variant-1" }]
66
+ }
67
+ });
68
+
69
+ const { findVariantForSelectedOptions } = useProductConfigurator();
70
+ const result = await findVariantForSelectedOptions({ "Size": "o1" });
71
+
72
+ expect(mockInvoke).toHaveBeenCalledWith("readProduct post /product", expect.any(Object));
73
+ expect(result).toEqual({ id: "variant-1" });
74
+ });
75
+
76
+ it("should return undefined on error in findVariantForSelectedOptions", async () => {
77
+ mockInvoke.mockRejectedValue(new Error("API Error"));
78
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
79
+
80
+ const { findVariantForSelectedOptions } = useProductConfigurator();
81
+ const result = await findVariantForSelectedOptions({ "Size": "o1" });
82
+
83
+ expect(result).toBeUndefined();
84
+ expect(consoleSpy).toHaveBeenCalled();
85
+ consoleSpy.mockRestore();
86
+ });
87
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
- import { useProductEvents } from "./useProductEvents";
2
+ import { useProductEvents } from "../../app/composables/useProductEvents";
3
3
  import { nextTick } from "vue";
4
4
 
5
5
  /**
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { useProductVariants } from "../../app/composables/useProductVariants";
3
+ import { ref } from "vue";
4
+
5
+ describe("useProductVariants", () => {
6
+ it("should return empty object when no settings provided", () => {
7
+ const settings = ref([]);
8
+ const { variants } = useProductVariants(settings as any);
9
+ expect(variants.value).toEqual({});
10
+ });
11
+
12
+ it("should group options by group id", () => {
13
+ const settings = ref([
14
+ {
15
+ option: {
16
+ id: "opt-1",
17
+ name: "Small",
18
+ group: {
19
+ id: "group-size",
20
+ name: "Size",
21
+ translated: { name: "Größe" }
22
+ },
23
+ translated: { name: "Klein" }
24
+ }
25
+ },
26
+ {
27
+ option: {
28
+ id: "opt-2",
29
+ name: "Large",
30
+ group: {
31
+ id: "group-size",
32
+ name: "Size",
33
+ translated: { name: "Größe" }
34
+ },
35
+ translated: { name: "Groß" }
36
+ }
37
+ },
38
+ {
39
+ option: {
40
+ id: "opt-3",
41
+ name: "Red",
42
+ group: {
43
+ id: "group-color",
44
+ name: "Color",
45
+ translated: { name: "Farbe" }
46
+ },
47
+ translated: { name: "Rot" }
48
+ }
49
+ }
50
+ ]);
51
+
52
+ const { variants } = useProductVariants(settings as any);
53
+
54
+ expect(variants.value["group-size"]).toBeDefined();
55
+ expect(variants.value["group-size"].name).toBe("Größe");
56
+ expect(variants.value["group-size"].options).toHaveLength(2);
57
+ expect(variants.value["group-size"].options[0]).toEqual({
58
+ label: "Klein",
59
+ value: "opt-1",
60
+ productId: "opt-1"
61
+ });
62
+
63
+ expect(variants.value["group-color"]).toBeDefined();
64
+ expect(variants.value["group-color"].name).toBe("Farbe");
65
+ expect(variants.value["group-color"].options).toHaveLength(1);
66
+ });
67
+
68
+ it("should avoid duplicate options", () => {
69
+ const settings = ref([
70
+ {
71
+ option: {
72
+ id: "opt-1",
73
+ name: "Small",
74
+ group: { id: "group-size", name: "Size" }
75
+ }
76
+ },
77
+ {
78
+ option: {
79
+ id: "opt-1",
80
+ name: "Small",
81
+ group: { id: "group-size", name: "Size" }
82
+ }
83
+ }
84
+ ]);
85
+
86
+ const { variants } = useProductVariants(settings as any);
87
+ expect(variants.value["group-size"].options).toHaveLength(1);
88
+ });
89
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { useProductVariantsZwei } from "../../app/composables/useProductVariantsZwei";
3
+ import { ref } from "vue";
4
+
5
+ describe("useProductVariantsZwei", () => {
6
+ it("should return empty object when no settings provided", () => {
7
+ const settings = ref([]);
8
+ const { variants } = useProductVariantsZwei(settings);
9
+ expect(variants.value).toEqual({});
10
+ });
11
+
12
+ it("should group options by group", () => {
13
+ const settings = ref([
14
+ {
15
+ id: "group-size",
16
+ name: "Size",
17
+ translated: { name: "Größe" },
18
+ options: [
19
+ { id: "opt-1", name: "Small", translated: { name: "Klein" } },
20
+ { id: "opt-2", name: "Large", translated: { name: "Groß" } }
21
+ ]
22
+ }
23
+ ]);
24
+
25
+ const { variants } = useProductVariantsZwei(settings);
26
+
27
+ expect(variants.value["group-size"]).toBeDefined();
28
+ expect(variants.value["group-size"]?.name).toBe("Größe");
29
+ expect(variants.value["group-size"]?.options).toHaveLength(2);
30
+ expect(variants.value["group-size"]?.options[0]?.label).toBe("Klein");
31
+ });
32
+
33
+ it("should avoid duplicate options", () => {
34
+ const settings = ref([
35
+ {
36
+ id: "group-size",
37
+ name: "Size",
38
+ options: [
39
+ { id: "opt-1", name: "Small" },
40
+ { id: "opt-1", name: "Small" }
41
+ ]
42
+ }
43
+ ]);
44
+
45
+ const { variants } = useProductVariantsZwei(settings);
46
+ expect(variants.value["group-size"]?.options).toHaveLength(1);
47
+ });
48
+ });
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { useScrollAnimation } from "../../app/composables/useScrollAnimation";
3
+ import { mount } from "@vue/test-utils";
4
+ import { defineComponent, nextTick } from "vue";
5
+
6
+ describe("useScrollAnimation", () => {
7
+ let observeMock: any;
8
+ let unobserveMock: any;
9
+ let disconnectMock: any;
10
+ let intersectionCallback: any;
11
+
12
+ beforeEach(() => {
13
+ observeMock = vi.fn();
14
+ unobserveMock = vi.fn();
15
+ disconnectMock = vi.fn();
16
+
17
+ // Mock IntersectionObserver
18
+ global.IntersectionObserver = vi.fn().mockImplementation(function (callback) {
19
+ intersectionCallback = callback;
20
+ this.observe = observeMock;
21
+ this.unobserve = unobserveMock;
22
+ this.disconnect = disconnectMock;
23
+ }) as any;
24
+ });
25
+
26
+ it("should initialize with isVisible false", () => {
27
+ const TestComponent = defineComponent({
28
+ setup() {
29
+ const { isVisible, elementRef } = useScrollAnimation();
30
+ return { isVisible, elementRef };
31
+ },
32
+ template: '<div ref="elementRef"></div>',
33
+ });
34
+
35
+ const wrapper = mount(TestComponent);
36
+ expect(wrapper.vm.isVisible).toBe(false);
37
+ });
38
+
39
+ it("should start observing on mount", async () => {
40
+ const TestComponent = defineComponent({
41
+ setup() {
42
+ const { isVisible, elementRef } = useScrollAnimation();
43
+ return { isVisible, elementRef };
44
+ },
45
+ template: '<div ref="elementRef"></div>',
46
+ });
47
+
48
+ mount(TestComponent);
49
+ await nextTick();
50
+
51
+ expect(global.IntersectionObserver).toHaveBeenCalled();
52
+ expect(observeMock).toHaveBeenCalled();
53
+ });
54
+
55
+ it("should set isVisible to true when intersecting and stop observing", async () => {
56
+ const TestComponent = defineComponent({
57
+ setup() {
58
+ const { isVisible, elementRef } = useScrollAnimation();
59
+ return { isVisible, elementRef };
60
+ },
61
+ template: '<div ref="elementRef"></div>',
62
+ });
63
+
64
+ const wrapper = mount(TestComponent);
65
+ await nextTick();
66
+
67
+ // Simulate intersection
68
+ const mockEntry = { isIntersecting: true, target: wrapper.element };
69
+ intersectionCallback([mockEntry]);
70
+
71
+ await nextTick();
72
+ expect(wrapper.vm.isVisible).toBe(true);
73
+ expect(unobserveMock).toHaveBeenCalledWith(wrapper.element);
74
+ });
75
+
76
+ it("should not set isVisible to true when not intersecting", async () => {
77
+ const TestComponent = defineComponent({
78
+ setup() {
79
+ const { isVisible, elementRef } = useScrollAnimation();
80
+ return { isVisible, elementRef };
81
+ },
82
+ template: '<div ref="elementRef"></div>',
83
+ });
84
+
85
+ const wrapper = mount(TestComponent);
86
+ await nextTick();
87
+
88
+ // Simulate non-intersection
89
+ const mockEntry = { isIntersecting: false, target: wrapper.element };
90
+ intersectionCallback([mockEntry]);
91
+
92
+ await nextTick();
93
+ expect(wrapper.vm.isVisible).toBe(false);
94
+ expect(unobserveMock).not.toHaveBeenCalled();
95
+ });
96
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { mockNuxtImport } from "@nuxt/test-utils/runtime";
3
+ import { useShopBiteConfig } from "~/composables/useShopBiteConfig";
4
+
5
+ const {
6
+ mockInvoke,
7
+ mockDeliveryTime,
8
+ mockIsCheckoutEnabled,
9
+ } = vi.hoisted(() => ({
10
+ mockInvoke: vi.fn(),
11
+ mockDeliveryTime: { value: 0 },
12
+ mockIsCheckoutEnabled: { value: false },
13
+ }));
14
+
15
+ mockNuxtImport("useShopwareContext", () => () => ({
16
+ apiClient: {
17
+ invoke: mockInvoke,
18
+ },
19
+ }));
20
+
21
+ mockNuxtImport("useContext", () => (key: string) => {
22
+ if (key === "deliveryTime") return mockDeliveryTime;
23
+ if (key === "isCheckoutActive") return mockIsCheckoutEnabled;
24
+ return { value: null };
25
+ });
26
+
27
+ describe("useShopBiteConfig", () => {
28
+ beforeEach(() => {
29
+ vi.clearAllMocks();
30
+ mockDeliveryTime.value = 0;
31
+ mockIsCheckoutEnabled.value = false;
32
+ });
33
+
34
+ it("should initialize with values from context", () => {
35
+ mockDeliveryTime.value = 30;
36
+ mockIsCheckoutEnabled.value = true;
37
+ const { deliveryTime, isCheckoutEnabled } = useShopBiteConfig();
38
+ expect(deliveryTime.value).toBe(30);
39
+ expect(isCheckoutEnabled.value).toBe(true);
40
+ });
41
+
42
+ it("should refresh values from API", async () => {
43
+ mockInvoke.mockResolvedValue({
44
+ data: {
45
+ deliveryTime: 45,
46
+ isCheckoutEnabled: true
47
+ }
48
+ });
49
+
50
+ const { refresh, deliveryTime, isCheckoutEnabled } = useShopBiteConfig();
51
+ await refresh();
52
+
53
+ expect(mockInvoke).toHaveBeenCalledWith(
54
+ "shopbite.config.get get /shopbite/config",
55
+ );
56
+ expect(deliveryTime.value).toBe(45);
57
+ expect(isCheckoutEnabled.value).toBe(true);
58
+ expect(mockDeliveryTime.value).toBe(45);
59
+ expect(mockIsCheckoutEnabled.value).toBe(true);
60
+ });
61
+ });
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { mockNuxtImport } from "@nuxt/test-utils/runtime";
3
+ import { useTopSellers } from "../../app/composables/useTopSellers";
4
+
5
+ const { mockInvoke } = vi.hoisted(() => ({
6
+ mockInvoke: vi.fn(),
7
+ }));
8
+
9
+ mockNuxtImport("useShopwareContext", () => {
10
+ return () => ({
11
+ apiClient: {
12
+ invoke: mockInvoke,
13
+ },
14
+ });
15
+ });
16
+
17
+ describe("useTopSellers", () => {
18
+ beforeEach(() => {
19
+ vi.clearAllMocks();
20
+ });
21
+
22
+ it("should load top sellers successfully", async () => {
23
+ const mockElements = [{ id: "1", name: "Top Product" }];
24
+ mockInvoke.mockResolvedValue({
25
+ data: {
26
+ elements: mockElements,
27
+ },
28
+ });
29
+
30
+ const { loadTopSellers } = useTopSellers();
31
+ const result = await loadTopSellers();
32
+
33
+ expect(mockInvoke).toHaveBeenCalledWith("getTopSellers post /product", expect.any(Object));
34
+ expect(result).toEqual(mockElements);
35
+ });
36
+
37
+ it("should return empty array on error", async () => {
38
+ mockInvoke.mockRejectedValue(new Error("Network error"));
39
+
40
+ // Silence console.error for this test
41
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
42
+
43
+ const { loadTopSellers } = useTopSellers();
44
+ const result = await loadTopSellers();
45
+
46
+ expect(result).toEqual([]);
47
+ expect(consoleSpy).toHaveBeenCalled();
48
+ consoleSpy.mockRestore();
49
+ });
50
+ });
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { mockNuxtImport } from "@nuxt/test-utils/runtime";
3
+ import { useWishlistActions } from "../../app/composables/useWishlistActions";
4
+
5
+
6
+ const {
7
+ mockAddProducts,
8
+ mockRefreshCart,
9
+ mockToastAdd,
10
+ mockTriggerProductAdded,
11
+ mockClearWishlist,
12
+ mockTrackEvent,
13
+ } = vi.hoisted(() => ({
14
+ mockAddProducts: vi.fn(),
15
+ mockRefreshCart: vi.fn(),
16
+ mockToastAdd: vi.fn(),
17
+ mockTriggerProductAdded: vi.fn(),
18
+ mockClearWishlist: vi.fn(),
19
+ mockTrackEvent: vi.fn(),
20
+ }));
21
+
22
+ mockNuxtImport("useCart", () => () => ({
23
+ addProducts: mockAddProducts,
24
+ refreshCart: mockRefreshCart,
25
+ }));
26
+
27
+ mockNuxtImport("useToast", () => () => ({
28
+ add: mockToastAdd,
29
+ }));
30
+
31
+ mockNuxtImport("useProductEvents", () => () => ({
32
+ triggerProductAdded: mockTriggerProductAdded,
33
+ }));
34
+
35
+ mockNuxtImport("useWishlist", () => () => ({
36
+ clearWishlist: mockClearWishlist,
37
+ }));
38
+
39
+ mockNuxtImport("useTrackEvent", () => mockTrackEvent);
40
+
41
+ describe("useWishlistActions", () => {
42
+ const mockProduct = {
43
+ id: "prod-1",
44
+ translated: { name: "Test Product" },
45
+ productNumber: "SW123",
46
+ } as any;
47
+
48
+ beforeEach(() => {
49
+ vi.clearAllMocks();
50
+ });
51
+
52
+ it("should clear wishlist", async () => {
53
+ const { clearWishlistHandler, isLoading } = useWishlistActions();
54
+ await clearWishlistHandler();
55
+ expect(mockClearWishlist).toHaveBeenCalled();
56
+ expect(mockToastAdd).toHaveBeenCalledWith(expect.objectContaining({ title: "Merkliste geleert" }));
57
+ expect(isLoading.value).toBe(false);
58
+ });
59
+
60
+ it("should add single item to cart", async () => {
61
+ const { addSingleItemToCart } = useWishlistActions();
62
+ mockAddProducts.mockResolvedValue({ id: "cart-1" });
63
+
64
+ await addSingleItemToCart(mockProduct);
65
+
66
+ expect(mockAddProducts).toHaveBeenCalledWith([{ id: "prod-1", quantity: 1, type: "product" }]);
67
+ expect(mockRefreshCart).toHaveBeenCalled();
68
+ expect(mockTriggerProductAdded).toHaveBeenCalled();
69
+ expect(mockToastAdd).toHaveBeenCalledWith(expect.objectContaining({ title: "In den Warenkorb gelegt" }));
70
+ expect(mockTrackEvent).toHaveBeenCalledWith("add_to_cart", expect.any(Object));
71
+ });
72
+
73
+ it("should warn when adding a base product with variants", async () => {
74
+ const baseProduct = { ...mockProduct, childCount: 2 };
75
+ const { addSingleItemToCart } = useWishlistActions();
76
+
77
+ await addSingleItemToCart(baseProduct);
78
+
79
+ expect(mockAddProducts).not.toHaveBeenCalled();
80
+ expect(mockToastAdd).toHaveBeenCalledWith(expect.objectContaining({ title: "Variante erforderlich" }));
81
+ });
82
+
83
+ it("should add all items to cart", async () => {
84
+ const products = [
85
+ { id: "p1", translated: { name: "P1" } },
86
+ { id: "p2", translated: { name: "P2" } },
87
+ ] as any[];
88
+ const { addAllItemsToCart, isAddingToCart } = useWishlistActions();
89
+ mockAddProducts.mockResolvedValue({ id: "cart-1" });
90
+
91
+ await addAllItemsToCart(products);
92
+
93
+ expect(mockAddProducts).toHaveBeenCalledWith([
94
+ { id: "p1", quantity: 1, type: "product" },
95
+ { id: "p2", quantity: 1, type: "product" },
96
+ ]);
97
+ expect(mockToastAdd).toHaveBeenCalledWith(expect.objectContaining({ title: "Produkte hinzugefügt" }));
98
+ expect(isAddingToCart.value).toBe(false);
99
+ });
100
+
101
+ it("should handle empty products in addAllItemsToCart", async () => {
102
+ const { addAllItemsToCart } = useWishlistActions();
103
+ await addAllItemsToCart([]);
104
+ expect(mockAddProducts).not.toHaveBeenCalled();
105
+ });
106
+
107
+ it("should skip base products in addAllItemsToCart", async () => {
108
+ const products = [
109
+ { id: "p1", translated: { name: "P1" } },
110
+ { id: "p2", childCount: 1, translated: { name: "P2" } },
111
+ ] as any[];
112
+ const { addAllItemsToCart } = useWishlistActions();
113
+ mockAddProducts.mockResolvedValue({ id: "cart-1" });
114
+
115
+ await addAllItemsToCart(products);
116
+
117
+ expect(mockAddProducts).toHaveBeenCalledWith([
118
+ { id: "p1", quantity: 1, type: "product" },
119
+ ]);
120
+ expect(mockToastAdd).toHaveBeenCalledWith(expect.objectContaining({
121
+ description: expect.stringContaining("1 Produkte hinzugefügt. 1 Produkt(e) übersprungen")
122
+ }));
123
+ });
124
+ });
@@ -0,0 +1,23 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import { defineVitestProject } from "@nuxt/test-utils/config";
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ projects: [
7
+ {
8
+ test: {
9
+ name: "unit",
10
+ include: ["test/unit/*.{test,spec}.ts"],
11
+ environment: "node",
12
+ },
13
+ },
14
+ await defineVitestProject({
15
+ test: {
16
+ name: "nuxt",
17
+ include: ["test/nuxt/*.{test,spec}.ts"],
18
+ environment: "nuxt",
19
+ },
20
+ }),
21
+ ],
22
+ },
23
+ });