@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.
Files changed (35) hide show
  1. package/.env.example +6 -1
  2. package/LICENSE +21 -0
  3. package/README.md +81 -0
  4. package/app/components/Address/Fields.vue +128 -0
  5. package/app/components/Checkout/PaymentAndDelivery.vue +49 -27
  6. package/app/components/Header.vue +26 -13
  7. package/app/components/User/LoginForm.vue +32 -11
  8. package/app/components/User/RegistrationForm.vue +105 -180
  9. package/app/composables/useAddressAutocomplete.ts +84 -0
  10. package/app/composables/useAddressValidation.ts +95 -0
  11. package/app/composables/useValidCitiesForDelivery.ts +12 -0
  12. package/app/pages/[...all].vue +28 -0
  13. package/app/pages/index.vue +4 -0
  14. package/app/pages/menu/[...all].vue +52 -0
  15. package/app/validation/registrationSchema.ts +105 -124
  16. package/content/{unternehmen/impressum.md → impressum.md} +5 -0
  17. package/content/index.yml +2 -1
  18. package/content/unternehmen/agb.md +5 -0
  19. package/content/unternehmen/datenschutz.md +5 -0
  20. package/content/unternehmen/zahlung-und-versand.md +34 -0
  21. package/content.config.ts +6 -2
  22. package/eslint.config.mjs +5 -3
  23. package/nuxt.config.ts +33 -22
  24. package/package.json +2 -1
  25. package/public/card.png +0 -0
  26. package/server/api/address/autocomplete.get.ts +33 -0
  27. package/test/nuxt/AddressFields.test.ts +284 -0
  28. package/test/nuxt/Header.test.ts +124 -0
  29. package/test/nuxt/LoginForm.test.ts +141 -0
  30. package/test/nuxt/PaymentAndDelivery.test.ts +78 -0
  31. package/test/nuxt/RegistrationForm.test.ts +255 -0
  32. package/test/nuxt/RegistrationValidation.test.ts +39 -0
  33. package/test/nuxt/registrationSchema.test.ts +242 -0
  34. package/test/nuxt/useAddressAutocomplete.test.ts +161 -0
  35. 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
+ });