@shopbite-de/storefront 1.16.1 → 1.17.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/.claude/settings.local.json +14 -1
- package/.env.example +0 -4
- package/.github/workflows/build.yaml +14 -8
- package/.github/workflows/ci.yaml +125 -42
- package/.nuxtrc +1 -1
- package/CLAUDE.md +106 -0
- package/app/components/Address/Form.vue +5 -2
- package/app/components/Category/Listing.vue +5 -4
- package/app/components/Checkout/DeliveryTimeSelect.vue +4 -2
- package/app/components/Contact/Form.vue +3 -2
- package/app/components/Hero.vue +1 -1
- package/app/components/ImageGallery.vue +1 -1
- package/app/components/Navigation/DesktopLeft.vue +2 -1
- package/app/components/Order/Detail.vue +2 -2
- package/app/components/Product/Card.vue +14 -8
- package/app/components/SalesChannelSwitch.vue +5 -3
- package/app/components/User/RegistrationForm.vue +5 -2
- package/app/composables/useBusinessHours.ts +11 -4
- package/app/composables/useCategory.ts +1 -0
- package/app/composables/useTopSellers.ts +2 -2
- package/app/pages/c/[...all].vue +2 -1
- package/app/pages/konto/adressen.vue +4 -1
- package/app/pages/konto/bestellung/[id].vue +14 -2
- package/app/pages/konto/profil.vue +5 -1
- package/app/utils/formatDate.ts +2 -1
- package/content.config.ts +1 -2
- package/eslint.config.mjs +10 -6
- package/node.dockerfile +7 -6
- package/nuxt.config.ts +16 -3
- package/package.json +38 -32
- package/renovate.json +9 -1
- package/server/api/address/autocomplete.get.ts +3 -2
- package/test/e2e/simple-checkout-as-recurring-customer.test.ts +1 -1
- package/test/nuxt/AddressFields.test.ts +12 -3
- package/test/nuxt/ContactForm.test.ts +31 -11
- package/test/nuxt/HeaderRight.test.ts +15 -6
- package/test/nuxt/LoginForm.test.ts +16 -8
- package/test/nuxt/PaymentAndDelivery.test.ts +2 -2
- package/test/nuxt/RegistrationForm.test.ts +6 -3
- package/test/nuxt/registrationSchema.test.ts +8 -8
- package/test/nuxt/useAddToCart.test.ts +5 -4
- package/test/nuxt/useAddressAutocomplete.test.ts +2 -0
- package/test/nuxt/useBusinessHours.test.ts +5 -5
- package/test/nuxt/useDeliveryTime.test.ts +17 -9
- package/test/nuxt/useProductConfigurator.test.ts +6 -4
- package/test/nuxt/useProductVariants.test.ts +16 -10
- package/test/nuxt/useProductVariantsZwei.test.ts +6 -2
- package/test/nuxt/useScrollAnimation.test.ts +16 -13
- package/test/nuxt/useTopSellers.test.ts +2 -2
- package/test/nuxt/useWishlistActions.test.ts +4 -3
- package/test/unit/useCategorySeo.spec.ts +26 -12
- package/tsconfig.json +21 -1
- package/server/utils/shopware/adminApiClient.ts +0 -24
- package/test/unit/sales-channels.test.ts +0 -66
|
@@ -50,7 +50,7 @@ describe("PaymentAndDelivery", () => {
|
|
|
50
50
|
it("updates payment method when changed", async () => {
|
|
51
51
|
const wrapper = await mountSuspended(PaymentAndDelivery);
|
|
52
52
|
|
|
53
|
-
// @ts-
|
|
53
|
+
// @ts-expect-error - access internal state
|
|
54
54
|
wrapper.vm.selectedPaymentMethodId = "pm2";
|
|
55
55
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
56
56
|
|
|
@@ -60,7 +60,7 @@ describe("PaymentAndDelivery", () => {
|
|
|
60
60
|
it("updates shipping method and refreshes cart when changed", async () => {
|
|
61
61
|
const wrapper = await mountSuspended(PaymentAndDelivery);
|
|
62
62
|
|
|
63
|
-
// @ts-
|
|
63
|
+
// @ts-expect-error - access internal state
|
|
64
64
|
wrapper.vm.selectedShippingMethodId = "sm2";
|
|
65
65
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
66
66
|
|
|
@@ -38,6 +38,7 @@ mockNuxtImport("useToast", () => () => ({
|
|
|
38
38
|
|
|
39
39
|
// Mock useRuntimeConfig
|
|
40
40
|
mockNuxtImport("useRuntimeConfig", () => () => ({
|
|
41
|
+
app: { baseURL: "/" },
|
|
41
42
|
public: {
|
|
42
43
|
shopware: {
|
|
43
44
|
devStorefrontUrl: "http://localhost:3000",
|
|
@@ -154,7 +155,7 @@ describe("RegistrationForm", () => {
|
|
|
154
155
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
155
156
|
|
|
156
157
|
expect(mockRegister).toHaveBeenCalled();
|
|
157
|
-
const calledData = mockRegister.mock.calls[0][0];
|
|
158
|
+
const calledData = mockRegister.mock.calls[0]![0];
|
|
158
159
|
expect(calledData.email).toBe("john@example.com");
|
|
159
160
|
expect(calledData.billingAddress.street).toBe("Musterstr 1");
|
|
160
161
|
// Check if firstName/lastName were copied to billing address as per logic in onSubmit
|
|
@@ -169,7 +170,7 @@ describe("RegistrationForm", () => {
|
|
|
169
170
|
detail: 'The email address "lirim@veliu.net" is already in use',
|
|
170
171
|
},
|
|
171
172
|
],
|
|
172
|
-
});
|
|
173
|
+
} as unknown as ConstructorParameters<typeof ApiClientError>[0]);
|
|
173
174
|
mockRegister.mockRejectedValueOnce(apiClientError);
|
|
174
175
|
const wrapper = await mountSuspended(RegistrationForm);
|
|
175
176
|
|
|
@@ -209,7 +210,9 @@ describe("RegistrationForm", () => {
|
|
|
209
210
|
});
|
|
210
211
|
|
|
211
212
|
it("handles ApiClientError with missing errors gracefully", async () => {
|
|
212
|
-
const apiClientError = new ApiClientError(
|
|
213
|
+
const apiClientError = new ApiClientError(
|
|
214
|
+
{} as unknown as ConstructorParameters<typeof ApiClientError>[0],
|
|
215
|
+
);
|
|
213
216
|
mockRegister.mockRejectedValueOnce(apiClientError);
|
|
214
217
|
const wrapper = await mountSuspended(RegistrationForm);
|
|
215
218
|
|
|
@@ -46,11 +46,11 @@ describe("registrationSchema", () => {
|
|
|
46
46
|
const result = schema.safeParse(businessData);
|
|
47
47
|
expect(result.success).toBe(false);
|
|
48
48
|
if (!result.success) {
|
|
49
|
-
expect(result.error.issues[0]
|
|
49
|
+
expect(result.error.issues[0]!.message).toBe(
|
|
50
50
|
"Als Geschäftskunde ist der Firmenname ein Pflichtfeld.",
|
|
51
51
|
);
|
|
52
|
-
expect(result.error.issues[0]
|
|
53
|
-
expect(result.error.issues[0]
|
|
52
|
+
expect(result.error.issues[0]!.path).toContain("billingAddress");
|
|
53
|
+
expect(result.error.issues[0]!.path).toContain("company");
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
56
|
|
|
@@ -189,10 +189,10 @@ describe("registrationSchema", () => {
|
|
|
189
189
|
});
|
|
190
190
|
expect(result.success).toBe(false);
|
|
191
191
|
if (!result.success) {
|
|
192
|
-
expect(result.error.issues[0]
|
|
192
|
+
expect(result.error.issues[0]!.message).toBe(
|
|
193
193
|
"Bitte akzeptieren Sie die Datenschutzbestimmungen.",
|
|
194
194
|
);
|
|
195
|
-
expect(result.error.issues[0]
|
|
195
|
+
expect(result.error.issues[0]!.path).toContain("acceptedDataProtection");
|
|
196
196
|
}
|
|
197
197
|
});
|
|
198
198
|
|
|
@@ -207,11 +207,11 @@ describe("registrationSchema", () => {
|
|
|
207
207
|
});
|
|
208
208
|
expect(result.success).toBe(false);
|
|
209
209
|
if (!result.success) {
|
|
210
|
-
expect(result.error.issues[0]
|
|
210
|
+
expect(result.error.issues[0]!.message).toBe(
|
|
211
211
|
"Bitte geben Sie Ihre Straße und Hausnummer an.",
|
|
212
212
|
);
|
|
213
|
-
expect(result.error.issues[0]
|
|
214
|
-
expect(result.error.issues[0]
|
|
213
|
+
expect(result.error.issues[0]!.path).toContain("billingAddress");
|
|
214
|
+
expect(result.error.issues[0]!.path).toContain("street");
|
|
215
215
|
}
|
|
216
216
|
});
|
|
217
217
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import { mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
3
3
|
|
|
4
|
-
import { useAddToCart } from "
|
|
4
|
+
import { useAddToCart } from "~/composables/useAddToCart";
|
|
5
|
+
import type { Schemas } from "#shopware";
|
|
5
6
|
import { nextTick } from "vue";
|
|
6
7
|
|
|
7
8
|
// Use vi.hoisted for variables used in mocks
|
|
@@ -66,7 +67,7 @@ describe("useAddToCart", () => {
|
|
|
66
67
|
translated: {
|
|
67
68
|
name: "Test Pizza",
|
|
68
69
|
},
|
|
69
|
-
} as
|
|
70
|
+
} as unknown as Schemas["Product"];
|
|
70
71
|
|
|
71
72
|
beforeEach(() => {
|
|
72
73
|
vi.clearAllMocks();
|
|
@@ -109,7 +110,7 @@ describe("useAddToCart", () => {
|
|
|
109
110
|
cartItemLabel,
|
|
110
111
|
} = useAddToCart();
|
|
111
112
|
setSelectedProduct(mockProduct);
|
|
112
|
-
setSelectedExtras([{ label: "Extra Cheese", value: "cheese" }]
|
|
113
|
+
setSelectedExtras([{ label: "Extra Cheese", value: "cheese", price: "" }]);
|
|
113
114
|
setDeselectedIngredients(["Onions"]);
|
|
114
115
|
await nextTick();
|
|
115
116
|
expect(cartItemLabel.value).toBe("Test Pizza +Extra Cheese -Onions");
|
|
@@ -141,7 +142,7 @@ describe("useAddToCart", () => {
|
|
|
141
142
|
it("should add product with extras to cart as container", async () => {
|
|
142
143
|
const { setSelectedProduct, setSelectedExtras, addToCart } = useAddToCart();
|
|
143
144
|
setSelectedProduct(mockProduct);
|
|
144
|
-
setSelectedExtras([{ label: "Extra Cheese", value: "cheese" }]
|
|
145
|
+
setSelectedExtras([{ label: "Extra Cheese", value: "cheese", price: "" }]);
|
|
145
146
|
|
|
146
147
|
mockAddProducts.mockResolvedValue({ id: "cart-123" });
|
|
147
148
|
|
|
@@ -5,7 +5,7 @@ import { useBusinessHours } from "~/composables/useBusinessHours";
|
|
|
5
5
|
|
|
6
6
|
// Mock useAsyncData
|
|
7
7
|
mockNuxtImport("useAsyncData", () => {
|
|
8
|
-
return (key: string, handler: () => Promise<
|
|
8
|
+
return (key: string, handler: () => Promise<unknown>) => {
|
|
9
9
|
const data = ref(null);
|
|
10
10
|
const pending = ref(false);
|
|
11
11
|
const refresh = vi.fn(async () => {
|
|
@@ -97,10 +97,10 @@ describe("useBusinessHours", () => {
|
|
|
97
97
|
const intervals = getServiceIntervals(monday);
|
|
98
98
|
|
|
99
99
|
expect(intervals).toHaveLength(2);
|
|
100
|
-
expect(intervals[0]
|
|
101
|
-
expect(intervals[1]
|
|
102
|
-
expect(intervals[0]
|
|
103
|
-
intervals[1]
|
|
100
|
+
expect(intervals[0]!.start.getHours()).toBe(11);
|
|
101
|
+
expect(intervals[1]!.start.getHours()).toBe(17);
|
|
102
|
+
expect(intervals[0]!.start.getTime()).toBeLessThan(
|
|
103
|
+
intervals[1]!.start.getTime(),
|
|
104
104
|
);
|
|
105
105
|
});
|
|
106
106
|
});
|
|
@@ -21,7 +21,7 @@ const { mockDeliveryTime, mockBusinessHours, mockHolidays } = vi.hoisted(
|
|
|
21
21
|
{ dayOfWeek: 0, openingTime: "17:30", closingTime: "23:00" },
|
|
22
22
|
],
|
|
23
23
|
},
|
|
24
|
-
mockHolidays: { value: [] },
|
|
24
|
+
mockHolidays: { value: [] as { start: string; end: string }[] | null },
|
|
25
25
|
}),
|
|
26
26
|
);
|
|
27
27
|
|
|
@@ -36,8 +36,12 @@ mockNuxtImport("useBusinessHours", () => () => ({
|
|
|
36
36
|
return mockBusinessHours.value
|
|
37
37
|
.filter((bh) => bh.dayOfWeek === dayOfWeek)
|
|
38
38
|
.map((bh) => {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
39
|
+
const startParts = bh.openingTime.split(":").map(Number);
|
|
40
|
+
const endParts = bh.closingTime.split(":").map(Number);
|
|
41
|
+
const startH = startParts[0] ?? 0;
|
|
42
|
+
const startM = startParts[1] ?? 0;
|
|
43
|
+
const endH = endParts[0] ?? 0;
|
|
44
|
+
const endM = endParts[1] ?? 0;
|
|
41
45
|
const start = new Date(date);
|
|
42
46
|
start.setHours(startH, startM, 0, 0);
|
|
43
47
|
const end = new Date(date);
|
|
@@ -73,8 +77,12 @@ mockNuxtImport("useBusinessHours", () => () => ({
|
|
|
73
77
|
]
|
|
74
78
|
.filter((bh) => bh.dayOfWeek === currentTime.getDay())
|
|
75
79
|
.map((bh) => {
|
|
76
|
-
const
|
|
77
|
-
const
|
|
80
|
+
const startParts = bh.openingTime.split(":").map(Number);
|
|
81
|
+
const endParts = bh.closingTime.split(":").map(Number);
|
|
82
|
+
const startH = startParts[0] ?? 0;
|
|
83
|
+
const startM = startParts[1] ?? 0;
|
|
84
|
+
const endH = endParts[0] ?? 0;
|
|
85
|
+
const endM = endParts[1] ?? 0;
|
|
78
86
|
const start = new Date(currentTime);
|
|
79
87
|
start.setHours(startH, startM, 0, 0);
|
|
80
88
|
const end = new Date(currentTime);
|
|
@@ -98,10 +106,10 @@ mockNuxtImport("useBusinessHours", () => () => ({
|
|
|
98
106
|
mockNuxtImport("useHolidays", () => () => ({
|
|
99
107
|
isClosedHoliday: (date: Date) => {
|
|
100
108
|
if (!mockHolidays.value) return undefined;
|
|
101
|
-
const formattedDate = date.toISOString().split("T")[0]
|
|
102
|
-
return mockHolidays.value.some((h:
|
|
103
|
-
const start = h.start.split("T")[0];
|
|
104
|
-
const end = h.end.split("T")[0];
|
|
109
|
+
const formattedDate = date.toISOString().split("T")[0]!;
|
|
110
|
+
return mockHolidays.value.some((h: { start: string; end: string }) => {
|
|
111
|
+
const start = h.start.split("T")[0] as string;
|
|
112
|
+
const end = h.end.split("T")[0] as string;
|
|
105
113
|
return formattedDate >= start && formattedDate <= end;
|
|
106
114
|
});
|
|
107
115
|
},
|
|
@@ -31,7 +31,7 @@ describe("useProductConfigurator", () => {
|
|
|
31
31
|
id: "p1",
|
|
32
32
|
optionIds: [],
|
|
33
33
|
options: [],
|
|
34
|
-
} as
|
|
34
|
+
} as unknown as typeof mockProduct.value;
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
it("should initialize with selected options from product", () => {
|
|
@@ -41,20 +41,22 @@ describe("useProductConfigurator", () => {
|
|
|
41
41
|
name: "Size",
|
|
42
42
|
options: [{ id: "o1", name: "Small" }],
|
|
43
43
|
},
|
|
44
|
-
] as
|
|
44
|
+
] as unknown as typeof mockConfigurator.value;
|
|
45
45
|
mockProduct.value = {
|
|
46
46
|
id: "p1-v1",
|
|
47
47
|
parentId: "p1",
|
|
48
48
|
optionIds: ["o1"],
|
|
49
49
|
options: [{ id: "o1" }],
|
|
50
|
-
} as
|
|
50
|
+
} as unknown as typeof mockProduct.value;
|
|
51
51
|
|
|
52
52
|
const { isLoadingOptions } = useProductConfigurator();
|
|
53
53
|
expect(isLoadingOptions.value).toBe(true);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it("should find variant for selected options", async () => {
|
|
57
|
-
mockProduct.value = {
|
|
57
|
+
mockProduct.value = {
|
|
58
|
+
parentId: "parent-1",
|
|
59
|
+
} as unknown as typeof mockProduct.value;
|
|
58
60
|
mockInvoke.mockResolvedValue({
|
|
59
61
|
data: {
|
|
60
62
|
elements: [{ id: "variant-1" }],
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { useProductVariants } from "
|
|
2
|
+
import { useProductVariants } from "~/composables/useProductVariants";
|
|
3
3
|
import { ref } from "vue";
|
|
4
4
|
|
|
5
5
|
describe("useProductVariants", () => {
|
|
6
6
|
it("should return empty object when no settings provided", () => {
|
|
7
7
|
const settings = ref([]);
|
|
8
|
-
const { variants } = useProductVariants(
|
|
8
|
+
const { variants } = useProductVariants(
|
|
9
|
+
settings as unknown as Parameters<typeof useProductVariants>[0],
|
|
10
|
+
);
|
|
9
11
|
expect(variants.value).toEqual({});
|
|
10
12
|
});
|
|
11
13
|
|
|
@@ -49,20 +51,22 @@ describe("useProductVariants", () => {
|
|
|
49
51
|
},
|
|
50
52
|
]);
|
|
51
53
|
|
|
52
|
-
const { variants } = useProductVariants(
|
|
54
|
+
const { variants } = useProductVariants(
|
|
55
|
+
settings as unknown as Parameters<typeof useProductVariants>[0],
|
|
56
|
+
);
|
|
53
57
|
|
|
54
58
|
expect(variants.value["group-size"]).toBeDefined();
|
|
55
|
-
expect(variants.value["group-size"]
|
|
56
|
-
expect(variants.value["group-size"]
|
|
57
|
-
expect(variants.value["group-size"]
|
|
59
|
+
expect(variants.value["group-size"]!.name).toBe("Größe");
|
|
60
|
+
expect(variants.value["group-size"]!.options).toHaveLength(2);
|
|
61
|
+
expect(variants.value["group-size"]!.options[0]).toEqual({
|
|
58
62
|
label: "Klein",
|
|
59
63
|
value: "opt-1",
|
|
60
64
|
productId: "opt-1",
|
|
61
65
|
});
|
|
62
66
|
|
|
63
67
|
expect(variants.value["group-color"]).toBeDefined();
|
|
64
|
-
expect(variants.value["group-color"]
|
|
65
|
-
expect(variants.value["group-color"]
|
|
68
|
+
expect(variants.value["group-color"]!.name).toBe("Farbe");
|
|
69
|
+
expect(variants.value["group-color"]!.options).toHaveLength(1);
|
|
66
70
|
});
|
|
67
71
|
|
|
68
72
|
it("should avoid duplicate options", () => {
|
|
@@ -83,7 +87,9 @@ describe("useProductVariants", () => {
|
|
|
83
87
|
},
|
|
84
88
|
]);
|
|
85
89
|
|
|
86
|
-
const { variants } = useProductVariants(
|
|
87
|
-
|
|
90
|
+
const { variants } = useProductVariants(
|
|
91
|
+
settings as unknown as Parameters<typeof useProductVariants>[0],
|
|
92
|
+
);
|
|
93
|
+
expect(variants.value["group-size"]!.options).toHaveLength(1);
|
|
88
94
|
});
|
|
89
95
|
});
|
|
@@ -22,7 +22,9 @@ describe("useProductVariantsZwei", () => {
|
|
|
22
22
|
},
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
|
-
const { variants } = useProductVariantsZwei(
|
|
25
|
+
const { variants } = useProductVariantsZwei(
|
|
26
|
+
settings as unknown as Parameters<typeof useProductVariantsZwei>[0],
|
|
27
|
+
);
|
|
26
28
|
|
|
27
29
|
expect(variants.value["group-size"]).toBeDefined();
|
|
28
30
|
expect(variants.value["group-size"]?.name).toBe("Größe");
|
|
@@ -42,7 +44,9 @@ describe("useProductVariantsZwei", () => {
|
|
|
42
44
|
},
|
|
43
45
|
]);
|
|
44
46
|
|
|
45
|
-
const { variants } = useProductVariantsZwei(
|
|
47
|
+
const { variants } = useProductVariantsZwei(
|
|
48
|
+
settings as unknown as Parameters<typeof useProductVariantsZwei>[0],
|
|
49
|
+
);
|
|
46
50
|
expect(variants.value["group-size"]?.options).toHaveLength(1);
|
|
47
51
|
});
|
|
48
52
|
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { useScrollAnimation } from "
|
|
2
|
+
import { useScrollAnimation } from "~/composables/useScrollAnimation";
|
|
3
3
|
import { mount } from "@vue/test-utils";
|
|
4
4
|
import { defineComponent, nextTick } from "vue";
|
|
5
5
|
|
|
6
6
|
describe("useScrollAnimation", () => {
|
|
7
|
-
let observeMock:
|
|
8
|
-
let unobserveMock:
|
|
9
|
-
let disconnectMock:
|
|
10
|
-
let intersectionCallback:
|
|
7
|
+
let observeMock: ReturnType<typeof vi.fn>;
|
|
8
|
+
let unobserveMock: ReturnType<typeof vi.fn>;
|
|
9
|
+
let disconnectMock: ReturnType<typeof vi.fn>;
|
|
10
|
+
let intersectionCallback: IntersectionObserverCallback;
|
|
11
11
|
|
|
12
12
|
beforeEach(() => {
|
|
13
13
|
observeMock = vi.fn();
|
|
@@ -15,14 +15,17 @@ describe("useScrollAnimation", () => {
|
|
|
15
15
|
disconnectMock = vi.fn();
|
|
16
16
|
|
|
17
17
|
// Mock IntersectionObserver
|
|
18
|
-
global.IntersectionObserver = vi
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
global.IntersectionObserver = vi.fn().mockImplementation(function (
|
|
19
|
+
this: IntersectionObserver,
|
|
20
|
+
callback: IntersectionObserverCallback,
|
|
21
|
+
) {
|
|
22
|
+
intersectionCallback = callback;
|
|
23
|
+
this.observe = observeMock as unknown as IntersectionObserver["observe"];
|
|
24
|
+
this.unobserve =
|
|
25
|
+
unobserveMock as unknown as IntersectionObserver["unobserve"];
|
|
26
|
+
this.disconnect =
|
|
27
|
+
disconnectMock as unknown as IntersectionObserver["disconnect"];
|
|
28
|
+
}) as unknown as typeof IntersectionObserver;
|
|
26
29
|
});
|
|
27
30
|
|
|
28
31
|
it("should initialize with isVisible false", () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import { mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
3
|
-
import { useTopSellers } from "
|
|
3
|
+
import { useTopSellers } from "~/composables/useTopSellers";
|
|
4
4
|
|
|
5
5
|
const { mockInvoke } = vi.hoisted(() => ({
|
|
6
6
|
mockInvoke: vi.fn(),
|
|
@@ -31,7 +31,7 @@ describe("useTopSellers", () => {
|
|
|
31
31
|
const result = await loadTopSellers();
|
|
32
32
|
|
|
33
33
|
expect(mockInvoke).toHaveBeenCalledWith(
|
|
34
|
-
"
|
|
34
|
+
"readProduct post /product",
|
|
35
35
|
expect.any(Object),
|
|
36
36
|
);
|
|
37
37
|
expect(result).toEqual(mockElements);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import { mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import { useWishlistActions } from "../../app/composables/useWishlistActions";
|
|
4
|
+
import type { Schemas } from "#shopware";
|
|
4
5
|
|
|
5
6
|
const {
|
|
6
7
|
mockAddProducts,
|
|
@@ -44,7 +45,7 @@ describe("useWishlistActions", () => {
|
|
|
44
45
|
id: "prod-1",
|
|
45
46
|
translated: { name: "Test Product" },
|
|
46
47
|
productNumber: "SW123",
|
|
47
|
-
} as
|
|
48
|
+
} as unknown as Schemas["Product"];
|
|
48
49
|
|
|
49
50
|
beforeEach(() => {
|
|
50
51
|
vi.clearAllMocks();
|
|
@@ -93,7 +94,7 @@ describe("useWishlistActions", () => {
|
|
|
93
94
|
const products = [
|
|
94
95
|
{ id: "p1", translated: { name: "P1" } },
|
|
95
96
|
{ id: "p2", translated: { name: "P2" } },
|
|
96
|
-
] as
|
|
97
|
+
] as unknown as Schemas["Product"][];
|
|
97
98
|
const { addAllItemsToCart, isAddingToCart } = useWishlistActions();
|
|
98
99
|
mockAddProducts.mockResolvedValue({ id: "cart-1" });
|
|
99
100
|
|
|
@@ -119,7 +120,7 @@ describe("useWishlistActions", () => {
|
|
|
119
120
|
const products = [
|
|
120
121
|
{ id: "p1", translated: { name: "P1" } },
|
|
121
122
|
{ id: "p2", childCount: 1, translated: { name: "P2" } },
|
|
122
|
-
] as
|
|
123
|
+
] as unknown as Schemas["Product"][];
|
|
123
124
|
const { addAllItemsToCart } = useWishlistActions();
|
|
124
125
|
mockAddProducts.mockResolvedValue({ id: "cart-1" });
|
|
125
126
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import { ref } from "vue";
|
|
3
3
|
|
|
4
|
+
// Re-import the mocks for assertions
|
|
5
|
+
import { useHead, useSeoMeta } from "#imports";
|
|
6
|
+
|
|
4
7
|
// Create shared mocks to be exported by both '#imports' and '#app' in a hoisted-safe way
|
|
5
8
|
const shared = vi.hoisted(() => ({
|
|
6
9
|
useHead: vi.fn(),
|
|
@@ -41,24 +44,25 @@ vi.mock("#app", async () => {
|
|
|
41
44
|
};
|
|
42
45
|
});
|
|
43
46
|
|
|
44
|
-
// Re-import the mocks for assertions
|
|
45
|
-
import { useHead, useSeoMeta } from "#imports";
|
|
46
|
-
|
|
47
47
|
// Target under test will be dynamically imported after setting up globals
|
|
48
|
-
let useCategorySeo: (
|
|
48
|
+
let useCategorySeo: (
|
|
49
|
+
arg: unknown,
|
|
50
|
+
) => ReturnType<
|
|
51
|
+
(typeof import("../../app/composables/useCategorySeo"))["useCategorySeo"]
|
|
52
|
+
>;
|
|
49
53
|
|
|
50
54
|
describe("useCategorySeo", () => {
|
|
51
55
|
beforeEach(async () => {
|
|
52
56
|
vi.clearAllMocks();
|
|
53
57
|
// Provide globals for auto-imported functions (when not transformed in unit env)
|
|
54
58
|
const vue = await import("vue");
|
|
55
|
-
(globalThis as
|
|
56
|
-
(globalThis as
|
|
57
|
-
(globalThis as
|
|
59
|
+
(globalThis as Record<string, unknown>).computed = vue.computed;
|
|
60
|
+
(globalThis as Record<string, unknown>).ref = vue.ref;
|
|
61
|
+
(globalThis as Record<string, unknown>).useRuntimeConfig = () => ({
|
|
58
62
|
public: { site: { name: "My Store" }, storeUrl: "https://example.com" },
|
|
59
63
|
});
|
|
60
|
-
(globalThis as
|
|
61
|
-
(globalThis as
|
|
64
|
+
(globalThis as Record<string, unknown>).useHead = useHead;
|
|
65
|
+
(globalThis as Record<string, unknown>).useSeoMeta = useSeoMeta;
|
|
62
66
|
|
|
63
67
|
// Dynamic import after globals are ready
|
|
64
68
|
useCategorySeo = (await import("../../app/composables/useCategorySeo"))
|
|
@@ -66,7 +70,17 @@ describe("useCategorySeo", () => {
|
|
|
66
70
|
});
|
|
67
71
|
|
|
68
72
|
it("computes core SEO refs and injects head tags", () => {
|
|
69
|
-
const category = ref<
|
|
73
|
+
const category = ref<{
|
|
74
|
+
translated?: {
|
|
75
|
+
metaTitle?: string;
|
|
76
|
+
metaDescription?: string;
|
|
77
|
+
breadcrumb?: string[];
|
|
78
|
+
name?: string;
|
|
79
|
+
};
|
|
80
|
+
seoUrl?: string;
|
|
81
|
+
active?: boolean;
|
|
82
|
+
media?: { url?: string };
|
|
83
|
+
}>({
|
|
70
84
|
translated: {
|
|
71
85
|
metaTitle: "Pizza & Pasta",
|
|
72
86
|
metaDescription: "Leckere Pizza und Pasta bestellen",
|
|
@@ -96,7 +110,7 @@ describe("useCategorySeo", () => {
|
|
|
96
110
|
useHead as unknown as ReturnType<typeof vi.fn>,
|
|
97
111
|
).toHaveBeenCalledTimes(1);
|
|
98
112
|
const headArg = (useHead as unknown as ReturnType<typeof vi.fn>).mock
|
|
99
|
-
.calls[0][0]
|
|
113
|
+
.calls[0]![0]!;
|
|
100
114
|
|
|
101
115
|
// Canonical link
|
|
102
116
|
const link = headArg.link?.[0];
|
|
@@ -116,7 +130,7 @@ describe("useCategorySeo", () => {
|
|
|
116
130
|
});
|
|
117
131
|
|
|
118
132
|
it("sets robots to noindex when category is inactive", () => {
|
|
119
|
-
const category = ref
|
|
133
|
+
const category = ref({
|
|
120
134
|
translated: { name: "Salate" },
|
|
121
135
|
active: false,
|
|
122
136
|
seoUrl: "/c/salate",
|
package/tsconfig.json
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
-
// https://v3.nuxtjs.org/concepts/typescript
|
|
3
2
|
"extends": "./.nuxt/tsconfig.json",
|
|
3
|
+
"include": [
|
|
4
|
+
".nuxt/nuxt.d.ts",
|
|
5
|
+
".nuxt/nuxt.node.d.ts",
|
|
6
|
+
"app/**/*",
|
|
7
|
+
"server/**/*",
|
|
8
|
+
"shared/**/*",
|
|
9
|
+
"modules/**/*",
|
|
10
|
+
"layers/**/*",
|
|
11
|
+
"test/**/*",
|
|
12
|
+
"tests/**/*",
|
|
13
|
+
"*.d.ts",
|
|
14
|
+
"nuxt.config.*",
|
|
15
|
+
".config/nuxt.*",
|
|
16
|
+
"content.config.*"
|
|
17
|
+
],
|
|
18
|
+
"exclude": [
|
|
19
|
+
"node_modules",
|
|
20
|
+
],
|
|
21
|
+
"compilerOptions": {
|
|
22
|
+
"typeRoots": ["./api-types", "./node_modules/@types", "./node_modules"]
|
|
23
|
+
}
|
|
4
24
|
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { createAdminAPIClient } from "@shopware/api-client";
|
|
2
|
-
import type { operations } from "@shopware/api-client/admin-api-types";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Creates a Shopware Admin API client
|
|
6
|
-
*/
|
|
7
|
-
export function createAdminApiClient(baseURL: string, accessToken: string) {
|
|
8
|
-
return createAdminAPIClient<operations>({
|
|
9
|
-
baseURL,
|
|
10
|
-
accessToken,
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Fetches sales channels using the Admin API client
|
|
16
|
-
*/
|
|
17
|
-
export async function getSalesChannels(client: any) {
|
|
18
|
-
const response = await client.invoke({
|
|
19
|
-
method: "GET",
|
|
20
|
-
path: "/api/v3/sales-channel",
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
return response.data;
|
|
24
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
createAdminApiClient,
|
|
4
|
-
getSalesChannels,
|
|
5
|
-
} from "../../server/utils/shopware/adminApiClient";
|
|
6
|
-
|
|
7
|
-
describe("Shopware Admin API Client", () => {
|
|
8
|
-
describe("createAdminApiClient", () => {
|
|
9
|
-
it("should create a client with correct configuration", () => {
|
|
10
|
-
const endpoint = "https://shopware.example.com/api";
|
|
11
|
-
const accessToken = "test-token";
|
|
12
|
-
|
|
13
|
-
const client = createAdminApiClient(endpoint, accessToken);
|
|
14
|
-
|
|
15
|
-
expect(client).toBeDefined();
|
|
16
|
-
// The client should have the invoke method
|
|
17
|
-
expect(client).toHaveProperty("invoke");
|
|
18
|
-
expect(typeof client.invoke).toBe("function");
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe("getSalesChannels", () => {
|
|
23
|
-
it("should fetch sales channels successfully", async () => {
|
|
24
|
-
// Mock the client
|
|
25
|
-
const mockClient = {
|
|
26
|
-
invoke: vi.fn().mockResolvedValue({
|
|
27
|
-
data: [
|
|
28
|
-
{ id: "1", name: "Storefront", typeId: "storefront" },
|
|
29
|
-
{ id: "2", name: "Headless", typeId: "headless" },
|
|
30
|
-
],
|
|
31
|
-
}),
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const result = await getSalesChannels(mockClient);
|
|
35
|
-
|
|
36
|
-
expect(result).toEqual([
|
|
37
|
-
{ id: "1", name: "Storefront", typeId: "storefront" },
|
|
38
|
-
{ id: "2", name: "Headless", typeId: "headless" },
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
|
-
expect(mockClient.invoke).toHaveBeenCalledWith({
|
|
42
|
-
method: "GET",
|
|
43
|
-
path: "/api/v3/sales-channel",
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("should handle errors when fetching sales channels", async () => {
|
|
48
|
-
const mockClient = {
|
|
49
|
-
invoke: vi.fn().mockRejectedValue(new Error("API error")),
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
await expect(getSalesChannels(mockClient)).rejects.toThrow("API error");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("should handle empty response gracefully", async () => {
|
|
56
|
-
const mockClient = {
|
|
57
|
-
invoke: vi.fn().mockResolvedValue({
|
|
58
|
-
data: [],
|
|
59
|
-
}),
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const result = await getSalesChannels(mockClient);
|
|
63
|
-
expect(result).toEqual([]);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
});
|