@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.
- package/.dockerignore +3 -1
- package/.github/workflows/build.yaml +30 -1
- package/.github/workflows/ci.yaml +37 -51
- package/api-types/storeApiSchema.json +279 -81
- package/api-types/storeApiTypes.d.ts +155 -113
- package/app/app.vue +1 -1
- package/app/components/Checkout/DeliveryTimeSelect.vue +1 -1
- package/app/components/Checkout/Summary.vue +1 -1
- package/app/components/Header.vue +1 -1
- package/app/components/Product/Card.vue +2 -1
- package/app/composables/useDeliveryTime.ts +1 -1
- package/app/composables/{usePizzaToppings.ts → useShopBiteConfig.ts} +5 -5
- package/app/layouts/listing.vue +1 -1
- package/compose.yml +0 -3
- package/container +0 -0
- package/node.dockerfile +7 -2
- package/package.json +31 -23
- package/playwright.config.ts +77 -0
- package/test/e2e/simple-checkout-as-recurring-customer.test.ts +149 -0
- package/test/nuxt/useAddToCart.test.ts +162 -0
- package/test/nuxt/useDeliveryTime.test.ts +61 -0
- package/test/nuxt/useInterval.test.ts +59 -0
- package/test/nuxt/useProductConfigurator.test.ts +87 -0
- package/{app/composables → test/nuxt}/useProductEvents.test.ts +1 -1
- package/test/nuxt/useProductVariants.test.ts +89 -0
- package/test/nuxt/useProductVariantsZwei.test.ts +48 -0
- package/test/nuxt/useScrollAnimation.test.ts +96 -0
- package/test/nuxt/useShopBiteConfig.test.ts +61 -0
- package/test/nuxt/useTopSellers.test.ts +50 -0
- package/test/nuxt/useWishlistActions.test.ts +124 -0
- package/vitest.config.ts +23 -0
|
@@ -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
|
+
});
|
|
@@ -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
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -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
|
+
});
|