@shopbite-de/storefront 1.4.0 → 1.5.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/api-types/storeApiSchema.json +4503 -4363
- package/api-types/storeApiTypes.d.ts +73 -0
- package/app/app.vue +11 -3
- package/app/components/Checkout/DeliveryTimeSelect.vue +16 -5
- package/app/composables/useBusinessHours.ts +156 -0
- package/app/composables/useDeliveryTime.ts +34 -10
- package/app/composables/useHolidays.ts +49 -0
- package/nuxt.config.ts +1 -1
- package/package.json +2 -2
- package/playwright.config.ts +2 -2
- package/test/nuxt/useAddToCart.test.ts +10 -7
- package/test/nuxt/useBusinessHours.test.ts +58 -0
- package/test/nuxt/useDeliveryTime.test.ts +118 -8
- package/test/nuxt/useProductConfigurator.test.ts +14 -16
- package/test/nuxt/useProductVariants.test.ts +17 -17
- package/test/nuxt/useProductVariantsZwei.test.ts +7 -7
- package/test/nuxt/useScrollAnimation.test.ts +8 -6
- package/test/nuxt/useShopBiteConfig.test.ts +9 -11
- package/test/nuxt/useTopSellers.test.ts +6 -3
- package/test/nuxt/useWishlistActions.test.ts +26 -10
- package/app/utils/businessHours.ts +0 -119
- package/app/utils/holidays.ts +0 -43
- package/app/utils/storeHours.ts +0 -8
|
@@ -3,14 +3,110 @@ import { mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
|
3
3
|
import { ref } from "vue";
|
|
4
4
|
import { useDeliveryTime } from "~/composables/useDeliveryTime";
|
|
5
5
|
|
|
6
|
-
const { mockDeliveryTime } = vi.hoisted(
|
|
7
|
-
|
|
8
|
-
}
|
|
6
|
+
const { mockDeliveryTime, mockBusinessHours, mockHolidays } = vi.hoisted(
|
|
7
|
+
() => ({
|
|
8
|
+
mockDeliveryTime: { value: 45 },
|
|
9
|
+
mockBusinessHours: {
|
|
10
|
+
value: [
|
|
11
|
+
{ dayOfWeek: 1, openingTime: "11:30", closingTime: "14:30" },
|
|
12
|
+
{ dayOfWeek: 1, openingTime: "17:30", closingTime: "23:00" },
|
|
13
|
+
{ dayOfWeek: 3, openingTime: "11:30", closingTime: "14:30" },
|
|
14
|
+
{ dayOfWeek: 3, openingTime: "17:30", closingTime: "23:00" },
|
|
15
|
+
{ dayOfWeek: 4, openingTime: "11:30", closingTime: "14:30" },
|
|
16
|
+
{ dayOfWeek: 4, openingTime: "17:30", closingTime: "23:00" },
|
|
17
|
+
{ dayOfWeek: 5, openingTime: "11:30", closingTime: "14:30" },
|
|
18
|
+
{ dayOfWeek: 5, openingTime: "17:30", closingTime: "23:00" },
|
|
19
|
+
{ dayOfWeek: 6, openingTime: "17:30", closingTime: "23:30" },
|
|
20
|
+
{ dayOfWeek: 0, openingTime: "11:30", closingTime: "14:30" },
|
|
21
|
+
{ dayOfWeek: 0, openingTime: "17:30", closingTime: "23:00" },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
mockHolidays: { value: [] },
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
9
27
|
|
|
10
28
|
mockNuxtImport("useShopBiteConfig", () => () => ({
|
|
11
29
|
deliveryTime: mockDeliveryTime,
|
|
12
30
|
}));
|
|
13
31
|
|
|
32
|
+
mockNuxtImport("useBusinessHours", () => () => ({
|
|
33
|
+
businessHours: mockBusinessHours,
|
|
34
|
+
getServiceIntervals: (date: Date) => {
|
|
35
|
+
const dayOfWeek = date.getDay();
|
|
36
|
+
return mockBusinessHours.value
|
|
37
|
+
.filter((bh) => bh.dayOfWeek === dayOfWeek)
|
|
38
|
+
.map((bh) => {
|
|
39
|
+
const [startH, startM] = bh.openingTime.split(":").map(Number);
|
|
40
|
+
const [endH, endM] = bh.closingTime.split(":").map(Number);
|
|
41
|
+
const start = new Date(date);
|
|
42
|
+
start.setHours(startH, startM, 0, 0);
|
|
43
|
+
const end = new Date(date);
|
|
44
|
+
end.setHours(endH, endM, 0, 0);
|
|
45
|
+
return { start, end };
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
getEarliestSelectableTime: (
|
|
49
|
+
currentTime: Date,
|
|
50
|
+
currentDeliveryTime: number | null,
|
|
51
|
+
) => {
|
|
52
|
+
const earliest = new Date(currentTime);
|
|
53
|
+
earliest.setMinutes(earliest.getMinutes() + (currentDeliveryTime ?? 30));
|
|
54
|
+
earliest.setSeconds(0, 0);
|
|
55
|
+
return earliest;
|
|
56
|
+
},
|
|
57
|
+
findActiveInterval: (
|
|
58
|
+
currentTime: Date,
|
|
59
|
+
currentDeliveryTime: number | null,
|
|
60
|
+
) => {
|
|
61
|
+
const intervals = [
|
|
62
|
+
{ dayOfWeek: 1, openingTime: "11:30", closingTime: "14:30" },
|
|
63
|
+
{ dayOfWeek: 1, openingTime: "17:30", closingTime: "23:00" },
|
|
64
|
+
{ dayOfWeek: 3, openingTime: "11:30", closingTime: "14:30" },
|
|
65
|
+
{ dayOfWeek: 3, openingTime: "17:30", closingTime: "23:00" },
|
|
66
|
+
{ dayOfWeek: 4, openingTime: "11:30", closingTime: "14:30" },
|
|
67
|
+
{ dayOfWeek: 4, openingTime: "17:30", closingTime: "23:00" },
|
|
68
|
+
{ dayOfWeek: 5, openingTime: "11:30", closingTime: "14:30" },
|
|
69
|
+
{ dayOfWeek: 5, openingTime: "17:30", closingTime: "23:00" },
|
|
70
|
+
{ dayOfWeek: 6, openingTime: "17:30", closingTime: "23:30" },
|
|
71
|
+
{ dayOfWeek: 0, openingTime: "11:30", closingTime: "14:30" },
|
|
72
|
+
{ dayOfWeek: 0, openingTime: "17:30", closingTime: "23:00" },
|
|
73
|
+
]
|
|
74
|
+
.filter((bh) => bh.dayOfWeek === currentTime.getDay())
|
|
75
|
+
.map((bh) => {
|
|
76
|
+
const [startH, startM] = bh.openingTime.split(":").map(Number);
|
|
77
|
+
const [endH, endM] = bh.closingTime.split(":").map(Number);
|
|
78
|
+
const start = new Date(currentTime);
|
|
79
|
+
start.setHours(startH, startM, 0, 0);
|
|
80
|
+
const end = new Date(currentTime);
|
|
81
|
+
end.setHours(endH, endM, 0, 0);
|
|
82
|
+
return { start, end };
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const earliest = new Date(currentTime);
|
|
86
|
+
earliest.setMinutes(earliest.getMinutes() + (currentDeliveryTime ?? 30));
|
|
87
|
+
earliest.setSeconds(0, 0);
|
|
88
|
+
|
|
89
|
+
if (intervals.length === 0) return null;
|
|
90
|
+
const current = intervals.find(
|
|
91
|
+
(i) => earliest >= i.start && earliest <= i.end,
|
|
92
|
+
);
|
|
93
|
+
if (current) return current;
|
|
94
|
+
return intervals.find((i) => i.start > earliest) ?? null;
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
mockNuxtImport("useHolidays", () => () => ({
|
|
99
|
+
isClosedHoliday: (date: Date) => {
|
|
100
|
+
if (!mockHolidays.value) return undefined;
|
|
101
|
+
const formattedDate = date.toISOString().split("T")[0];
|
|
102
|
+
return mockHolidays.value.some((h: any) => {
|
|
103
|
+
const start = h.start.split("T")[0];
|
|
104
|
+
const end = h.end.split("T")[0];
|
|
105
|
+
return formattedDate >= start && formattedDate <= end;
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
}));
|
|
109
|
+
|
|
14
110
|
describe("useDeliveryTime", () => {
|
|
15
111
|
const now = ref(new Date("2023-10-27T12:00:00")); // A Friday
|
|
16
112
|
|
|
@@ -42,7 +138,9 @@ describe("useDeliveryTime", () => {
|
|
|
42
138
|
|
|
43
139
|
it("should return error for invalid format", () => {
|
|
44
140
|
const { validate } = useDeliveryTime(now);
|
|
45
|
-
expect(validate("invalid")).toBe(
|
|
141
|
+
expect(validate("invalid")).toBe(
|
|
142
|
+
"Bitte eine gültige Uhrzeit im Format HH:MM eingeben.",
|
|
143
|
+
);
|
|
46
144
|
});
|
|
47
145
|
|
|
48
146
|
it("should return error for time before minTime", () => {
|
|
@@ -52,10 +150,22 @@ describe("useDeliveryTime", () => {
|
|
|
52
150
|
const [hours, mins] = minTime.value.split(":").map(Number);
|
|
53
151
|
const earlyTime = new Date(now.value);
|
|
54
152
|
earlyTime.setHours(hours as number);
|
|
55
|
-
earlyTime.setMinutes(mins as number - 5);
|
|
56
|
-
const earlyTimeStr =
|
|
57
|
-
|
|
58
|
-
|
|
153
|
+
earlyTime.setMinutes((mins as number) - 5);
|
|
154
|
+
const earlyTimeStr =
|
|
155
|
+
earlyTime.getHours().toString().padStart(2, "0") +
|
|
156
|
+
":" +
|
|
157
|
+
earlyTime.getMinutes().toString().padStart(2, "0");
|
|
158
|
+
|
|
159
|
+
expect(validate(earlyTimeStr)).toContain(
|
|
160
|
+
"vor dem frühestmöglichen Zeitpunkt",
|
|
161
|
+
);
|
|
59
162
|
}
|
|
60
163
|
});
|
|
164
|
+
|
|
165
|
+
it("should return loading message when holidays are not loaded", () => {
|
|
166
|
+
mockHolidays.value = null;
|
|
167
|
+
const { helperText } = useDeliveryTime(now);
|
|
168
|
+
expect(helperText.value).toBe("Lade Informationen...");
|
|
169
|
+
mockHolidays.value = []; // Reset for other tests
|
|
170
|
+
});
|
|
61
171
|
});
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import { useProductConfigurator } from "../../app/composables/useProductConfigurator";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
mockInvoke,
|
|
7
|
-
mockConfigurator,
|
|
8
|
-
mockProduct,
|
|
9
|
-
} = vi.hoisted(() => ({
|
|
4
|
+
const { mockInvoke, mockConfigurator, mockProduct } = vi.hoisted(() => ({
|
|
10
5
|
mockInvoke: vi.fn(),
|
|
11
6
|
mockConfigurator: { value: [] },
|
|
12
7
|
mockProduct: { value: { id: "p1", optionIds: [], options: [] } },
|
|
@@ -35,7 +30,7 @@ describe("useProductConfigurator", () => {
|
|
|
35
30
|
mockProduct.value = {
|
|
36
31
|
id: "p1",
|
|
37
32
|
optionIds: [],
|
|
38
|
-
options: []
|
|
33
|
+
options: [],
|
|
39
34
|
} as any;
|
|
40
35
|
});
|
|
41
36
|
|
|
@@ -44,14 +39,14 @@ describe("useProductConfigurator", () => {
|
|
|
44
39
|
{
|
|
45
40
|
id: "g1",
|
|
46
41
|
name: "Size",
|
|
47
|
-
options: [{ id: "o1", name: "Small" }]
|
|
48
|
-
}
|
|
42
|
+
options: [{ id: "o1", name: "Small" }],
|
|
43
|
+
},
|
|
49
44
|
] as any;
|
|
50
45
|
mockProduct.value = {
|
|
51
46
|
id: "p1-v1",
|
|
52
47
|
parentId: "p1",
|
|
53
48
|
optionIds: ["o1"],
|
|
54
|
-
options: [{ id: "o1" }]
|
|
49
|
+
options: [{ id: "o1" }],
|
|
55
50
|
} as any;
|
|
56
51
|
|
|
57
52
|
const { isLoadingOptions } = useProductConfigurator();
|
|
@@ -62,23 +57,26 @@ describe("useProductConfigurator", () => {
|
|
|
62
57
|
mockProduct.value = { parentId: "parent-1" } as any;
|
|
63
58
|
mockInvoke.mockResolvedValue({
|
|
64
59
|
data: {
|
|
65
|
-
elements: [{ id: "variant-1" }]
|
|
66
|
-
}
|
|
60
|
+
elements: [{ id: "variant-1" }],
|
|
61
|
+
},
|
|
67
62
|
});
|
|
68
63
|
|
|
69
64
|
const { findVariantForSelectedOptions } = useProductConfigurator();
|
|
70
|
-
const result = await findVariantForSelectedOptions({
|
|
65
|
+
const result = await findVariantForSelectedOptions({ Size: "o1" });
|
|
71
66
|
|
|
72
|
-
expect(mockInvoke).toHaveBeenCalledWith(
|
|
67
|
+
expect(mockInvoke).toHaveBeenCalledWith(
|
|
68
|
+
"readProduct post /product",
|
|
69
|
+
expect.any(Object),
|
|
70
|
+
);
|
|
73
71
|
expect(result).toEqual({ id: "variant-1" });
|
|
74
72
|
});
|
|
75
73
|
|
|
76
74
|
it("should return undefined on error in findVariantForSelectedOptions", async () => {
|
|
77
75
|
mockInvoke.mockRejectedValue(new Error("API Error"));
|
|
78
|
-
const consoleSpy = vi.spyOn(console,
|
|
76
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
79
77
|
|
|
80
78
|
const { findVariantForSelectedOptions } = useProductConfigurator();
|
|
81
|
-
const result = await findVariantForSelectedOptions({
|
|
79
|
+
const result = await findVariantForSelectedOptions({ Size: "o1" });
|
|
82
80
|
|
|
83
81
|
expect(result).toBeUndefined();
|
|
84
82
|
expect(consoleSpy).toHaveBeenCalled();
|
|
@@ -18,10 +18,10 @@ describe("useProductVariants", () => {
|
|
|
18
18
|
group: {
|
|
19
19
|
id: "group-size",
|
|
20
20
|
name: "Size",
|
|
21
|
-
translated: { name: "Größe" }
|
|
21
|
+
translated: { name: "Größe" },
|
|
22
22
|
},
|
|
23
|
-
translated: { name: "Klein" }
|
|
24
|
-
}
|
|
23
|
+
translated: { name: "Klein" },
|
|
24
|
+
},
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
option: {
|
|
@@ -30,10 +30,10 @@ describe("useProductVariants", () => {
|
|
|
30
30
|
group: {
|
|
31
31
|
id: "group-size",
|
|
32
32
|
name: "Size",
|
|
33
|
-
translated: { name: "Größe" }
|
|
33
|
+
translated: { name: "Größe" },
|
|
34
34
|
},
|
|
35
|
-
translated: { name: "Groß" }
|
|
36
|
-
}
|
|
35
|
+
translated: { name: "Groß" },
|
|
36
|
+
},
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
option: {
|
|
@@ -42,22 +42,22 @@ describe("useProductVariants", () => {
|
|
|
42
42
|
group: {
|
|
43
43
|
id: "group-color",
|
|
44
44
|
name: "Color",
|
|
45
|
-
translated: { name: "Farbe" }
|
|
45
|
+
translated: { name: "Farbe" },
|
|
46
46
|
},
|
|
47
|
-
translated: { name: "Rot" }
|
|
48
|
-
}
|
|
49
|
-
}
|
|
47
|
+
translated: { name: "Rot" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
50
|
]);
|
|
51
51
|
|
|
52
52
|
const { variants } = useProductVariants(settings as any);
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
expect(variants.value["group-size"]).toBeDefined();
|
|
55
55
|
expect(variants.value["group-size"].name).toBe("Größe");
|
|
56
56
|
expect(variants.value["group-size"].options).toHaveLength(2);
|
|
57
57
|
expect(variants.value["group-size"].options[0]).toEqual({
|
|
58
58
|
label: "Klein",
|
|
59
59
|
value: "opt-1",
|
|
60
|
-
productId: "opt-1"
|
|
60
|
+
productId: "opt-1",
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
expect(variants.value["group-color"]).toBeDefined();
|
|
@@ -71,16 +71,16 @@ describe("useProductVariants", () => {
|
|
|
71
71
|
option: {
|
|
72
72
|
id: "opt-1",
|
|
73
73
|
name: "Small",
|
|
74
|
-
group: { id: "group-size", name: "Size" }
|
|
75
|
-
}
|
|
74
|
+
group: { id: "group-size", name: "Size" },
|
|
75
|
+
},
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
option: {
|
|
79
79
|
id: "opt-1",
|
|
80
80
|
name: "Small",
|
|
81
|
-
group: { id: "group-size", name: "Size" }
|
|
82
|
-
}
|
|
83
|
-
}
|
|
81
|
+
group: { id: "group-size", name: "Size" },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
84
|
]);
|
|
85
85
|
|
|
86
86
|
const { variants } = useProductVariants(settings as any);
|
|
@@ -17,13 +17,13 @@ describe("useProductVariantsZwei", () => {
|
|
|
17
17
|
translated: { name: "Größe" },
|
|
18
18
|
options: [
|
|
19
19
|
{ id: "opt-1", name: "Small", translated: { name: "Klein" } },
|
|
20
|
-
{ id: "opt-2", name: "Large", translated: { name: "Groß" } }
|
|
21
|
-
]
|
|
22
|
-
}
|
|
20
|
+
{ id: "opt-2", name: "Large", translated: { name: "Groß" } },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
25
|
const { variants } = useProductVariantsZwei(settings);
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
expect(variants.value["group-size"]).toBeDefined();
|
|
28
28
|
expect(variants.value["group-size"]?.name).toBe("Größe");
|
|
29
29
|
expect(variants.value["group-size"]?.options).toHaveLength(2);
|
|
@@ -37,9 +37,9 @@ describe("useProductVariantsZwei", () => {
|
|
|
37
37
|
name: "Size",
|
|
38
38
|
options: [
|
|
39
39
|
{ id: "opt-1", name: "Small" },
|
|
40
|
-
{ id: "opt-1", name: "Small" }
|
|
41
|
-
]
|
|
42
|
-
}
|
|
40
|
+
{ id: "opt-1", name: "Small" },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
43
|
]);
|
|
44
44
|
|
|
45
45
|
const { variants } = useProductVariantsZwei(settings);
|
|
@@ -15,12 +15,14 @@ describe("useScrollAnimation", () => {
|
|
|
15
15
|
disconnectMock = vi.fn();
|
|
16
16
|
|
|
17
17
|
// Mock IntersectionObserver
|
|
18
|
-
global.IntersectionObserver = vi
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
global.IntersectionObserver = vi
|
|
19
|
+
.fn()
|
|
20
|
+
.mockImplementation(function (callback) {
|
|
21
|
+
intersectionCallback = callback;
|
|
22
|
+
this.observe = observeMock;
|
|
23
|
+
this.unobserve = unobserveMock;
|
|
24
|
+
this.disconnect = disconnectMock;
|
|
25
|
+
}) as any;
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
it("should initialize with isVisible false", () => {
|
|
@@ -2,15 +2,13 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
2
2
|
import { mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import { useShopBiteConfig } from "~/composables/useShopBiteConfig";
|
|
4
4
|
|
|
5
|
-
const {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
mockIsCheckoutEnabled: { value: false },
|
|
13
|
-
}));
|
|
5
|
+
const { mockInvoke, mockDeliveryTime, mockIsCheckoutEnabled } = vi.hoisted(
|
|
6
|
+
() => ({
|
|
7
|
+
mockInvoke: vi.fn(),
|
|
8
|
+
mockDeliveryTime: { value: 0 },
|
|
9
|
+
mockIsCheckoutEnabled: { value: false },
|
|
10
|
+
}),
|
|
11
|
+
);
|
|
14
12
|
|
|
15
13
|
mockNuxtImport("useShopwareContext", () => () => ({
|
|
16
14
|
apiClient: {
|
|
@@ -43,8 +41,8 @@ describe("useShopBiteConfig", () => {
|
|
|
43
41
|
mockInvoke.mockResolvedValue({
|
|
44
42
|
data: {
|
|
45
43
|
deliveryTime: 45,
|
|
46
|
-
isCheckoutEnabled: true
|
|
47
|
-
}
|
|
44
|
+
isCheckoutEnabled: true,
|
|
45
|
+
},
|
|
48
46
|
});
|
|
49
47
|
|
|
50
48
|
const { refresh, deliveryTime, isCheckoutEnabled } = useShopBiteConfig();
|
|
@@ -30,15 +30,18 @@ describe("useTopSellers", () => {
|
|
|
30
30
|
const { loadTopSellers } = useTopSellers();
|
|
31
31
|
const result = await loadTopSellers();
|
|
32
32
|
|
|
33
|
-
expect(mockInvoke).toHaveBeenCalledWith(
|
|
33
|
+
expect(mockInvoke).toHaveBeenCalledWith(
|
|
34
|
+
"getTopSellers post /product",
|
|
35
|
+
expect.any(Object),
|
|
36
|
+
);
|
|
34
37
|
expect(result).toEqual(mockElements);
|
|
35
38
|
});
|
|
36
39
|
|
|
37
40
|
it("should return empty array on error", async () => {
|
|
38
41
|
mockInvoke.mockRejectedValue(new Error("Network error"));
|
|
39
|
-
|
|
42
|
+
|
|
40
43
|
// Silence console.error for this test
|
|
41
|
-
const consoleSpy = vi.spyOn(console,
|
|
44
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
42
45
|
|
|
43
46
|
const { loadTopSellers } = useTopSellers();
|
|
44
47
|
const result = await loadTopSellers();
|
|
@@ -2,7 +2,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
2
2
|
import { mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import { useWishlistActions } from "../../app/composables/useWishlistActions";
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
const {
|
|
7
6
|
mockAddProducts,
|
|
8
7
|
mockRefreshCart,
|
|
@@ -53,7 +52,9 @@ describe("useWishlistActions", () => {
|
|
|
53
52
|
const { clearWishlistHandler, isLoading } = useWishlistActions();
|
|
54
53
|
await clearWishlistHandler();
|
|
55
54
|
expect(mockClearWishlist).toHaveBeenCalled();
|
|
56
|
-
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
55
|
+
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
56
|
+
expect.objectContaining({ title: "Merkliste geleert" }),
|
|
57
|
+
);
|
|
57
58
|
expect(isLoading.value).toBe(false);
|
|
58
59
|
});
|
|
59
60
|
|
|
@@ -63,11 +64,18 @@ describe("useWishlistActions", () => {
|
|
|
63
64
|
|
|
64
65
|
await addSingleItemToCart(mockProduct);
|
|
65
66
|
|
|
66
|
-
expect(mockAddProducts).toHaveBeenCalledWith([
|
|
67
|
+
expect(mockAddProducts).toHaveBeenCalledWith([
|
|
68
|
+
{ id: "prod-1", quantity: 1, type: "product" },
|
|
69
|
+
]);
|
|
67
70
|
expect(mockRefreshCart).toHaveBeenCalled();
|
|
68
71
|
expect(mockTriggerProductAdded).toHaveBeenCalled();
|
|
69
|
-
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
70
|
-
|
|
72
|
+
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
73
|
+
expect.objectContaining({ title: "In den Warenkorb gelegt" }),
|
|
74
|
+
);
|
|
75
|
+
expect(mockTrackEvent).toHaveBeenCalledWith(
|
|
76
|
+
"add_to_cart",
|
|
77
|
+
expect.any(Object),
|
|
78
|
+
);
|
|
71
79
|
});
|
|
72
80
|
|
|
73
81
|
it("should warn when adding a base product with variants", async () => {
|
|
@@ -77,7 +85,9 @@ describe("useWishlistActions", () => {
|
|
|
77
85
|
await addSingleItemToCart(baseProduct);
|
|
78
86
|
|
|
79
87
|
expect(mockAddProducts).not.toHaveBeenCalled();
|
|
80
|
-
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
88
|
+
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
89
|
+
expect.objectContaining({ title: "Variante erforderlich" }),
|
|
90
|
+
);
|
|
81
91
|
});
|
|
82
92
|
|
|
83
93
|
it("should add all items to cart", async () => {
|
|
@@ -94,7 +104,9 @@ describe("useWishlistActions", () => {
|
|
|
94
104
|
{ id: "p1", quantity: 1, type: "product" },
|
|
95
105
|
{ id: "p2", quantity: 1, type: "product" },
|
|
96
106
|
]);
|
|
97
|
-
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
107
|
+
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
108
|
+
expect.objectContaining({ title: "Produkte hinzugefügt" }),
|
|
109
|
+
);
|
|
98
110
|
expect(isAddingToCart.value).toBe(false);
|
|
99
111
|
});
|
|
100
112
|
|
|
@@ -117,8 +129,12 @@ describe("useWishlistActions", () => {
|
|
|
117
129
|
expect(mockAddProducts).toHaveBeenCalledWith([
|
|
118
130
|
{ id: "p1", quantity: 1, type: "product" },
|
|
119
131
|
]);
|
|
120
|
-
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
133
|
+
expect.objectContaining({
|
|
134
|
+
description: expect.stringContaining(
|
|
135
|
+
"1 Produkte hinzugefügt. 1 Produkt(e) übersprungen",
|
|
136
|
+
),
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
123
139
|
});
|
|
124
140
|
});
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { setTime } from "./time";
|
|
2
|
-
import { isClosedHoliday } from "~/utils/holidays";
|
|
3
|
-
|
|
4
|
-
export type ServiceInterval = { start: Date; end: Date };
|
|
5
|
-
|
|
6
|
-
export function isTuesday(date: Date): boolean {
|
|
7
|
-
return date.getDay() === 2;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function isSaturday(date: Date): boolean {
|
|
11
|
-
return date.getDay() === 6;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function getServiceIntervals(date: Date): Array<ServiceInterval> {
|
|
15
|
-
if (isTuesday(date)) return [];
|
|
16
|
-
|
|
17
|
-
const lunchStart = setTime(date, 11, 30);
|
|
18
|
-
const lunchEnd = setTime(date, 14, 30);
|
|
19
|
-
const dinnerStart = setTime(date, 17, 30);
|
|
20
|
-
const dinnerEnd = isSaturday(date)
|
|
21
|
-
? setTime(date, 23, 30)
|
|
22
|
-
: setTime(date, 23, 0);
|
|
23
|
-
|
|
24
|
-
if (isSaturday(date)) {
|
|
25
|
-
// Saturday: only dinner, no lunch
|
|
26
|
-
return [{ start: dinnerStart, end: dinnerEnd }];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return [
|
|
30
|
-
{ start: lunchStart, end: lunchEnd },
|
|
31
|
-
{ start: dinnerStart, end: dinnerEnd },
|
|
32
|
-
];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function getEarliestSelectableTime(
|
|
36
|
-
currentTime: Date,
|
|
37
|
-
currentDeliveryTime: number | null,
|
|
38
|
-
): Date {
|
|
39
|
-
const earliest = new Date(currentTime);
|
|
40
|
-
earliest.setMinutes(earliest.getMinutes() + (currentDeliveryTime ?? 30));
|
|
41
|
-
earliest.setSeconds(0, 0);
|
|
42
|
-
return earliest;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function getNextOpeningTime(now: Ref<Date>): string | null {
|
|
46
|
-
const currentDate = now.value;
|
|
47
|
-
|
|
48
|
-
// Check if closed for holiday
|
|
49
|
-
if (isClosedHoliday(currentDate)) {
|
|
50
|
-
return "13.08."; // Based on your existing code
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Check today's intervals
|
|
54
|
-
const todayIntervals = getServiceIntervals(currentDate);
|
|
55
|
-
const currentTime = currentDate.getTime();
|
|
56
|
-
|
|
57
|
-
// Find next opening today
|
|
58
|
-
for (const interval of todayIntervals) {
|
|
59
|
-
if (interval.start.getTime() > currentTime) {
|
|
60
|
-
const hours = interval.start.getHours().toString().padStart(2, "0");
|
|
61
|
-
const minutes = interval.start.getMinutes().toString().padStart(2, "0");
|
|
62
|
-
return `${hours}:${minutes} Uhr`;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Check tomorrow
|
|
67
|
-
const tomorrow = new Date(currentDate);
|
|
68
|
-
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
69
|
-
tomorrow.setHours(0, 0, 0, 0);
|
|
70
|
-
|
|
71
|
-
// Try up to 7 days ahead to find next opening
|
|
72
|
-
for (let i = 0; i < 7; i++) {
|
|
73
|
-
const checkDate = new Date(tomorrow);
|
|
74
|
-
checkDate.setDate(checkDate.getDate() + i);
|
|
75
|
-
|
|
76
|
-
const intervals = getServiceIntervals(checkDate);
|
|
77
|
-
if (intervals.length > 0) {
|
|
78
|
-
const nextOpen = intervals[0].start;
|
|
79
|
-
const dayName = [
|
|
80
|
-
"Sonntag",
|
|
81
|
-
"Montag",
|
|
82
|
-
"Dienstag",
|
|
83
|
-
"Mittwoch",
|
|
84
|
-
"Donnerstag",
|
|
85
|
-
"Freitag",
|
|
86
|
-
"Samstag",
|
|
87
|
-
][nextOpen.getDay()];
|
|
88
|
-
const hours = nextOpen.getHours().toString().padStart(2, "0");
|
|
89
|
-
const minutes = nextOpen.getMinutes().toString().padStart(2, "0");
|
|
90
|
-
|
|
91
|
-
if (i === 0) {
|
|
92
|
-
return `morgen um ${hours}:${minutes} Uhr`;
|
|
93
|
-
}
|
|
94
|
-
return `${dayName} um ${hours}:${minutes} Uhr`;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function findActiveInterval(
|
|
102
|
-
currentTime: Date,
|
|
103
|
-
currentDeliveryTime: number | null,
|
|
104
|
-
): ServiceInterval | null {
|
|
105
|
-
const intervals = getServiceIntervals(currentTime);
|
|
106
|
-
const earliest = getEarliestSelectableTime(
|
|
107
|
-
currentTime,
|
|
108
|
-
currentDeliveryTime ?? 30,
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
if (intervals.length === 0) return null;
|
|
112
|
-
|
|
113
|
-
const current = intervals.find(
|
|
114
|
-
(interval) => earliest >= interval.start && earliest <= interval.end,
|
|
115
|
-
);
|
|
116
|
-
if (current) return current;
|
|
117
|
-
|
|
118
|
-
return intervals.find((interval) => interval.start > earliest) ?? null;
|
|
119
|
-
}
|
package/app/utils/holidays.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
export function isClosedHoliday(date: Date): boolean {
|
|
2
|
-
// Format date as YYYY-MM-DD for comparison
|
|
3
|
-
const formattedDate = formatDateYYYYMMDD(date);
|
|
4
|
-
|
|
5
|
-
// List of holidays (YYYY-MM-DD format)
|
|
6
|
-
const holidays = [
|
|
7
|
-
"2025-07-21",
|
|
8
|
-
"2025-07-22",
|
|
9
|
-
"2025-07-23",
|
|
10
|
-
"2025-07-24",
|
|
11
|
-
"2025-07-25",
|
|
12
|
-
"2025-07-26",
|
|
13
|
-
"2025-07-27",
|
|
14
|
-
"2025-07-28",
|
|
15
|
-
"2025-07-29",
|
|
16
|
-
"2025-07-30",
|
|
17
|
-
"2025-07-31",
|
|
18
|
-
"2025-08-01",
|
|
19
|
-
"2025-08-02",
|
|
20
|
-
"2025-08-03",
|
|
21
|
-
"2025-08-04",
|
|
22
|
-
"2025-08-05",
|
|
23
|
-
"2025-08-06",
|
|
24
|
-
"2025-08-07",
|
|
25
|
-
"2025-08-08",
|
|
26
|
-
"2025-08-09",
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
return holidays.includes(formattedDate);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Formats a date as YYYY-MM-DD
|
|
34
|
-
* @param date The date to format
|
|
35
|
-
* @returns The formatted date string
|
|
36
|
-
*/
|
|
37
|
-
function formatDateYYYYMMDD(date: Date): string {
|
|
38
|
-
const year = date.getFullYear();
|
|
39
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
40
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
41
|
-
|
|
42
|
-
return `${year}-${month}-${day}`;
|
|
43
|
-
}
|
package/app/utils/storeHours.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// utils/storeHours.ts
|
|
2
|
-
import { getServiceIntervals } from "~/utils/businessHours";
|
|
3
|
-
|
|
4
|
-
export function isStoreOpen(date: Date = new Date()): boolean {
|
|
5
|
-
const intervals = getServiceIntervals(date);
|
|
6
|
-
if (intervals.length === 0) return false;
|
|
7
|
-
return intervals.some(({ start, end }) => date >= start && date <= end);
|
|
8
|
-
}
|