@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,255 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { ref, nextTick } from "vue";
|
|
3
|
+
import { mountSuspended, mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
4
|
+
import RegistrationForm from "~/components/User/RegistrationForm.vue";
|
|
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
|
+
const { mockRegister, mockIsLoggedIn, mockGetSuggestions } = vi.hoisted(() => {
|
|
18
|
+
return {
|
|
19
|
+
mockRegister: vi.fn(),
|
|
20
|
+
mockIsLoggedIn: { value: false },
|
|
21
|
+
mockGetSuggestions: vi.fn().mockResolvedValue([]),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
mockNuxtImport("useUser", () => () => ({
|
|
26
|
+
register: mockRegister,
|
|
27
|
+
isLoggedIn: ref(mockIsLoggedIn.value),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
mockNuxtImport("useAddressAutocomplete", () => () => ({
|
|
31
|
+
getSuggestions: mockGetSuggestions,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const mockToastAdd = vi.fn();
|
|
35
|
+
mockNuxtImport("useToast", () => () => ({
|
|
36
|
+
add: mockToastAdd,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// Mock useRuntimeConfig
|
|
40
|
+
mockNuxtImport("useRuntimeConfig", () => () => ({
|
|
41
|
+
public: {
|
|
42
|
+
shopware: {
|
|
43
|
+
devStorefrontUrl: "http://localhost:3000",
|
|
44
|
+
},
|
|
45
|
+
site: {
|
|
46
|
+
countryId: "default-country-id",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
describe("RegistrationForm", () => {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vi.clearAllMocks();
|
|
54
|
+
mockIsLoggedIn.value = false;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("renders correctly", async () => {
|
|
58
|
+
const wrapper = await mountSuspended(RegistrationForm);
|
|
59
|
+
expect(wrapper.exists()).toBeTruthy();
|
|
60
|
+
expect(wrapper.find('input[name="email"]').exists()).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("shows company field only for business account", async () => {
|
|
64
|
+
const wrapper = await mountSuspended(RegistrationForm);
|
|
65
|
+
|
|
66
|
+
// Initially private, so company field should NOT be in billing address
|
|
67
|
+
expect(wrapper.find('input[name="billingAddress.company"]').exists()).toBe(
|
|
68
|
+
false,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Switch to business
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
wrapper.vm.state.accountType = "business";
|
|
74
|
+
await nextTick();
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
76
|
+
|
|
77
|
+
// Try to find by name again
|
|
78
|
+
expect(wrapper.find('input[name="billingAddress.company"]').exists()).toBe(
|
|
79
|
+
true,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("shows shipping address fields when checkbox is checked", async () => {
|
|
84
|
+
const wrapper = await mountSuspended(RegistrationForm);
|
|
85
|
+
|
|
86
|
+
// Initial count of street inputs
|
|
87
|
+
const initialStreets = wrapper.findAll(
|
|
88
|
+
'input[name="billingAddress.street"]',
|
|
89
|
+
).length;
|
|
90
|
+
|
|
91
|
+
// @ts-ignore
|
|
92
|
+
wrapper.vm.state.isShippingAddressDifferent = true;
|
|
93
|
+
await nextTick();
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
95
|
+
|
|
96
|
+
// Should have more street inputs now (billing street + shipping street)
|
|
97
|
+
expect(wrapper.findAll('input[name$=".street"]').length).toBeGreaterThan(
|
|
98
|
+
initialStreets,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("submits the form with correct data", async () => {
|
|
103
|
+
const wrapper = await mountSuspended(RegistrationForm);
|
|
104
|
+
|
|
105
|
+
// Fill required fields
|
|
106
|
+
await wrapper.find('input[name="firstName"]').setValue("John");
|
|
107
|
+
await wrapper.find('input[name="lastName"]').setValue("Doe");
|
|
108
|
+
await wrapper.find('input[name="email"]').setValue("john@example.com");
|
|
109
|
+
|
|
110
|
+
// Billing address fields (AddressFields component)
|
|
111
|
+
// Manually update state since USelectMenu is hard to interact with in simple tests
|
|
112
|
+
// @ts-ignore
|
|
113
|
+
wrapper.vm.state.billingAddress.street = "Musterstr 1";
|
|
114
|
+
// Set zipcode and city directly since fields are removed
|
|
115
|
+
// @ts-ignore
|
|
116
|
+
wrapper.vm.state.billingAddress.zipcode = "12345";
|
|
117
|
+
// @ts-ignore
|
|
118
|
+
wrapper.vm.state.billingAddress.city = "Musterstadt";
|
|
119
|
+
|
|
120
|
+
await wrapper
|
|
121
|
+
.find('input[name="billingAddress.phoneNumber"]')
|
|
122
|
+
.setValue("12345678");
|
|
123
|
+
|
|
124
|
+
// Accept data protection
|
|
125
|
+
const dpCheckbox = wrapper.find('input[name="acceptedDataProtection"]');
|
|
126
|
+
await dpCheckbox.trigger("click");
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
128
|
+
|
|
129
|
+
// Submit
|
|
130
|
+
await wrapper.find("form").trigger("submit");
|
|
131
|
+
|
|
132
|
+
// Wait for async validation and submission
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
134
|
+
|
|
135
|
+
expect(mockRegister).toHaveBeenCalled();
|
|
136
|
+
const calledData = mockRegister.mock.calls[0][0];
|
|
137
|
+
expect(calledData.email).toBe("john@example.com");
|
|
138
|
+
expect(calledData.billingAddress.street).toBe("Musterstr 1");
|
|
139
|
+
// Check if firstName/lastName were copied to billing address as per logic in onSubmit
|
|
140
|
+
expect(calledData.billingAddress.firstName).toBe("John");
|
|
141
|
+
expect(calledData.billingAddress.lastName).toBe("Doe");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("shows detailed error toast on ApiClientError during registration", async () => {
|
|
145
|
+
const apiClientError = new ApiClientError({
|
|
146
|
+
errors: [
|
|
147
|
+
{
|
|
148
|
+
detail: 'The email address "lirim@veliu.net" is already in use',
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
mockRegister.mockRejectedValueOnce(apiClientError);
|
|
153
|
+
const wrapper = await mountSuspended(RegistrationForm);
|
|
154
|
+
|
|
155
|
+
// Fill minimum required fields to trigger onSubmit
|
|
156
|
+
await wrapper.find('input[name="firstName"]').setValue("John");
|
|
157
|
+
await wrapper.find('input[name="lastName"]').setValue("Doe");
|
|
158
|
+
await wrapper.find('input[name="email"]').setValue("lirim@veliu.net");
|
|
159
|
+
// @ts-ignore
|
|
160
|
+
wrapper.vm.state.billingAddress.street = "Musterstr 1";
|
|
161
|
+
// @ts-ignore
|
|
162
|
+
wrapper.vm.state.billingAddress.city = "Musterstadt";
|
|
163
|
+
await wrapper
|
|
164
|
+
.find('input[name="billingAddress.phoneNumber"]')
|
|
165
|
+
.setValue("12345678");
|
|
166
|
+
await wrapper.find('input[name="acceptedDataProtection"]').trigger("click");
|
|
167
|
+
|
|
168
|
+
await wrapper.find("form").trigger("submit");
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
170
|
+
|
|
171
|
+
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
172
|
+
expect.objectContaining({
|
|
173
|
+
title: "Registrierung fehlgeschlagen",
|
|
174
|
+
description: 'The email address "lirim@veliu.net" is already in use',
|
|
175
|
+
color: "error",
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("handles ApiClientError with missing errors gracefully", async () => {
|
|
181
|
+
const apiClientError = new ApiClientError({});
|
|
182
|
+
mockRegister.mockRejectedValueOnce(apiClientError);
|
|
183
|
+
const wrapper = await mountSuspended(RegistrationForm);
|
|
184
|
+
|
|
185
|
+
// Fill minimum required fields to trigger onSubmit
|
|
186
|
+
await wrapper.find('input[name="firstName"]').setValue("John");
|
|
187
|
+
await wrapper.find('input[name="lastName"]').setValue("Doe");
|
|
188
|
+
await wrapper.find('input[name="email"]').setValue("lirim@veliu.net");
|
|
189
|
+
// @ts-ignore
|
|
190
|
+
wrapper.vm.state.billingAddress.street = "Musterstr 1";
|
|
191
|
+
// @ts-ignore
|
|
192
|
+
wrapper.vm.state.billingAddress.city = "Musterstadt";
|
|
193
|
+
await wrapper
|
|
194
|
+
.find('input[name="billingAddress.phoneNumber"]')
|
|
195
|
+
.setValue("12345678");
|
|
196
|
+
await wrapper.find('input[name="acceptedDataProtection"]').trigger("click");
|
|
197
|
+
|
|
198
|
+
await wrapper.find("form").trigger("submit");
|
|
199
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
200
|
+
|
|
201
|
+
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
202
|
+
expect.objectContaining({
|
|
203
|
+
title: "Registrierung fehlgeschlagen",
|
|
204
|
+
description: "Bitte versuchen Sie es erneut.",
|
|
205
|
+
color: "error",
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("shows correction suggestion and stops submission when address needs correction", async () => {
|
|
211
|
+
mockGetSuggestions.mockResolvedValueOnce([
|
|
212
|
+
{
|
|
213
|
+
street: "Corrected Street 123",
|
|
214
|
+
city: "Corrected City",
|
|
215
|
+
zipcode: "54321",
|
|
216
|
+
label: "Corrected Street 123, 54321 Corrected City",
|
|
217
|
+
},
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
const wrapper = await mountSuspended(RegistrationForm);
|
|
221
|
+
|
|
222
|
+
// Fill required fields
|
|
223
|
+
await wrapper.find('input[name="firstName"]').setValue("John");
|
|
224
|
+
await wrapper.find('input[name="lastName"]').setValue("Doe");
|
|
225
|
+
await wrapper.find('input[name="email"]').setValue("john@example.com");
|
|
226
|
+
// @ts-ignore
|
|
227
|
+
wrapper.vm.state.billingAddress.street = "Musterstr 1";
|
|
228
|
+
// @ts-ignore
|
|
229
|
+
wrapper.vm.state.billingAddress.zipcode = "12345";
|
|
230
|
+
// @ts-ignore
|
|
231
|
+
wrapper.vm.state.billingAddress.city = "Musterstadt";
|
|
232
|
+
await wrapper
|
|
233
|
+
.find('input[name="billingAddress.phoneNumber"]')
|
|
234
|
+
.setValue("12345678");
|
|
235
|
+
await wrapper.find('input[name="acceptedDataProtection"]').trigger("click");
|
|
236
|
+
|
|
237
|
+
await wrapper.find("form").trigger("submit");
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
239
|
+
|
|
240
|
+
// Should NOT have called register
|
|
241
|
+
expect(mockRegister).not.toHaveBeenCalled();
|
|
242
|
+
|
|
243
|
+
// Should have shown a toast
|
|
244
|
+
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
245
|
+
expect.objectContaining({
|
|
246
|
+
title: "Adresskorrektur vorgeschlagen",
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// Should show the correction alert in the form
|
|
251
|
+
expect(wrapper.text()).toContain(
|
|
252
|
+
"Meinten Sie: Corrected Street 123, 54321 Corrected City?",
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import RegistrationForm from "~/components/User/RegistrationForm.vue";
|
|
4
|
+
import { flushPromises } from "@vue/test-utils";
|
|
5
|
+
|
|
6
|
+
describe("RegistrationForm Validation", () => {
|
|
7
|
+
it("shows error for street when empty and clears it when filled", async () => {
|
|
8
|
+
const wrapper = await mountSuspended(RegistrationForm);
|
|
9
|
+
|
|
10
|
+
const form = wrapper.find("form");
|
|
11
|
+
await form.trigger("submit");
|
|
12
|
+
await flushPromises();
|
|
13
|
+
|
|
14
|
+
// Check for street error message
|
|
15
|
+
expect(wrapper.text()).toContain(
|
|
16
|
+
"Bitte geben Sie Ihre Straße und Hausnummer an.",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// Find the street input and fill it
|
|
20
|
+
const streetInput = wrapper.find('input[name="billingAddress.street"]');
|
|
21
|
+
await streetInput.setValue("Hauptstraße 1");
|
|
22
|
+
|
|
23
|
+
// Fill other required fields to satisfy overall validation if needed
|
|
24
|
+
const zipInput = wrapper.find('input[name="billingAddress.zipcode"]');
|
|
25
|
+
await zipInput.setValue("60311");
|
|
26
|
+
const cityInput = wrapper.find('input[name="billingAddress.city"]');
|
|
27
|
+
await cityInput.setValue("Frankfurt");
|
|
28
|
+
|
|
29
|
+
await flushPromises();
|
|
30
|
+
|
|
31
|
+
// Trigger validation again or check if error disappeared
|
|
32
|
+
await form.trigger("submit");
|
|
33
|
+
await flushPromises();
|
|
34
|
+
|
|
35
|
+
expect(wrapper.text()).not.toContain(
|
|
36
|
+
"Bitte geben Sie Ihre Straße und Hausnummer an.",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createRegistrationSchema } from "~/validation/registrationSchema";
|
|
3
|
+
|
|
4
|
+
describe("registrationSchema", () => {
|
|
5
|
+
const baseState = {
|
|
6
|
+
accountType: "private",
|
|
7
|
+
guest: false,
|
|
8
|
+
isShippingAddressDifferent: false,
|
|
9
|
+
password: "password123",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const validAddress = {
|
|
13
|
+
street: "Musterstraße 1",
|
|
14
|
+
city: "Musterstadt",
|
|
15
|
+
countryId: "country-id",
|
|
16
|
+
phoneNumber: "123456789",
|
|
17
|
+
zipcode: "12345",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const validData = {
|
|
21
|
+
accountType: "private",
|
|
22
|
+
email: "test@example.com",
|
|
23
|
+
firstName: "John",
|
|
24
|
+
lastName: "Doe",
|
|
25
|
+
guest: false,
|
|
26
|
+
acceptedDataProtection: true,
|
|
27
|
+
password: "password123",
|
|
28
|
+
passwordConfirm: "password123",
|
|
29
|
+
billingAddress: validAddress,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
it("should validate a valid private account", () => {
|
|
33
|
+
const schema = createRegistrationSchema(baseState);
|
|
34
|
+
const result = schema.safeParse(validData);
|
|
35
|
+
expect(result.success).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should fail if company is missing for business account", () => {
|
|
39
|
+
const businessState = { ...baseState, accountType: "business" };
|
|
40
|
+
const businessData = {
|
|
41
|
+
...validData,
|
|
42
|
+
accountType: "business",
|
|
43
|
+
billingAddress: { ...validAddress },
|
|
44
|
+
};
|
|
45
|
+
const schema = createRegistrationSchema(businessState);
|
|
46
|
+
const result = schema.safeParse(businessData);
|
|
47
|
+
expect(result.success).toBe(false);
|
|
48
|
+
if (!result.success) {
|
|
49
|
+
expect(result.error.issues[0].message).toBe(
|
|
50
|
+
"Als Geschäftskunde ist der Firmenname ein Pflichtfeld.",
|
|
51
|
+
);
|
|
52
|
+
expect(result.error.issues[0].path).toContain("billingAddress");
|
|
53
|
+
expect(result.error.issues[0].path).toContain("company");
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should validate a valid business account", () => {
|
|
58
|
+
const businessState = { ...baseState, accountType: "business" };
|
|
59
|
+
const businessData = {
|
|
60
|
+
...validData,
|
|
61
|
+
accountType: "business",
|
|
62
|
+
billingAddress: { ...validAddress, company: "Test Corp" },
|
|
63
|
+
};
|
|
64
|
+
const schema = createRegistrationSchema(businessState);
|
|
65
|
+
const result = schema.safeParse(businessData);
|
|
66
|
+
expect(result.success).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should require shipping address fields if different from billing address", () => {
|
|
70
|
+
const shippingState = { ...baseState, isShippingAddressDifferent: true };
|
|
71
|
+
const shippingData = {
|
|
72
|
+
...validData,
|
|
73
|
+
isShippingAddressDifferent: true,
|
|
74
|
+
shippingAddress: {
|
|
75
|
+
firstName: "",
|
|
76
|
+
lastName: "",
|
|
77
|
+
street: "",
|
|
78
|
+
city: "",
|
|
79
|
+
phoneNumber: "",
|
|
80
|
+
company: "",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const schema = createRegistrationSchema(shippingState);
|
|
84
|
+
const result = schema.safeParse(shippingData);
|
|
85
|
+
expect(result.success).toBe(false);
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
const messages = result.error.issues.map((i) => i.message);
|
|
88
|
+
expect(messages).toContain(
|
|
89
|
+
"Bitte geben Sie den Vornamen für die Lieferadresse an.",
|
|
90
|
+
);
|
|
91
|
+
expect(messages).toContain(
|
|
92
|
+
"Bitte geben Sie den Nachnamen für die Lieferadresse an.",
|
|
93
|
+
);
|
|
94
|
+
expect(messages).toContain(
|
|
95
|
+
"Bitte geben Sie die Straße für die Lieferadresse an.",
|
|
96
|
+
);
|
|
97
|
+
expect(messages).toContain(
|
|
98
|
+
"Bitte geben Sie den Ort für die Lieferadresse an.",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should require company in shipping address for business account if different", () => {
|
|
104
|
+
const shippingState = {
|
|
105
|
+
...baseState,
|
|
106
|
+
accountType: "business",
|
|
107
|
+
isShippingAddressDifferent: true,
|
|
108
|
+
};
|
|
109
|
+
const shippingData = {
|
|
110
|
+
...validData,
|
|
111
|
+
accountType: "business",
|
|
112
|
+
isShippingAddressDifferent: true,
|
|
113
|
+
billingAddress: { ...validAddress, company: "Billing Corp" },
|
|
114
|
+
shippingAddress: {
|
|
115
|
+
firstName: "Jane",
|
|
116
|
+
lastName: "Doe",
|
|
117
|
+
street: "Shipping St 1",
|
|
118
|
+
city: "ShipCity",
|
|
119
|
+
phoneNumber: "987654321",
|
|
120
|
+
company: "",
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
const schema = createRegistrationSchema(shippingState);
|
|
124
|
+
const result = schema.safeParse(shippingData);
|
|
125
|
+
expect(result.success).toBe(false);
|
|
126
|
+
if (!result.success) {
|
|
127
|
+
const messages = result.error.issues.map((i) => i.message);
|
|
128
|
+
expect(messages).toContain(
|
|
129
|
+
"Bitte geben Sie den Firmennamen für die Lieferadresse an.",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should validate different shipping address for business", () => {
|
|
135
|
+
const shippingState = {
|
|
136
|
+
...baseState,
|
|
137
|
+
accountType: "business",
|
|
138
|
+
isShippingAddressDifferent: true,
|
|
139
|
+
};
|
|
140
|
+
const shippingData = {
|
|
141
|
+
...validData,
|
|
142
|
+
accountType: "business",
|
|
143
|
+
isShippingAddressDifferent: true,
|
|
144
|
+
billingAddress: { ...validAddress, company: "Billing Corp" },
|
|
145
|
+
shippingAddress: {
|
|
146
|
+
firstName: "Jane",
|
|
147
|
+
lastName: "Doe",
|
|
148
|
+
street: "Shipping St 1",
|
|
149
|
+
city: "ShipCity",
|
|
150
|
+
phoneNumber: "987654321",
|
|
151
|
+
company: "Shipping Corp",
|
|
152
|
+
zipcode: "54321",
|
|
153
|
+
countryId: "country-id",
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
const schema = createRegistrationSchema(shippingState);
|
|
157
|
+
const result = schema.safeParse(shippingData);
|
|
158
|
+
expect(result.success).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should not require password for guest", () => {
|
|
162
|
+
const guestState = { ...baseState, guest: true };
|
|
163
|
+
const guestData = {
|
|
164
|
+
...validData,
|
|
165
|
+
guest: true,
|
|
166
|
+
password: "",
|
|
167
|
+
passwordConfirm: "",
|
|
168
|
+
};
|
|
169
|
+
const schema = createRegistrationSchema(guestState);
|
|
170
|
+
const result = schema.safeParse(guestData);
|
|
171
|
+
expect(result.success).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should require password for non-guest", () => {
|
|
175
|
+
const schema = createRegistrationSchema(baseState);
|
|
176
|
+
const result = schema.safeParse({
|
|
177
|
+
...validData,
|
|
178
|
+
password: "",
|
|
179
|
+
passwordConfirm: "",
|
|
180
|
+
});
|
|
181
|
+
expect(result.success).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should fail if data protection is not accepted", () => {
|
|
185
|
+
const schema = createRegistrationSchema(baseState);
|
|
186
|
+
const result = schema.safeParse({
|
|
187
|
+
...validData,
|
|
188
|
+
acceptedDataProtection: false,
|
|
189
|
+
});
|
|
190
|
+
expect(result.success).toBe(false);
|
|
191
|
+
if (!result.success) {
|
|
192
|
+
expect(result.error.issues[0].message).toBe(
|
|
193
|
+
"Bitte akzeptieren Sie die Datenschutzbestimmungen.",
|
|
194
|
+
);
|
|
195
|
+
expect(result.error.issues[0].path).toContain("acceptedDataProtection");
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should fail if street does not contain a house number", () => {
|
|
200
|
+
const schema = createRegistrationSchema(baseState);
|
|
201
|
+
const result = schema.safeParse({
|
|
202
|
+
...validData,
|
|
203
|
+
billingAddress: {
|
|
204
|
+
...validAddress,
|
|
205
|
+
street: "Musterstraße",
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
expect(result.success).toBe(false);
|
|
209
|
+
if (!result.success) {
|
|
210
|
+
expect(result.error.issues[0].message).toBe(
|
|
211
|
+
"Bitte geben Sie Ihre Straße und Hausnummer an.",
|
|
212
|
+
);
|
|
213
|
+
expect(result.error.issues[0].path).toContain("billingAddress");
|
|
214
|
+
expect(result.error.issues[0].path).toContain("street");
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should fail if shipping street does not contain a house number when different", () => {
|
|
219
|
+
const shippingState = { ...baseState, isShippingAddressDifferent: true };
|
|
220
|
+
const shippingData = {
|
|
221
|
+
...validData,
|
|
222
|
+
isShippingAddressDifferent: true,
|
|
223
|
+
shippingAddress: {
|
|
224
|
+
...validAddress,
|
|
225
|
+
street: "Shipping Street",
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
const schema = createRegistrationSchema(shippingState);
|
|
229
|
+
const result = schema.safeParse(shippingData);
|
|
230
|
+
expect(result.success).toBe(false);
|
|
231
|
+
if (!result.success) {
|
|
232
|
+
const messages = result.error.issues.map((i) => i.message);
|
|
233
|
+
expect(messages).toContain(
|
|
234
|
+
"Bitte geben Sie Ihre Straße und Hausnummer an.",
|
|
235
|
+
);
|
|
236
|
+
const streetIssue = result.error.issues.find((i) =>
|
|
237
|
+
i.path.includes("street"),
|
|
238
|
+
);
|
|
239
|
+
expect(streetIssue?.path).toContain("shippingAddress");
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import { useAddressAutocomplete } from "~/composables/useAddressAutocomplete";
|
|
4
|
+
import { ref } from "vue";
|
|
5
|
+
|
|
6
|
+
// Mock useFetch
|
|
7
|
+
const { mockUseFetch } = vi.hoisted(() => ({
|
|
8
|
+
mockUseFetch: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
mockNuxtImport("useFetch", () => mockUseFetch);
|
|
11
|
+
|
|
12
|
+
// Mock useRuntimeConfig
|
|
13
|
+
mockNuxtImport("useRuntimeConfig", () => () => ({
|
|
14
|
+
geoapifyApiKey: "test-api-key",
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mock useValidCitiesForDelivery
|
|
18
|
+
const { mockBoundingBoxCoordinates } = vi.hoisted(() => ({
|
|
19
|
+
mockBoundingBoxCoordinates: {
|
|
20
|
+
value: "8.822251,50.055026,8.899077,50.104327",
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
mockNuxtImport("useValidCitiesForDelivery", () => () => ({
|
|
24
|
+
validCities: ref(["Obertshausen", "Lämmerspiel", "Hausen"]),
|
|
25
|
+
boundingBoxCoordinates: mockBoundingBoxCoordinates,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
describe("useAddressAutocomplete", () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should return empty suggestions for short input", async () => {
|
|
34
|
+
const { getSuggestions } = useAddressAutocomplete();
|
|
35
|
+
const suggestions = await getSuggestions("ab");
|
|
36
|
+
expect(suggestions).toEqual([]);
|
|
37
|
+
expect(mockUseFetch).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should fetch suggestions from Geoapify", async () => {
|
|
41
|
+
const mockResponse = {
|
|
42
|
+
features: [
|
|
43
|
+
{
|
|
44
|
+
properties: {
|
|
45
|
+
street: "Main St",
|
|
46
|
+
housenumber: "1",
|
|
47
|
+
city: "Obertshausen",
|
|
48
|
+
postcode: "63179",
|
|
49
|
+
formatted: "Main St 1, 63179 Obertshausen, Germany",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
mockUseFetch.mockResolvedValue({
|
|
56
|
+
data: ref(mockResponse),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const { getSuggestions } = useAddressAutocomplete();
|
|
60
|
+
const suggestions = await getSuggestions("Main St 1");
|
|
61
|
+
|
|
62
|
+
expect(mockUseFetch).toHaveBeenCalledWith(
|
|
63
|
+
"/api/address/autocomplete",
|
|
64
|
+
expect.objectContaining({
|
|
65
|
+
query: expect.objectContaining({
|
|
66
|
+
text: "Main St 1",
|
|
67
|
+
lang: "de",
|
|
68
|
+
limit: 5,
|
|
69
|
+
filter: "rect:8.822251,50.055026,8.899077,50.104327",
|
|
70
|
+
}),
|
|
71
|
+
}),
|
|
72
|
+
expect.any(String),
|
|
73
|
+
);
|
|
74
|
+
expect(suggestions).toHaveLength(1);
|
|
75
|
+
expect(suggestions[0]).toEqual({
|
|
76
|
+
street: "Main St 1",
|
|
77
|
+
city: "Obertshausen",
|
|
78
|
+
zipcode: "63179",
|
|
79
|
+
label: "Main St 1, 63179 Obertshausen, Germany",
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should handle missing features in API response", async () => {
|
|
84
|
+
mockUseFetch.mockResolvedValue({
|
|
85
|
+
data: ref({}),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const { getSuggestions } = useAddressAutocomplete();
|
|
89
|
+
const suggestions = await getSuggestions("Some address");
|
|
90
|
+
expect(suggestions).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should handle fetch error", async () => {
|
|
94
|
+
mockUseFetch.mockRejectedValue(new Error("Network error"));
|
|
95
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
96
|
+
|
|
97
|
+
const { getSuggestions } = useAddressAutocomplete();
|
|
98
|
+
const suggestions = await getSuggestions("Some address");
|
|
99
|
+
|
|
100
|
+
expect(suggestions).toEqual([]);
|
|
101
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
102
|
+
consoleSpy.mockRestore();
|
|
103
|
+
});
|
|
104
|
+
it("should filter suggestions based on valid cities", async () => {
|
|
105
|
+
const mockResponse = {
|
|
106
|
+
features: [
|
|
107
|
+
{
|
|
108
|
+
properties: {
|
|
109
|
+
street: "Main St",
|
|
110
|
+
housenumber: "1",
|
|
111
|
+
city: "Obertshausen",
|
|
112
|
+
postcode: "63179",
|
|
113
|
+
formatted: "Main St 1, 63179 Obertshausen, Germany",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
properties: {
|
|
118
|
+
street: "Other St",
|
|
119
|
+
housenumber: "2",
|
|
120
|
+
city: "Berlin",
|
|
121
|
+
postcode: "10115",
|
|
122
|
+
formatted: "Other St 2, 10115 Berlin, Germany",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
mockUseFetch.mockResolvedValue({
|
|
129
|
+
data: ref(mockResponse),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const { getSuggestions } = useAddressAutocomplete();
|
|
133
|
+
const suggestions = await getSuggestions("Main St 1");
|
|
134
|
+
|
|
135
|
+
expect(suggestions).toHaveLength(1);
|
|
136
|
+
expect(suggestions[0]?.city).toBe("Obertshausen");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should not include filter when boundingBoxCoordinates is empty", async () => {
|
|
140
|
+
mockBoundingBoxCoordinates.value = "";
|
|
141
|
+
mockUseFetch.mockResolvedValue({
|
|
142
|
+
data: ref({ features: [] }),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const { getSuggestions } = useAddressAutocomplete();
|
|
146
|
+
await getSuggestions("Main St 1");
|
|
147
|
+
|
|
148
|
+
expect(mockUseFetch).toHaveBeenCalledWith(
|
|
149
|
+
"/api/address/autocomplete",
|
|
150
|
+
expect.objectContaining({
|
|
151
|
+
query: expect.not.objectContaining({
|
|
152
|
+
filter: expect.anything(),
|
|
153
|
+
}),
|
|
154
|
+
}),
|
|
155
|
+
expect.any(String),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Reset for other tests
|
|
159
|
+
mockBoundingBoxCoordinates.value = "8.822251,50.055026,8.899077,50.104327";
|
|
160
|
+
});
|
|
161
|
+
});
|