@shopbite-de/storefront 1.5.3 → 1.6.1
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/.env.example +6 -1
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/app/components/Address/Fields.vue +128 -0
- package/app/components/Checkout/PaymentAndDelivery.vue +49 -27
- package/app/components/Header.vue +26 -13
- package/app/components/User/LoginForm.vue +32 -11
- package/app/components/User/RegistrationForm.vue +105 -180
- package/app/composables/useAddressAutocomplete.ts +84 -0
- package/app/composables/useAddressValidation.ts +95 -0
- package/app/composables/useValidCitiesForDelivery.ts +12 -0
- package/app/pages/[...all].vue +28 -0
- package/app/pages/index.vue +4 -0
- package/app/pages/menu/[...all].vue +52 -0
- package/app/validation/registrationSchema.ts +105 -124
- package/content/{unternehmen/impressum.md → impressum.md} +5 -0
- package/content/index.yml +2 -1
- package/content/unternehmen/agb.md +5 -0
- package/content/unternehmen/datenschutz.md +5 -0
- package/content/unternehmen/zahlung-und-versand.md +34 -0
- package/content.config.ts +6 -2
- package/eslint.config.mjs +5 -3
- package/nuxt.config.ts +33 -22
- package/package.json +2 -1
- package/public/card.png +0 -0
- package/server/api/address/autocomplete.get.ts +33 -0
- package/test/nuxt/AddressFields.test.ts +284 -0
- package/test/nuxt/Header.test.ts +124 -0
- package/test/nuxt/LoginForm.test.ts +141 -0
- package/test/nuxt/PaymentAndDelivery.test.ts +78 -0
- package/test/nuxt/RegistrationForm.test.ts +255 -0
- package/test/nuxt/RegistrationValidation.test.ts +39 -0
- package/test/nuxt/registrationSchema.test.ts +242 -0
- package/test/nuxt/useAddressAutocomplete.test.ts +161 -0
- package/app/pages/unternehmen/[slug].vue +0 -66
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { reactive, ref } from "vue";
|
|
3
|
+
import { mountSuspended, mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
4
|
+
import Fields from "~/components/Address/Fields.vue";
|
|
5
|
+
|
|
6
|
+
const { mockGetSuggestions } = vi.hoisted(() => ({
|
|
7
|
+
mockGetSuggestions: vi.fn().mockResolvedValue([]),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
mockNuxtImport("useAddressAutocomplete", () => () => ({
|
|
11
|
+
getSuggestions: mockGetSuggestions,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe("AddressFields", () => {
|
|
15
|
+
const defaultProps = {
|
|
16
|
+
modelValue: {
|
|
17
|
+
firstName: "",
|
|
18
|
+
lastName: "",
|
|
19
|
+
street: "",
|
|
20
|
+
zipcode: "",
|
|
21
|
+
city: "",
|
|
22
|
+
phoneNumber: "",
|
|
23
|
+
},
|
|
24
|
+
prefix: "billingAddress",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
it("renders basic address fields", async () => {
|
|
28
|
+
const wrapper = await mountSuspended(Fields, {
|
|
29
|
+
props: defaultProps,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(wrapper.find('input[name="billingAddress.street"]').exists()).toBe(
|
|
33
|
+
true,
|
|
34
|
+
);
|
|
35
|
+
expect(wrapper.find('input[name="billingAddress.zipcode"]').exists()).toBe(
|
|
36
|
+
true,
|
|
37
|
+
);
|
|
38
|
+
expect(wrapper.find('input[name="billingAddress.city"]').exists()).toBe(
|
|
39
|
+
true,
|
|
40
|
+
);
|
|
41
|
+
expect(
|
|
42
|
+
wrapper.find('input[name="billingAddress.phoneNumber"]').exists(),
|
|
43
|
+
).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders name fields when showNames is true", async () => {
|
|
47
|
+
const wrapper = await mountSuspended(Fields, {
|
|
48
|
+
props: {
|
|
49
|
+
...defaultProps,
|
|
50
|
+
showNames: true,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(
|
|
55
|
+
wrapper.find('input[name="billingAddress.firstName"]').exists(),
|
|
56
|
+
).toBe(true);
|
|
57
|
+
expect(wrapper.find('input[name="billingAddress.lastName"]').exists()).toBe(
|
|
58
|
+
true,
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("does not render name fields when showNames is false", async () => {
|
|
63
|
+
const wrapper = await mountSuspended(Fields, {
|
|
64
|
+
props: {
|
|
65
|
+
...defaultProps,
|
|
66
|
+
showNames: false,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(
|
|
71
|
+
wrapper.find('input[name="billingAddress.firstName"]').exists(),
|
|
72
|
+
).toBe(false);
|
|
73
|
+
expect(wrapper.find('input[name="billingAddress.lastName"]').exists()).toBe(
|
|
74
|
+
false,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("renders business fields when accountType is business", async () => {
|
|
79
|
+
const wrapper = await mountSuspended(Fields, {
|
|
80
|
+
props: {
|
|
81
|
+
...defaultProps,
|
|
82
|
+
accountType: "business",
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(wrapper.find('input[name="billingAddress.company"]').exists()).toBe(
|
|
87
|
+
true,
|
|
88
|
+
);
|
|
89
|
+
expect(
|
|
90
|
+
wrapper.find('input[name="billingAddress.department"]').exists(),
|
|
91
|
+
).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("updates modelValue when input changes", async () => {
|
|
95
|
+
const modelValue = reactive({
|
|
96
|
+
firstName: "",
|
|
97
|
+
lastName: "",
|
|
98
|
+
street: "",
|
|
99
|
+
zipcode: "",
|
|
100
|
+
city: "",
|
|
101
|
+
phoneNumber: "",
|
|
102
|
+
});
|
|
103
|
+
const wrapper = await mountSuspended(Fields, {
|
|
104
|
+
props: {
|
|
105
|
+
...defaultProps,
|
|
106
|
+
modelValue,
|
|
107
|
+
"onUpdate:modelValue": (val: any) => Object.assign(modelValue, val),
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await wrapper
|
|
112
|
+
.find('input[name="billingAddress.street"]')
|
|
113
|
+
.setValue("New Street");
|
|
114
|
+
expect(modelValue.street).toBe("New Street");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("shows warning when an invalid city is selected for shipping address", async () => {
|
|
118
|
+
const modelValue = reactive({
|
|
119
|
+
firstName: "John",
|
|
120
|
+
lastName: "Doe",
|
|
121
|
+
street: "Main St 1",
|
|
122
|
+
zipcode: "12345",
|
|
123
|
+
city: "InvalidCity",
|
|
124
|
+
phoneNumber: "12345678",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const wrapper = await mountSuspended(Fields, {
|
|
128
|
+
props: {
|
|
129
|
+
...defaultProps,
|
|
130
|
+
modelValue,
|
|
131
|
+
isShipping: true,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(wrapper.text()).toContain(
|
|
136
|
+
"An diese Adresse können wir leider nicht liefern.",
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("does not show warning for valid city in shipping address", async () => {
|
|
141
|
+
const modelValue = reactive({
|
|
142
|
+
firstName: "John",
|
|
143
|
+
lastName: "Doe",
|
|
144
|
+
street: "Main St 1",
|
|
145
|
+
zipcode: "63179",
|
|
146
|
+
city: "Obertshausen",
|
|
147
|
+
phoneNumber: "12345678",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const wrapper = await mountSuspended(Fields, {
|
|
151
|
+
props: {
|
|
152
|
+
...defaultProps,
|
|
153
|
+
modelValue,
|
|
154
|
+
isShipping: true,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(wrapper.text()).not.toContain(
|
|
159
|
+
"An diese Adresse können wir leider nicht liefern.",
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("does not show warning for billing address even if city is 'invalid'", async () => {
|
|
164
|
+
const modelValue = reactive({
|
|
165
|
+
firstName: "John",
|
|
166
|
+
lastName: "Doe",
|
|
167
|
+
street: "Main St 1",
|
|
168
|
+
zipcode: "12345",
|
|
169
|
+
city: "InvalidCity",
|
|
170
|
+
phoneNumber: "12345678",
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const wrapper = await mountSuspended(Fields, {
|
|
174
|
+
props: {
|
|
175
|
+
...defaultProps,
|
|
176
|
+
modelValue,
|
|
177
|
+
isShipping: false,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(wrapper.text()).not.toContain(
|
|
182
|
+
"An diese Adresse können wir leider nicht liefern.",
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("shows correction automatically on input after debounce", async () => {
|
|
187
|
+
mockGetSuggestions.mockResolvedValue([
|
|
188
|
+
{
|
|
189
|
+
street: "Corrected Street 123",
|
|
190
|
+
city: "Corrected City",
|
|
191
|
+
zipcode: "54321",
|
|
192
|
+
label: "Corrected Street 123, 54321 Corrected City",
|
|
193
|
+
},
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
const modelValue = reactive({
|
|
197
|
+
firstName: "John",
|
|
198
|
+
lastName: "Doe",
|
|
199
|
+
street: "Musterstr 1",
|
|
200
|
+
zipcode: "12345",
|
|
201
|
+
city: "Musterstadt",
|
|
202
|
+
phoneNumber: "12345678",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const wrapper = await mountSuspended(Fields, {
|
|
206
|
+
props: {
|
|
207
|
+
...defaultProps,
|
|
208
|
+
modelValue,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Change input to trigger watch
|
|
213
|
+
await wrapper
|
|
214
|
+
.find('input[name="billingAddress.street"]')
|
|
215
|
+
.setValue("Musterstr 2");
|
|
216
|
+
|
|
217
|
+
// Wait for debounce and async check
|
|
218
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
219
|
+
|
|
220
|
+
expect(wrapper.text()).toContain(
|
|
221
|
+
"Meinten Sie: Corrected Street 123, 54321 Corrected City?",
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("shows correction when checkAddress is called", async () => {
|
|
226
|
+
const modelValue = reactive({
|
|
227
|
+
firstName: "John",
|
|
228
|
+
lastName: "Doe",
|
|
229
|
+
street: "Musterstr 1",
|
|
230
|
+
zipcode: "12345",
|
|
231
|
+
city: "Musterstadt",
|
|
232
|
+
phoneNumber: "12345678",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const wrapper = await mountSuspended(Fields, {
|
|
236
|
+
props: {
|
|
237
|
+
...defaultProps,
|
|
238
|
+
modelValue,
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Mock getSuggestions to return a correction
|
|
243
|
+
// Note: In a real test we might need to mock useAddressAutocomplete
|
|
244
|
+
// But let's see if we can just call the exposed method
|
|
245
|
+
|
|
246
|
+
// For this to work reliably in test, we might need to mock useAddressAutocomplete
|
|
247
|
+
// since it's used inside checkAddress
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("shows correction automatically on input after debounce even for short street names", async () => {
|
|
251
|
+
mockGetSuggestions.mockResolvedValueOnce([
|
|
252
|
+
{
|
|
253
|
+
street: "B 1",
|
|
254
|
+
city: "Berlin",
|
|
255
|
+
zipcode: "10115",
|
|
256
|
+
label: "B 1, 10115 Berlin",
|
|
257
|
+
},
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
const modelValue = reactive({
|
|
261
|
+
firstName: "John",
|
|
262
|
+
lastName: "Doe",
|
|
263
|
+
street: "",
|
|
264
|
+
zipcode: "10115",
|
|
265
|
+
city: "Berlin",
|
|
266
|
+
phoneNumber: "12345678",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const wrapper = await mountSuspended(Fields, {
|
|
270
|
+
props: {
|
|
271
|
+
...defaultProps,
|
|
272
|
+
modelValue,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Change input to trigger watch
|
|
277
|
+
await wrapper.find('input[name="billingAddress.street"]').setValue("B1");
|
|
278
|
+
|
|
279
|
+
// Wait for debounce and async check
|
|
280
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
281
|
+
|
|
282
|
+
expect(wrapper.text()).toContain("Meinten Sie: B 1, 10115 Berlin?");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import { mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
4
|
+
import Header from "~/components/Header.vue";
|
|
5
|
+
import { ref, reactive, toRefs } from "vue";
|
|
6
|
+
|
|
7
|
+
const mocks = vi.hoisted(() => ({
|
|
8
|
+
state: {
|
|
9
|
+
isLoggedIn: false,
|
|
10
|
+
isGuestSession: false,
|
|
11
|
+
user: null as any,
|
|
12
|
+
},
|
|
13
|
+
logout: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const reactiveState = reactive(mocks.state);
|
|
17
|
+
|
|
18
|
+
mockNuxtImport("useUser", () => () => ({
|
|
19
|
+
...toRefs(reactiveState),
|
|
20
|
+
logout: mocks.logout,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
mockNuxtImport("useShopBiteConfig", () => () => ({
|
|
24
|
+
isCheckoutEnabled: ref(true),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
mockNuxtImport("useCart", () => () => ({
|
|
28
|
+
count: ref(0),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
mockNuxtImport("useRuntimeConfig", () => () => ({
|
|
32
|
+
app: {
|
|
33
|
+
baseURL: "/",
|
|
34
|
+
},
|
|
35
|
+
public: {
|
|
36
|
+
site: {
|
|
37
|
+
name: "ShopBite",
|
|
38
|
+
},
|
|
39
|
+
shopware: {
|
|
40
|
+
devStorefrontUrl: "http://localhost:3000",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Mock Nuxt Content queryCollection
|
|
46
|
+
mockNuxtImport("queryCollection", () => (collection: string) => ({
|
|
47
|
+
first: () =>
|
|
48
|
+
Promise.resolve({
|
|
49
|
+
main: [],
|
|
50
|
+
account: {
|
|
51
|
+
loggedIn: [
|
|
52
|
+
[{ label: "Mein Konto", type: "label" }],
|
|
53
|
+
[
|
|
54
|
+
{ label: "Konto", icon: "i-lucide-user", to: "/konto" },
|
|
55
|
+
{
|
|
56
|
+
label: "Bestellungen",
|
|
57
|
+
icon: "i-lucide-pizza",
|
|
58
|
+
to: "/konto/bestellungen",
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
[{ label: "Abmelden", icon: "i-lucide-log-out", action: "logout" }],
|
|
62
|
+
],
|
|
63
|
+
loggedOut: [
|
|
64
|
+
[{ label: "Jetzt anmelden", type: "label" }],
|
|
65
|
+
[{ label: "Zur Anmeldung", icon: "i-lucide-user", to: "/anmelden" }],
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
describe("Header", () => {
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
reactiveState.isLoggedIn = false;
|
|
74
|
+
reactiveState.isGuestSession = false;
|
|
75
|
+
reactiveState.user = null;
|
|
76
|
+
vi.clearAllMocks();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("renders correctly when logged out", async () => {
|
|
80
|
+
const component = await mountSuspended(Header);
|
|
81
|
+
// Should show user icon
|
|
82
|
+
expect(component.html()).toContain("i-lucide:user");
|
|
83
|
+
// Should NOT show checkmark chip
|
|
84
|
+
expect(component.html()).not.toContain("✓");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("renders correctly when logged in", async () => {
|
|
88
|
+
reactiveState.isLoggedIn = true;
|
|
89
|
+
reactiveState.user = { firstName: "John", lastName: "Doe" };
|
|
90
|
+
const component = await mountSuspended(Header);
|
|
91
|
+
|
|
92
|
+
// Should show checkmark chip
|
|
93
|
+
expect(component.html()).toContain("✓");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("renders correctly for guest session", async () => {
|
|
97
|
+
reactiveState.isGuestSession = true;
|
|
98
|
+
reactiveState.user = { firstName: "Guest", lastName: "User" };
|
|
99
|
+
const component = await mountSuspended(Header);
|
|
100
|
+
|
|
101
|
+
// Should show checkmark chip for guest session too
|
|
102
|
+
expect(component.html()).toContain("✓");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("filters menu items for guest session", async () => {
|
|
106
|
+
reactiveState.isGuestSession = true;
|
|
107
|
+
reactiveState.user = { firstName: "Guest", lastName: "User" };
|
|
108
|
+
const component = await mountSuspended(Header);
|
|
109
|
+
|
|
110
|
+
// Find the dropdown menu
|
|
111
|
+
const dropdown = component.findComponent({ name: "UDropdownMenu" });
|
|
112
|
+
expect(dropdown.exists()).toBe(true);
|
|
113
|
+
|
|
114
|
+
const items = dropdown.props("items");
|
|
115
|
+
|
|
116
|
+
// Group 0 should have the user label
|
|
117
|
+
// Group 1 should be gone (filtered out because it only contained links)
|
|
118
|
+
// Group 2 (now index 1) should have logout
|
|
119
|
+
expect(items.length).toBe(2);
|
|
120
|
+
expect(items[0][0].label).toBe("Guest User");
|
|
121
|
+
expect(items[1][0].label).toBe("Abmelden");
|
|
122
|
+
expect(items[1][0].onSelect).toBeDefined();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import LoginForm from "@/components/User/LoginForm.vue";
|
|
4
|
+
import { useUser } from "@shopware/composables";
|
|
5
|
+
import { ApiClientError } from "@shopware/api-client";
|
|
6
|
+
|
|
7
|
+
vi.mock("@shopware/api-client", () => ({
|
|
8
|
+
ApiClientError: class extends Error {
|
|
9
|
+
details: any;
|
|
10
|
+
constructor(details: any) {
|
|
11
|
+
super("ApiClientError");
|
|
12
|
+
this.details = details;
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("@shopware/composables", () => ({
|
|
18
|
+
useUser: vi.fn(),
|
|
19
|
+
useWishlist: vi.fn(() => ({
|
|
20
|
+
mergeWishlistProducts: vi.fn(),
|
|
21
|
+
})),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const mockToastAdd = vi.fn();
|
|
25
|
+
mockNuxtImport("useToast", () => {
|
|
26
|
+
return () => ({
|
|
27
|
+
add: mockToastAdd,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("LoginForm", () => {
|
|
32
|
+
let loginMock: any;
|
|
33
|
+
let userMock: any;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
loginMock = vi.fn();
|
|
38
|
+
userMock = { value: { firstName: "John", lastName: "Doe" } };
|
|
39
|
+
(useUser as any).mockReturnValue({
|
|
40
|
+
isLoggedIn: { value: false },
|
|
41
|
+
login: loginMock,
|
|
42
|
+
user: userMock,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should show success toast on successful login", async () => {
|
|
47
|
+
loginMock.mockResolvedValueOnce({});
|
|
48
|
+
const wrapper = await mountSuspended(LoginForm);
|
|
49
|
+
|
|
50
|
+
// Call onSubmit directly if form trigger is not working in test
|
|
51
|
+
await (wrapper.vm as any).onSubmit({
|
|
52
|
+
data: {
|
|
53
|
+
email: "test@example.com",
|
|
54
|
+
password: "password123",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(loginMock).toHaveBeenCalledWith({
|
|
59
|
+
username: "test@example.com",
|
|
60
|
+
password: "password123",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
64
|
+
expect.objectContaining({
|
|
65
|
+
title: "Hallo John Doe!",
|
|
66
|
+
color: "success",
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should show error toast on failed login", async () => {
|
|
72
|
+
loginMock.mockRejectedValueOnce(new Error("Invalid credentials"));
|
|
73
|
+
const wrapper = await mountSuspended(LoginForm);
|
|
74
|
+
|
|
75
|
+
// Call onSubmit directly if form trigger is not working in test
|
|
76
|
+
await (wrapper.vm as any).onSubmit({
|
|
77
|
+
data: {
|
|
78
|
+
email: "test@example.com",
|
|
79
|
+
password: "wrongpassword",
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(loginMock).toHaveBeenCalled();
|
|
84
|
+
|
|
85
|
+
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
86
|
+
expect.objectContaining({
|
|
87
|
+
title: "Login fehlgeschlagen",
|
|
88
|
+
description: "Bitte überprüfen Sie Ihre Zugangsdaten.",
|
|
89
|
+
color: "error",
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should show detailed error toast on ApiClientError", async () => {
|
|
95
|
+
const apiClientError = new ApiClientError({
|
|
96
|
+
errors: [
|
|
97
|
+
{
|
|
98
|
+
detail: 'The email address "test@example.com" is already in use',
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
loginMock.mockRejectedValueOnce(apiClientError);
|
|
103
|
+
const wrapper = await mountSuspended(LoginForm);
|
|
104
|
+
|
|
105
|
+
await (wrapper.vm as any).onSubmit({
|
|
106
|
+
data: {
|
|
107
|
+
email: "test@example.com",
|
|
108
|
+
password: "password123",
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
113
|
+
expect.objectContaining({
|
|
114
|
+
title: "Login fehlgeschlagen",
|
|
115
|
+
description: 'The email address "test@example.com" is already in use',
|
|
116
|
+
color: "error",
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should handle ApiClientError with missing errors gracefully", async () => {
|
|
122
|
+
const apiClientError = new ApiClientError({});
|
|
123
|
+
loginMock.mockRejectedValueOnce(apiClientError);
|
|
124
|
+
const wrapper = await mountSuspended(LoginForm);
|
|
125
|
+
|
|
126
|
+
await (wrapper.vm as any).onSubmit({
|
|
127
|
+
data: {
|
|
128
|
+
email: "test@example.com",
|
|
129
|
+
password: "password123",
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
134
|
+
expect.objectContaining({
|
|
135
|
+
title: "Login fehlgeschlagen",
|
|
136
|
+
description: "Bitte überprüfen Sie Ihre Zugangsdaten.",
|
|
137
|
+
color: "error",
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { mountSuspended, mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import PaymentAndDelivery from "~/components/Checkout/PaymentAndDelivery.vue";
|
|
4
|
+
import { ref } from "vue";
|
|
5
|
+
|
|
6
|
+
const { mockSetPaymentMethod, mockSetShippingMethod, mockRefreshCart } =
|
|
7
|
+
vi.hoisted(() => ({
|
|
8
|
+
mockSetPaymentMethod: vi.fn(),
|
|
9
|
+
mockSetShippingMethod: vi.fn(),
|
|
10
|
+
mockRefreshCart: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
mockNuxtImport("useCheckout", () => () => ({
|
|
14
|
+
paymentMethods: ref([
|
|
15
|
+
{ id: "pm1", distinguishableName: "Payment 1" },
|
|
16
|
+
{ id: "pm2", distinguishableName: "Payment 2" },
|
|
17
|
+
]),
|
|
18
|
+
shippingMethods: ref([
|
|
19
|
+
{ id: "sm1", name: "Shipping 1" },
|
|
20
|
+
{ id: "sm2", name: "Shipping 2" },
|
|
21
|
+
]),
|
|
22
|
+
selectedPaymentMethod: ref({ id: "pm1", distinguishableName: "Payment 1" }),
|
|
23
|
+
selectedShippingMethod: ref({ id: "sm1", name: "Shipping 1" }),
|
|
24
|
+
setPaymentMethod: mockSetPaymentMethod,
|
|
25
|
+
setShippingMethod: mockSetShippingMethod,
|
|
26
|
+
getPaymentMethods: vi.fn(),
|
|
27
|
+
getShippingMethods: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
mockNuxtImport("useCart", () => () => ({
|
|
31
|
+
refreshCart: mockRefreshCart,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
mockNuxtImport("useToast", () => () => ({
|
|
35
|
+
add: vi.fn(),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
describe("PaymentAndDelivery", () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.clearAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("renders correctly", async () => {
|
|
44
|
+
const wrapper = await mountSuspended(PaymentAndDelivery);
|
|
45
|
+
expect(wrapper.exists()).toBeTruthy();
|
|
46
|
+
expect(wrapper.text()).toContain("Zahlungsarten");
|
|
47
|
+
expect(wrapper.text()).toContain("Versandarten");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("updates payment method when changed", async () => {
|
|
51
|
+
const wrapper = await mountSuspended(PaymentAndDelivery);
|
|
52
|
+
|
|
53
|
+
// @ts-ignore - access internal state
|
|
54
|
+
wrapper.vm.selectedPaymentMethodId = "pm2";
|
|
55
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
56
|
+
|
|
57
|
+
expect(mockSetPaymentMethod).toHaveBeenCalledWith({ id: "pm2" });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("updates shipping method and refreshes cart when changed", async () => {
|
|
61
|
+
const wrapper = await mountSuspended(PaymentAndDelivery);
|
|
62
|
+
|
|
63
|
+
// @ts-ignore - access internal state
|
|
64
|
+
wrapper.vm.selectedShippingMethodId = "sm2";
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
66
|
+
|
|
67
|
+
expect(mockSetShippingMethod).toHaveBeenCalledWith({ id: "sm2" });
|
|
68
|
+
expect(mockRefreshCart).toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("renders help buttons with correct links", async () => {
|
|
72
|
+
const wrapper = await mountSuspended(PaymentAndDelivery);
|
|
73
|
+
const helpButtons = wrapper.findAll(
|
|
74
|
+
'a[href="/unternehmen/zahlung-und-versand"]',
|
|
75
|
+
);
|
|
76
|
+
expect(helpButtons.length).toBe(2);
|
|
77
|
+
});
|
|
78
|
+
});
|