@shopbite-de/storefront 1.17.4 → 1.18.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/.github/workflows/build.yaml +2 -2
- package/.github/workflows/ci.yaml +8 -8
- package/app/components/Address/Fields.vue +2 -0
- package/app/components/Checkout/Summary.vue +1 -0
- package/app/components/Checkout/VoucherInput.vue +48 -0
- package/app/components/User/RegistrationForm.vue +3 -21
- package/app/composables/useAddressValidation.ts +12 -0
- package/app/composables/useVoucherCode.ts +51 -0
- package/package.json +4 -4
- package/renovate.json +7 -5
- package/test/nuxt/CheckoutVoucherInput.test.ts +196 -0
- package/test/nuxt/RegistrationForm.test.ts +6 -14
|
@@ -23,9 +23,9 @@ jobs:
|
|
|
23
23
|
steps:
|
|
24
24
|
- uses: actions/checkout@v6
|
|
25
25
|
|
|
26
|
-
- uses: pnpm/action-setup@
|
|
26
|
+
- uses: pnpm/action-setup@v5
|
|
27
27
|
with:
|
|
28
|
-
version: 10.
|
|
28
|
+
version: 10.33.0
|
|
29
29
|
|
|
30
30
|
- uses: actions/setup-node@v6
|
|
31
31
|
with:
|
|
@@ -67,9 +67,9 @@ jobs:
|
|
|
67
67
|
steps:
|
|
68
68
|
- uses: actions/checkout@v6
|
|
69
69
|
|
|
70
|
-
- uses: pnpm/action-setup@
|
|
70
|
+
- uses: pnpm/action-setup@v5
|
|
71
71
|
with:
|
|
72
|
-
version: 10.
|
|
72
|
+
version: 10.33.0
|
|
73
73
|
|
|
74
74
|
- uses: actions/setup-node@v6
|
|
75
75
|
with:
|
|
@@ -100,9 +100,9 @@ jobs:
|
|
|
100
100
|
steps:
|
|
101
101
|
- uses: actions/checkout@v6
|
|
102
102
|
|
|
103
|
-
- uses: pnpm/action-setup@
|
|
103
|
+
- uses: pnpm/action-setup@v5
|
|
104
104
|
with:
|
|
105
|
-
version: 10.
|
|
105
|
+
version: 10.33.0
|
|
106
106
|
|
|
107
107
|
- uses: actions/setup-node@v6
|
|
108
108
|
with:
|
|
@@ -130,9 +130,9 @@ jobs:
|
|
|
130
130
|
steps:
|
|
131
131
|
- uses: actions/checkout@v6
|
|
132
132
|
|
|
133
|
-
- uses: pnpm/action-setup@
|
|
133
|
+
- uses: pnpm/action-setup@v5
|
|
134
134
|
with:
|
|
135
|
-
version: 10.
|
|
135
|
+
version: 10.33.0
|
|
136
136
|
|
|
137
137
|
- uses: actions/setup-node@v6
|
|
138
138
|
with:
|
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
correction,
|
|
19
19
|
isInvalidCity,
|
|
20
20
|
checkAddress,
|
|
21
|
+
flushPendingCheck,
|
|
21
22
|
applyCorrection,
|
|
22
23
|
} = useAddressValidation(model, {
|
|
23
24
|
isShipping: props.isShipping,
|
|
@@ -27,6 +28,7 @@ const {
|
|
|
27
28
|
|
|
28
29
|
defineExpose({
|
|
29
30
|
checkAddress,
|
|
31
|
+
flushPendingCheck,
|
|
30
32
|
showCorrection,
|
|
31
33
|
});
|
|
32
34
|
</script>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const {
|
|
3
|
+
voucherCode,
|
|
4
|
+
voucherLoading,
|
|
5
|
+
applyVoucher,
|
|
6
|
+
appliedPromotionCodes,
|
|
7
|
+
removeItem,
|
|
8
|
+
} = useVoucherCode();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div class="flex flex-col gap-2">
|
|
13
|
+
<div class="flex flex-row gap-2">
|
|
14
|
+
<UInput
|
|
15
|
+
v-model="voucherCode"
|
|
16
|
+
placeholder="Gutscheincode"
|
|
17
|
+
class="flex-1"
|
|
18
|
+
:disabled="voucherLoading"
|
|
19
|
+
@keyup.enter="applyVoucher"
|
|
20
|
+
/>
|
|
21
|
+
<UButton
|
|
22
|
+
label="Einlösen"
|
|
23
|
+
variant="outline"
|
|
24
|
+
:loading="voucherLoading"
|
|
25
|
+
:disabled="!voucherCode.trim() || voucherLoading"
|
|
26
|
+
@click="applyVoucher"
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
<div
|
|
30
|
+
v-for="promo in appliedPromotionCodes"
|
|
31
|
+
:key="promo.id"
|
|
32
|
+
class="flex flex-row justify-between items-center text-sm text-success"
|
|
33
|
+
>
|
|
34
|
+
<span class="flex items-center gap-1">
|
|
35
|
+
<UIcon name="i-lucide-tag" />
|
|
36
|
+
{{ promo.label }}
|
|
37
|
+
</span>
|
|
38
|
+
<UButton
|
|
39
|
+
icon="i-lucide-x"
|
|
40
|
+
size="xs"
|
|
41
|
+
variant="ghost"
|
|
42
|
+
color="neutral"
|
|
43
|
+
aria-label="Gutschein entfernen"
|
|
44
|
+
@click="removeItem(promo)"
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
@@ -65,30 +65,12 @@ const billingAddressFields = ref();
|
|
|
65
65
|
const shippingAddressFields = ref();
|
|
66
66
|
|
|
67
67
|
async function onSubmit(event: FormSubmitEvent<RegistrationSchema>) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Check for address corrections
|
|
71
|
-
// If a correction is already shown, treat it as found to prevent submission
|
|
72
|
-
const billingCorrectionFound =
|
|
73
|
-
Boolean(billingAddressFields.value?.showCorrection || false) ||
|
|
74
|
-
(await billingAddressFields.value?.checkAddress());
|
|
75
|
-
|
|
76
|
-
let shippingCorrectionFound = false;
|
|
68
|
+
billingAddressFields.value?.flushPendingCheck();
|
|
77
69
|
if (state.isShippingAddressDifferent) {
|
|
78
|
-
|
|
79
|
-
Boolean(shippingAddressFields.value?.showCorrection || false) ||
|
|
80
|
-
(await shippingAddressFields.value?.checkAddress());
|
|
70
|
+
shippingAddressFields.value?.flushPendingCheck();
|
|
81
71
|
}
|
|
82
72
|
|
|
83
|
-
|
|
84
|
-
toast.add({
|
|
85
|
-
title: "Adresskorrektur vorgeschlagen",
|
|
86
|
-
description:
|
|
87
|
-
"Bitte überprüfen Sie die vorgeschlagene Adresskorrektur, bevor Sie fortfahren.",
|
|
88
|
-
color: "info",
|
|
89
|
-
});
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
73
|
+
const registrationData = { ...event.data };
|
|
92
74
|
|
|
93
75
|
if (
|
|
94
76
|
!registrationData.billingAddress.firstName &&
|
|
@@ -64,6 +64,7 @@ export function useAddressValidation(
|
|
|
64
64
|
|
|
65
65
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
66
66
|
debounceTimer = setTimeout(() => {
|
|
67
|
+
debounceTimer = null;
|
|
67
68
|
if (model.value.street) {
|
|
68
69
|
checkAddress();
|
|
69
70
|
}
|
|
@@ -72,6 +73,16 @@ export function useAddressValidation(
|
|
|
72
73
|
{ deep: true },
|
|
73
74
|
);
|
|
74
75
|
|
|
76
|
+
function flushPendingCheck() {
|
|
77
|
+
if (debounceTimer) {
|
|
78
|
+
clearTimeout(debounceTimer);
|
|
79
|
+
debounceTimer = null;
|
|
80
|
+
if (model.value.street) {
|
|
81
|
+
checkAddress();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
75
86
|
onUnmounted(() => {
|
|
76
87
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
77
88
|
});
|
|
@@ -90,6 +101,7 @@ export function useAddressValidation(
|
|
|
90
101
|
correction,
|
|
91
102
|
isInvalidCity,
|
|
92
103
|
checkAddress,
|
|
104
|
+
flushPendingCheck,
|
|
93
105
|
applyCorrection,
|
|
94
106
|
};
|
|
95
107
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function useVoucherCode() {
|
|
2
|
+
const { cart, addPromotionCode, appliedPromotionCodes, removeItem } =
|
|
3
|
+
useCart();
|
|
4
|
+
const toast = useToast();
|
|
5
|
+
|
|
6
|
+
const voucherCode = ref("");
|
|
7
|
+
const voucherLoading = ref(false);
|
|
8
|
+
|
|
9
|
+
async function applyVoucher() {
|
|
10
|
+
const code = voucherCode.value.trim();
|
|
11
|
+
if (!code || voucherLoading.value) return;
|
|
12
|
+
voucherLoading.value = true;
|
|
13
|
+
try {
|
|
14
|
+
await addPromotionCode(code);
|
|
15
|
+
const errors = cart.value?.errors ?? {};
|
|
16
|
+
const promotionError = Object.values(errors).find(
|
|
17
|
+
(e) => (e as { promotionCode?: string }).promotionCode === code,
|
|
18
|
+
) as { translatedMessage?: string } | undefined;
|
|
19
|
+
if (promotionError) {
|
|
20
|
+
toast.add({
|
|
21
|
+
title: "Gutschein ungültig",
|
|
22
|
+
description:
|
|
23
|
+
promotionError.translatedMessage ??
|
|
24
|
+
"Der eingegebene Gutscheincode ist ungültig oder abgelaufen.",
|
|
25
|
+
color: "error",
|
|
26
|
+
icon: "i-lucide-x-circle",
|
|
27
|
+
});
|
|
28
|
+
} else {
|
|
29
|
+
voucherCode.value = "";
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
toast.add({
|
|
33
|
+
title: "Gutschein ungültig",
|
|
34
|
+
description:
|
|
35
|
+
"Der eingegebene Gutscheincode ist ungültig oder abgelaufen.",
|
|
36
|
+
color: "error",
|
|
37
|
+
icon: "i-lucide-x-circle",
|
|
38
|
+
});
|
|
39
|
+
} finally {
|
|
40
|
+
voucherLoading.value = false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
voucherCode,
|
|
46
|
+
voucherLoading,
|
|
47
|
+
applyVoucher,
|
|
48
|
+
appliedPromotionCodes,
|
|
49
|
+
removeItem,
|
|
50
|
+
};
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shopbite-de/storefront",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"main": "nuxt.config.ts",
|
|
5
5
|
"description": "Shopware storefront for food delivery shops",
|
|
6
6
|
"keywords": [
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@nuxt/image": "^2.0.0",
|
|
27
27
|
"@nuxt/scripts": "0.13.2",
|
|
28
28
|
"@nuxt/ui": "^4.5.1",
|
|
29
|
-
"@nuxtjs/robots": "^
|
|
29
|
+
"@nuxtjs/robots": "^6.0.0",
|
|
30
30
|
"@sentry/nuxt": "^10.43.0",
|
|
31
31
|
"@shopware/api-client": "^1.4.0",
|
|
32
32
|
"@shopware/api-gen": "^1.4.0",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
|
54
54
|
"@typescript-eslint/parser": "^8.57.0",
|
|
55
55
|
"@vitejs/plugin-vue": "^6.0.5",
|
|
56
|
-
"@vitest/ui": "4.1.
|
|
56
|
+
"@vitest/ui": "4.1.2",
|
|
57
57
|
"@vue/compiler-dom": "^3.5.30",
|
|
58
58
|
"@vue/server-renderer": "^3.5.30",
|
|
59
59
|
"@vue/test-utils": "^2.4.6",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"playwright-core": "^1.58.2",
|
|
66
66
|
"prettier": "^3.8.1",
|
|
67
67
|
"tailwindcss": "^4.2.1",
|
|
68
|
-
"typescript": "^
|
|
68
|
+
"typescript": "^6.0.0",
|
|
69
69
|
"vitest": "^4.1.0"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|
package/renovate.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
|
+
"automerge": true,
|
|
4
|
+
"platformAutomerge": true,
|
|
5
|
+
"labels": ["dependencies"],
|
|
3
6
|
"extends": ["config:recommended"],
|
|
7
|
+
"separateMultipleMajor": true,
|
|
8
|
+
"rebaseWhen": "auto",
|
|
4
9
|
"packageRules": [
|
|
5
10
|
{
|
|
6
|
-
"matchUpdateTypes": ["
|
|
7
|
-
"automerge":
|
|
8
|
-
"automergeType": "pr",
|
|
9
|
-
"automergeStrategy": "rebase",
|
|
10
|
-
"platformAutomerge": true
|
|
11
|
+
"matchUpdateTypes": ["major", "minor"],
|
|
12
|
+
"automerge": false
|
|
11
13
|
}
|
|
12
14
|
]
|
|
13
15
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
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 CheckoutVoucherInput from "~/components/Checkout/VoucherInput.vue";
|
|
5
|
+
|
|
6
|
+
const { mockAddPromotionCode, mockRemoveItem } = vi.hoisted(() => ({
|
|
7
|
+
mockAddPromotionCode: vi.fn().mockResolvedValue(undefined),
|
|
8
|
+
mockRemoveItem: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const mockCart = ref<{ errors?: Record<string, unknown> } | null>(null);
|
|
12
|
+
const mockAppliedPromotionCodes = ref<{ id: string; label: string }[]>([]);
|
|
13
|
+
|
|
14
|
+
mockNuxtImport("useCart", () => () => ({
|
|
15
|
+
cart: mockCart,
|
|
16
|
+
addPromotionCode: mockAddPromotionCode,
|
|
17
|
+
appliedPromotionCodes: mockAppliedPromotionCodes,
|
|
18
|
+
removeItem: mockRemoveItem,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const mockToastAdd = vi.fn();
|
|
22
|
+
mockNuxtImport("useToast", () => () => ({
|
|
23
|
+
add: mockToastAdd,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
describe("CheckoutVoucherInput", () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
mockAddPromotionCode.mockResolvedValue(undefined);
|
|
30
|
+
mockAppliedPromotionCodes.value = [];
|
|
31
|
+
mockCart.value = null;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("renders the input and button", async () => {
|
|
35
|
+
const wrapper = await mountSuspended(CheckoutVoucherInput);
|
|
36
|
+
expect(wrapper.find("input").exists()).toBe(true);
|
|
37
|
+
expect(wrapper.text()).toContain("Einlösen");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("apply button is disabled when input is empty", async () => {
|
|
41
|
+
const wrapper = await mountSuspended(CheckoutVoucherInput);
|
|
42
|
+
const button = wrapper.find("button");
|
|
43
|
+
expect(button.attributes("disabled")).toBeDefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("apply button is enabled when input has a value", async () => {
|
|
47
|
+
const wrapper = await mountSuspended(CheckoutVoucherInput);
|
|
48
|
+
await wrapper.find("input").setValue("SAVE10");
|
|
49
|
+
await nextTick();
|
|
50
|
+
const button = wrapper.find("button");
|
|
51
|
+
expect(button.attributes("disabled")).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("does not call addPromotionCode a second time if already loading", async () => {
|
|
55
|
+
let resolvePromotion!: () => void;
|
|
56
|
+
mockAddPromotionCode.mockReturnValueOnce(
|
|
57
|
+
new Promise<void>((resolve) => {
|
|
58
|
+
resolvePromotion = resolve;
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const wrapper = await mountSuspended(CheckoutVoucherInput);
|
|
63
|
+
await wrapper.find("input").setValue("SAVE10");
|
|
64
|
+
await nextTick();
|
|
65
|
+
|
|
66
|
+
// First invocation — in-flight
|
|
67
|
+
wrapper.find("button").trigger("click");
|
|
68
|
+
await nextTick();
|
|
69
|
+
|
|
70
|
+
// Second invocation while still loading
|
|
71
|
+
wrapper.find("button").trigger("click");
|
|
72
|
+
await nextTick();
|
|
73
|
+
await wrapper.find("input").trigger("keyup.enter");
|
|
74
|
+
await nextTick();
|
|
75
|
+
|
|
76
|
+
resolvePromotion();
|
|
77
|
+
mockCart.value = { errors: {} };
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
79
|
+
|
|
80
|
+
expect(mockAddPromotionCode).toHaveBeenCalledTimes(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("disables the button and input while loading to prevent concurrent submits", async () => {
|
|
84
|
+
let resolvePromotion!: () => void;
|
|
85
|
+
mockAddPromotionCode.mockReturnValueOnce(
|
|
86
|
+
new Promise<void>((resolve) => {
|
|
87
|
+
resolvePromotion = resolve;
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const wrapper = await mountSuspended(CheckoutVoucherInput);
|
|
92
|
+
await wrapper.find("input").setValue("SAVE10");
|
|
93
|
+
await nextTick();
|
|
94
|
+
|
|
95
|
+
wrapper.find("button").trigger("click");
|
|
96
|
+
await nextTick();
|
|
97
|
+
|
|
98
|
+
// While the request is in-flight both button and input must be disabled
|
|
99
|
+
expect(wrapper.find("button").attributes("disabled")).toBeDefined();
|
|
100
|
+
expect(wrapper.find("input").attributes("disabled")).toBeDefined();
|
|
101
|
+
|
|
102
|
+
// Resolve and confirm the loading guard is lifted
|
|
103
|
+
resolvePromotion();
|
|
104
|
+
mockCart.value = { errors: {} };
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
106
|
+
|
|
107
|
+
// Input is no longer disabled by loading (code was cleared on success, button
|
|
108
|
+
// disabled due to empty input is expected and correct)
|
|
109
|
+
expect(wrapper.find("input").attributes("disabled")).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("clears the input and does not show a toast on successful voucher apply", async () => {
|
|
113
|
+
mockCart.value = { errors: {} };
|
|
114
|
+
const wrapper = await mountSuspended(CheckoutVoucherInput);
|
|
115
|
+
await wrapper.find("input").setValue("SAVE10");
|
|
116
|
+
await wrapper.find("button").trigger("click");
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
118
|
+
|
|
119
|
+
expect(mockAddPromotionCode).toHaveBeenCalledWith("SAVE10");
|
|
120
|
+
expect(mockToastAdd).not.toHaveBeenCalled();
|
|
121
|
+
expect(wrapper.find("input").element.value).toBe("");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("shows the translated error from cart.errors when the code is invalid", async () => {
|
|
125
|
+
mockCart.value = {
|
|
126
|
+
errors: {
|
|
127
|
+
"promotion-not-found": {
|
|
128
|
+
promotionCode: "INVALID",
|
|
129
|
+
translatedMessage: 'Gutscheincode "INVALID" existiert nicht.',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const wrapper = await mountSuspended(CheckoutVoucherInput);
|
|
134
|
+
await wrapper.find("input").setValue("INVALID");
|
|
135
|
+
await wrapper.find("button").trigger("click");
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
137
|
+
|
|
138
|
+
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
139
|
+
expect.objectContaining({
|
|
140
|
+
title: "Gutschein ungültig",
|
|
141
|
+
description: 'Gutscheincode "INVALID" existiert nicht.',
|
|
142
|
+
color: "error",
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
expect(wrapper.find("input").element.value).toBe("INVALID");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("shows a generic error toast when addPromotionCode throws", async () => {
|
|
149
|
+
mockAddPromotionCode.mockRejectedValueOnce(new Error("network error"));
|
|
150
|
+
const wrapper = await mountSuspended(CheckoutVoucherInput);
|
|
151
|
+
await wrapper.find("input").setValue("SAVE10");
|
|
152
|
+
await wrapper.find("button").trigger("click");
|
|
153
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
154
|
+
|
|
155
|
+
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
156
|
+
expect.objectContaining({
|
|
157
|
+
title: "Gutschein ungültig",
|
|
158
|
+
color: "error",
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("submits on Enter key", async () => {
|
|
164
|
+
mockCart.value = { errors: {} };
|
|
165
|
+
const wrapper = await mountSuspended(CheckoutVoucherInput);
|
|
166
|
+
await wrapper.find("input").setValue("ENTER10");
|
|
167
|
+
await wrapper.find("input").trigger("keyup.enter");
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
169
|
+
|
|
170
|
+
expect(mockAddPromotionCode).toHaveBeenCalledWith("ENTER10");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("renders applied promotion codes", async () => {
|
|
174
|
+
mockAppliedPromotionCodes.value = [
|
|
175
|
+
{ id: "promo-1", label: "10% Rabatt" },
|
|
176
|
+
{ id: "promo-2", label: "Gratis Versand" },
|
|
177
|
+
];
|
|
178
|
+
const wrapper = await mountSuspended(CheckoutVoucherInput);
|
|
179
|
+
expect(wrapper.text()).toContain("10% Rabatt");
|
|
180
|
+
expect(wrapper.text()).toContain("Gratis Versand");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("calls removeItem when remove button is clicked", async () => {
|
|
184
|
+
const promo = { id: "promo-1", label: "10% Rabatt" };
|
|
185
|
+
mockAppliedPromotionCodes.value = [promo];
|
|
186
|
+
const wrapper = await mountSuspended(CheckoutVoucherInput);
|
|
187
|
+
|
|
188
|
+
const removeButtons = wrapper.findAll(
|
|
189
|
+
'button[aria-label="Gutschein entfernen"]',
|
|
190
|
+
);
|
|
191
|
+
expect(removeButtons).toHaveLength(1);
|
|
192
|
+
await removeButtons[0]!.trigger("click");
|
|
193
|
+
|
|
194
|
+
expect(mockRemoveItem).toHaveBeenCalledWith(promo);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -53,6 +53,7 @@ describe("RegistrationForm", () => {
|
|
|
53
53
|
beforeEach(() => {
|
|
54
54
|
vi.clearAllMocks();
|
|
55
55
|
mockIsLoggedIn.value = false;
|
|
56
|
+
mockGetSuggestions.mockResolvedValue([]);
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
it("renders correctly", async () => {
|
|
@@ -251,8 +252,8 @@ describe("RegistrationForm", () => {
|
|
|
251
252
|
);
|
|
252
253
|
});
|
|
253
254
|
|
|
254
|
-
it("
|
|
255
|
-
mockGetSuggestions.
|
|
255
|
+
it("proceeds with registration even when getSuggestions returns a correction", async () => {
|
|
256
|
+
mockGetSuggestions.mockResolvedValue([
|
|
256
257
|
{
|
|
257
258
|
street: "Corrected Street 123",
|
|
258
259
|
city: "Corrected City",
|
|
@@ -263,11 +264,9 @@ describe("RegistrationForm", () => {
|
|
|
263
264
|
|
|
264
265
|
const wrapper = await mountSuspended(RegistrationForm);
|
|
265
266
|
|
|
266
|
-
// Register as guest to avoid password requirements
|
|
267
267
|
(wrapper.vm as unknown as { state: { guest: boolean } }).state.guest = true;
|
|
268
268
|
await nextTick();
|
|
269
269
|
|
|
270
|
-
// Fill required fields
|
|
271
270
|
await wrapper.find('input[name="firstName"]').setValue("John");
|
|
272
271
|
await wrapper.find('input[name="lastName"]').setValue("Doe");
|
|
273
272
|
await wrapper.find('input[name="email"]').setValue("john@example.com");
|
|
@@ -300,17 +299,10 @@ describe("RegistrationForm", () => {
|
|
|
300
299
|
await wrapper.find("form").trigger("submit");
|
|
301
300
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
302
301
|
|
|
303
|
-
//
|
|
304
|
-
expect(mockRegister).
|
|
305
|
-
|
|
306
|
-
// Should have shown a toast
|
|
307
|
-
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
308
|
-
expect.objectContaining({
|
|
309
|
-
title: "Adresskorrektur vorgeschlagen",
|
|
310
|
-
}),
|
|
311
|
-
);
|
|
302
|
+
// Registration must not be blocked by the correction suggestion
|
|
303
|
+
expect(mockRegister).toHaveBeenCalled();
|
|
312
304
|
|
|
313
|
-
//
|
|
305
|
+
// Correction UI surfaces non-blocking (flushPendingCheck triggers the async check)
|
|
314
306
|
expect(wrapper.text()).toContain(
|
|
315
307
|
"Meinten Sie: Corrected Street 123, 54321 Corrected City?",
|
|
316
308
|
);
|