@shopbite-de/storefront 1.5.3 → 1.6.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/.env.example +3 -1
- 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/validation/registrationSchema.ts +105 -124
- package/content/unternehmen/zahlung-und-versand.md +29 -0
- package/nuxt.config.ts +1 -0
- package/package.json +1 -1
- 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
|
@@ -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
|
+
});
|
|
@@ -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
|
+
});
|