@shopbite-de/storefront 1.16.0 → 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 +21 -0
- 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/app.vue +53 -34
- package/app/components/AddToWishlist.vue +0 -3
- 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/LoginForm.vue +1 -4
- 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/layouts/account.vue +0 -1
- package/app/pages/anmelden.vue +8 -4
- 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 +17 -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 +41 -23
- 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/app/middleware/trailing-slash.global.ts +0 -19
- package/server/utils/shopware/adminApiClient.ts +0 -24
- package/test/unit/sales-channels.test.ts +0 -66
|
@@ -2,6 +2,14 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
2
2
|
import { mountSuspended, mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import ContactForm from "~/components/Contact/Form.vue";
|
|
4
4
|
|
|
5
|
+
type ContactFormVm = {
|
|
6
|
+
submitted: boolean;
|
|
7
|
+
successMessage: string;
|
|
8
|
+
onSubmit: (arg: {
|
|
9
|
+
data: Record<string, string | undefined>;
|
|
10
|
+
}) => Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
|
|
5
13
|
const { mockInvoke } = vi.hoisted(() => {
|
|
6
14
|
return {
|
|
7
15
|
mockInvoke: vi.fn(),
|
|
@@ -52,7 +60,9 @@ describe("ContactForm", () => {
|
|
|
52
60
|
comment: "Ich habe eine Frage zu Ihrem Produkt.",
|
|
53
61
|
};
|
|
54
62
|
|
|
55
|
-
await (wrapper.vm as
|
|
63
|
+
await (wrapper.vm as unknown as ContactFormVm).onSubmit({
|
|
64
|
+
data: validData,
|
|
65
|
+
});
|
|
56
66
|
|
|
57
67
|
expect(mockInvoke).toHaveBeenCalled();
|
|
58
68
|
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
@@ -63,8 +73,10 @@ describe("ContactForm", () => {
|
|
|
63
73
|
);
|
|
64
74
|
|
|
65
75
|
// Verify submitted state
|
|
66
|
-
expect((wrapper.vm as
|
|
67
|
-
expect((wrapper.vm as
|
|
76
|
+
expect((wrapper.vm as unknown as ContactFormVm).submitted).toBe(true);
|
|
77
|
+
expect((wrapper.vm as unknown as ContactFormVm).successMessage).toBe(
|
|
78
|
+
customMessage,
|
|
79
|
+
);
|
|
68
80
|
|
|
69
81
|
// Wait for DOM update
|
|
70
82
|
await nextTick();
|
|
@@ -76,7 +88,7 @@ describe("ContactForm", () => {
|
|
|
76
88
|
|
|
77
89
|
// Click on "Weiteres Formular senden"
|
|
78
90
|
await wrapper.find("button").trigger("click");
|
|
79
|
-
expect((wrapper.vm as
|
|
91
|
+
expect((wrapper.vm as unknown as ContactFormVm).submitted).toBe(false);
|
|
80
92
|
|
|
81
93
|
await nextTick();
|
|
82
94
|
expect(wrapper.find("form").exists()).toBe(true);
|
|
@@ -102,10 +114,14 @@ describe("ContactForm", () => {
|
|
|
102
114
|
comment: "Ich habe eine Frage zu Ihrem Produkt.",
|
|
103
115
|
};
|
|
104
116
|
|
|
105
|
-
await (wrapper.vm as
|
|
117
|
+
await (wrapper.vm as unknown as ContactFormVm).onSubmit({
|
|
118
|
+
data: validData,
|
|
119
|
+
});
|
|
106
120
|
|
|
107
121
|
const defaultMessage = "Deine Nachricht wurde erfolgreich versendet.";
|
|
108
|
-
expect((wrapper.vm as
|
|
122
|
+
expect((wrapper.vm as unknown as ContactFormVm).successMessage).toBe(
|
|
123
|
+
defaultMessage,
|
|
124
|
+
);
|
|
109
125
|
|
|
110
126
|
await nextTick();
|
|
111
127
|
expect(wrapper.text()).toContain(defaultMessage);
|
|
@@ -127,7 +143,9 @@ describe("ContactForm", () => {
|
|
|
127
143
|
comment: "Dies ist ein Test mit nur Pflichtfeldern.",
|
|
128
144
|
};
|
|
129
145
|
|
|
130
|
-
await (wrapper.vm as
|
|
146
|
+
await (wrapper.vm as unknown as ContactFormVm).onSubmit({
|
|
147
|
+
data: mandatoryDataOnly,
|
|
148
|
+
});
|
|
131
149
|
|
|
132
150
|
expect(mockInvoke).toHaveBeenCalledWith(
|
|
133
151
|
"sendContactMail post /contact-form",
|
|
@@ -140,7 +158,7 @@ describe("ContactForm", () => {
|
|
|
140
158
|
},
|
|
141
159
|
);
|
|
142
160
|
|
|
143
|
-
expect((wrapper.vm as
|
|
161
|
+
expect((wrapper.vm as unknown as ContactFormVm).submitted).toBe(true);
|
|
144
162
|
});
|
|
145
163
|
|
|
146
164
|
it("shows error toast when submission fails", async () => {
|
|
@@ -158,7 +176,9 @@ describe("ContactForm", () => {
|
|
|
158
176
|
comment: "Bitte um Unterstützung.",
|
|
159
177
|
};
|
|
160
178
|
|
|
161
|
-
await (wrapper.vm as
|
|
179
|
+
await (wrapper.vm as unknown as ContactFormVm).onSubmit({
|
|
180
|
+
data: validData,
|
|
181
|
+
});
|
|
162
182
|
|
|
163
183
|
expect(mockInvoke).toHaveBeenCalled();
|
|
164
184
|
|
|
@@ -180,8 +200,8 @@ describe("ContactForm", () => {
|
|
|
180
200
|
comment: "This is spam.",
|
|
181
201
|
hp: "I am a bot",
|
|
182
202
|
};
|
|
183
|
-
await (wrapper.vm as
|
|
203
|
+
await (wrapper.vm as unknown as ContactFormVm).onSubmit({ data: botData });
|
|
184
204
|
expect(mockInvoke).not.toHaveBeenCalled();
|
|
185
|
-
expect((wrapper.vm as
|
|
205
|
+
expect((wrapper.vm as unknown as ContactFormVm).submitted).toBe(true);
|
|
186
206
|
});
|
|
187
207
|
});
|
|
@@ -7,7 +7,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
7
7
|
state: {
|
|
8
8
|
isLoggedIn: false,
|
|
9
9
|
isGuestSession: false,
|
|
10
|
-
user: null as
|
|
10
|
+
user: null as { firstName: string; lastName: string } | null,
|
|
11
11
|
},
|
|
12
12
|
toastAddCalled: { value: false },
|
|
13
13
|
}));
|
|
@@ -25,9 +25,13 @@ mockNuxtImport("useUser", () => () => ({
|
|
|
25
25
|
}));
|
|
26
26
|
|
|
27
27
|
mockNuxtImport("useToast", () => () => ({
|
|
28
|
-
add: (
|
|
29
|
-
if (
|
|
30
|
-
|
|
28
|
+
add: (_payload: unknown) => {
|
|
29
|
+
if (
|
|
30
|
+
typeof global !== "undefined" &&
|
|
31
|
+
(global as Record<string, { value: boolean }>).toastAddCalled
|
|
32
|
+
) {
|
|
33
|
+
(global as Record<string, { value: boolean }>).toastAddCalled!.value =
|
|
34
|
+
true;
|
|
31
35
|
}
|
|
32
36
|
},
|
|
33
37
|
}));
|
|
@@ -42,10 +46,15 @@ mockNuxtImport("useCart", () => () => ({
|
|
|
42
46
|
|
|
43
47
|
mockNuxtImport("useRuntimeConfig", () => () => ({
|
|
44
48
|
app: { baseURL: "/" },
|
|
49
|
+
public: { shopware: {} },
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
mockNuxtImport("useWishlist", () => () => ({
|
|
53
|
+
count: ref(0),
|
|
45
54
|
}));
|
|
46
55
|
|
|
47
56
|
// Mock Nuxt Content queryCollection
|
|
48
|
-
mockNuxtImport("queryCollection", () => (
|
|
57
|
+
mockNuxtImport("queryCollection", () => (_collection: string) => ({
|
|
49
58
|
first: () =>
|
|
50
59
|
Promise.resolve({
|
|
51
60
|
account: {
|
|
@@ -74,7 +83,7 @@ describe("HeaderRight", () => {
|
|
|
74
83
|
reactiveState.isGuestSession = false;
|
|
75
84
|
reactiveState.user = null;
|
|
76
85
|
mocks.toastAddCalled.value = false;
|
|
77
|
-
(global as
|
|
86
|
+
(global as Record<string, unknown>).toastAddCalled = mocks.toastAddCalled;
|
|
78
87
|
vi.clearAllMocks();
|
|
79
88
|
});
|
|
80
89
|
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
2
|
+
import { mountSuspended, mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import { ref } from "vue";
|
|
3
4
|
import LoginForm from "@/components/User/LoginForm.vue";
|
|
4
|
-
import { useUser } from "@shopware/composables";
|
|
5
5
|
import { ApiClientError } from "@shopware/api-client";
|
|
6
6
|
|
|
7
|
+
type LoginFormVm = {
|
|
8
|
+
onSubmit: (arg: {
|
|
9
|
+
data: { email: string; password: string };
|
|
10
|
+
}) => Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
|
|
7
13
|
vi.mock("@shopware/api-client", () => ({
|
|
8
14
|
ApiClientError: class extends Error {
|
|
9
|
-
details:
|
|
10
|
-
constructor(details:
|
|
15
|
+
details: unknown;
|
|
16
|
+
constructor(details: unknown) {
|
|
11
17
|
super("ApiClientError");
|
|
12
18
|
this.details = details;
|
|
13
19
|
}
|
|
@@ -16,9 +22,7 @@ vi.mock("@shopware/api-client", () => ({
|
|
|
16
22
|
|
|
17
23
|
vi.mock("@shopware/composables", () => ({
|
|
18
24
|
useUser: vi.fn(),
|
|
19
|
-
useWishlist: vi.fn(
|
|
20
|
-
mergeWishlistProducts: vi.fn(),
|
|
21
|
-
})),
|
|
25
|
+
useWishlist: vi.fn(),
|
|
22
26
|
}));
|
|
23
27
|
|
|
24
28
|
const mockToastAdd = vi.fn();
|
|
@@ -28,19 +32,30 @@ mockNuxtImport("useToast", () => {
|
|
|
28
32
|
});
|
|
29
33
|
});
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
const loginMock = vi.fn();
|
|
36
|
+
const userMock = ref({ firstName: "John", lastName: "Doe" });
|
|
37
|
+
const isLoggedInMock = ref(false);
|
|
38
|
+
|
|
39
|
+
mockNuxtImport("useUser", () => {
|
|
40
|
+
return () => ({
|
|
41
|
+
isLoggedIn: isLoggedInMock,
|
|
42
|
+
login: loginMock,
|
|
43
|
+
user: userMock,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
mockNuxtImport("useWishlist", () => {
|
|
48
|
+
return () => ({
|
|
49
|
+
mergeWishlistProducts: vi.fn(),
|
|
50
|
+
});
|
|
51
|
+
});
|
|
34
52
|
|
|
53
|
+
describe("LoginForm", () => {
|
|
35
54
|
beforeEach(() => {
|
|
36
55
|
vi.clearAllMocks();
|
|
37
|
-
loginMock
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
isLoggedIn: { value: false },
|
|
41
|
-
login: loginMock,
|
|
42
|
-
user: userMock,
|
|
43
|
-
});
|
|
56
|
+
loginMock.mockReset();
|
|
57
|
+
isLoggedInMock.value = false;
|
|
58
|
+
userMock.value = { firstName: "John", lastName: "Doe" };
|
|
44
59
|
});
|
|
45
60
|
|
|
46
61
|
it("should show success toast on successful login", async () => {
|
|
@@ -48,7 +63,7 @@ describe("LoginForm", () => {
|
|
|
48
63
|
const wrapper = await mountSuspended(LoginForm);
|
|
49
64
|
|
|
50
65
|
// Call onSubmit directly if form trigger is not working in test
|
|
51
|
-
await (wrapper.vm as
|
|
66
|
+
await (wrapper.vm as unknown as LoginFormVm).onSubmit({
|
|
52
67
|
data: {
|
|
53
68
|
email: "test@example.com",
|
|
54
69
|
password: "password123",
|
|
@@ -63,6 +78,7 @@ describe("LoginForm", () => {
|
|
|
63
78
|
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
64
79
|
expect.objectContaining({
|
|
65
80
|
title: "Hallo John Doe!",
|
|
81
|
+
description: "Erfolgreich angemeldet.",
|
|
66
82
|
color: "success",
|
|
67
83
|
}),
|
|
68
84
|
);
|
|
@@ -73,7 +89,7 @@ describe("LoginForm", () => {
|
|
|
73
89
|
const wrapper = await mountSuspended(LoginForm);
|
|
74
90
|
|
|
75
91
|
// Call onSubmit directly if form trigger is not working in test
|
|
76
|
-
await (wrapper.vm as
|
|
92
|
+
await (wrapper.vm as unknown as LoginFormVm).onSubmit({
|
|
77
93
|
data: {
|
|
78
94
|
email: "test@example.com",
|
|
79
95
|
password: "wrongpassword",
|
|
@@ -98,11 +114,11 @@ describe("LoginForm", () => {
|
|
|
98
114
|
detail: 'The email address "test@example.com" is already in use',
|
|
99
115
|
},
|
|
100
116
|
],
|
|
101
|
-
});
|
|
117
|
+
} as unknown as ConstructorParameters<typeof ApiClientError>[0]);
|
|
102
118
|
loginMock.mockRejectedValueOnce(apiClientError);
|
|
103
119
|
const wrapper = await mountSuspended(LoginForm);
|
|
104
120
|
|
|
105
|
-
await (wrapper.vm as
|
|
121
|
+
await (wrapper.vm as unknown as LoginFormVm).onSubmit({
|
|
106
122
|
data: {
|
|
107
123
|
email: "test@example.com",
|
|
108
124
|
password: "password123",
|
|
@@ -119,11 +135,13 @@ describe("LoginForm", () => {
|
|
|
119
135
|
});
|
|
120
136
|
|
|
121
137
|
it("should handle ApiClientError with missing errors gracefully", async () => {
|
|
122
|
-
const apiClientError = new ApiClientError(
|
|
138
|
+
const apiClientError = new ApiClientError(
|
|
139
|
+
{} as unknown as ConstructorParameters<typeof ApiClientError>[0],
|
|
140
|
+
);
|
|
123
141
|
loginMock.mockRejectedValueOnce(apiClientError);
|
|
124
142
|
const wrapper = await mountSuspended(LoginForm);
|
|
125
143
|
|
|
126
|
-
await (wrapper.vm as
|
|
144
|
+
await (wrapper.vm as unknown as LoginFormVm).onSubmit({
|
|
127
145
|
data: {
|
|
128
146
|
email: "test@example.com",
|
|
129
147
|
password: "password123",
|
|
@@ -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", () => {
|