@shopbite-de/storefront 1.16.0 → 1.17.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/.claude/settings.local.json +21 -0
- package/.env.example +0 -4
- package/.github/workflows/build.yaml +14 -8
- package/.github/workflows/ci.yaml +125 -42
- package/.nuxtrc +1 -1
- package/CLAUDE.md +106 -0
- package/app/app.vue +53 -34
- package/app/components/AddToWishlist.vue +0 -3
- package/app/components/Address/Form.vue +5 -2
- package/app/components/Category/Listing.vue +5 -4
- package/app/components/Checkout/DeliveryTimeSelect.vue +4 -2
- package/app/components/Contact/Form.vue +3 -2
- package/app/components/Hero.vue +1 -1
- package/app/components/ImageGallery.vue +1 -1
- package/app/components/Navigation/DesktopLeft.vue +2 -1
- package/app/components/Order/Detail.vue +2 -2
- package/app/components/Product/Card.vue +14 -8
- package/app/components/SalesChannelSwitch.vue +5 -3
- package/app/components/User/LoginForm.vue +1 -4
- package/app/components/User/RegistrationForm.vue +5 -2
- package/app/composables/useBusinessHours.ts +11 -4
- package/app/composables/useCategory.ts +1 -0
- package/app/composables/useTopSellers.ts +2 -2
- package/app/layouts/account.vue +0 -1
- package/app/pages/anmelden.vue +8 -4
- package/app/pages/c/[...all].vue +2 -1
- package/app/pages/konto/adressen.vue +4 -1
- package/app/pages/konto/bestellung/[id].vue +14 -2
- package/app/pages/konto/profil.vue +5 -1
- package/app/utils/formatDate.ts +2 -1
- package/content.config.ts +1 -2
- package/eslint.config.mjs +10 -6
- package/node.dockerfile +7 -6
- package/nuxt.config.ts +17 -3
- package/package.json +38 -32
- package/renovate.json +9 -1
- package/server/api/address/autocomplete.get.ts +3 -2
- package/test/e2e/simple-checkout-as-recurring-customer.test.ts +1 -1
- package/test/nuxt/AddressFields.test.ts +12 -3
- package/test/nuxt/ContactForm.test.ts +31 -11
- package/test/nuxt/HeaderRight.test.ts +15 -6
- package/test/nuxt/LoginForm.test.ts +41 -23
- package/test/nuxt/PaymentAndDelivery.test.ts +2 -2
- package/test/nuxt/RegistrationForm.test.ts +6 -3
- package/test/nuxt/registrationSchema.test.ts +8 -8
- package/test/nuxt/useAddToCart.test.ts +5 -4
- package/test/nuxt/useAddressAutocomplete.test.ts +2 -0
- package/test/nuxt/useBusinessHours.test.ts +5 -5
- package/test/nuxt/useDeliveryTime.test.ts +17 -9
- package/test/nuxt/useProductConfigurator.test.ts +6 -4
- package/test/nuxt/useProductVariants.test.ts +16 -10
- package/test/nuxt/useProductVariantsZwei.test.ts +6 -2
- package/test/nuxt/useScrollAnimation.test.ts +16 -13
- package/test/nuxt/useTopSellers.test.ts +2 -2
- package/test/nuxt/useWishlistActions.test.ts +4 -3
- package/test/unit/useCategorySeo.spec.ts +26 -12
- package/tsconfig.json +21 -1
- package/app/middleware/trailing-slash.global.ts +0 -19
- package/server/utils/shopware/adminApiClient.ts +0 -24
- package/test/unit/sales-channels.test.ts +0 -66
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import { mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
3
|
-
import { useTopSellers } from "
|
|
3
|
+
import { useTopSellers } from "~/composables/useTopSellers";
|
|
4
4
|
|
|
5
5
|
const { mockInvoke } = vi.hoisted(() => ({
|
|
6
6
|
mockInvoke: vi.fn(),
|
|
@@ -31,7 +31,7 @@ describe("useTopSellers", () => {
|
|
|
31
31
|
const result = await loadTopSellers();
|
|
32
32
|
|
|
33
33
|
expect(mockInvoke).toHaveBeenCalledWith(
|
|
34
|
-
"
|
|
34
|
+
"readProduct post /product",
|
|
35
35
|
expect.any(Object),
|
|
36
36
|
);
|
|
37
37
|
expect(result).toEqual(mockElements);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import type { Schemas } from "#shopware";
|
|
4
5
|
|
|
5
6
|
const {
|
|
6
7
|
mockAddProducts,
|
|
@@ -44,7 +45,7 @@ describe("useWishlistActions", () => {
|
|
|
44
45
|
id: "prod-1",
|
|
45
46
|
translated: { name: "Test Product" },
|
|
46
47
|
productNumber: "SW123",
|
|
47
|
-
} as
|
|
48
|
+
} as unknown as Schemas["Product"];
|
|
48
49
|
|
|
49
50
|
beforeEach(() => {
|
|
50
51
|
vi.clearAllMocks();
|
|
@@ -93,7 +94,7 @@ describe("useWishlistActions", () => {
|
|
|
93
94
|
const products = [
|
|
94
95
|
{ id: "p1", translated: { name: "P1" } },
|
|
95
96
|
{ id: "p2", translated: { name: "P2" } },
|
|
96
|
-
] as
|
|
97
|
+
] as unknown as Schemas["Product"][];
|
|
97
98
|
const { addAllItemsToCart, isAddingToCart } = useWishlistActions();
|
|
98
99
|
mockAddProducts.mockResolvedValue({ id: "cart-1" });
|
|
99
100
|
|
|
@@ -119,7 +120,7 @@ describe("useWishlistActions", () => {
|
|
|
119
120
|
const products = [
|
|
120
121
|
{ id: "p1", translated: { name: "P1" } },
|
|
121
122
|
{ id: "p2", childCount: 1, translated: { name: "P2" } },
|
|
122
|
-
] as
|
|
123
|
+
] as unknown as Schemas["Product"][];
|
|
123
124
|
const { addAllItemsToCart } = useWishlistActions();
|
|
124
125
|
mockAddProducts.mockResolvedValue({ id: "cart-1" });
|
|
125
126
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import { ref } from "vue";
|
|
3
3
|
|
|
4
|
+
// Re-import the mocks for assertions
|
|
5
|
+
import { useHead, useSeoMeta } from "#imports";
|
|
6
|
+
|
|
4
7
|
// Create shared mocks to be exported by both '#imports' and '#app' in a hoisted-safe way
|
|
5
8
|
const shared = vi.hoisted(() => ({
|
|
6
9
|
useHead: vi.fn(),
|
|
@@ -41,24 +44,25 @@ vi.mock("#app", async () => {
|
|
|
41
44
|
};
|
|
42
45
|
});
|
|
43
46
|
|
|
44
|
-
// Re-import the mocks for assertions
|
|
45
|
-
import { useHead, useSeoMeta } from "#imports";
|
|
46
|
-
|
|
47
47
|
// Target under test will be dynamically imported after setting up globals
|
|
48
|
-
let useCategorySeo: (
|
|
48
|
+
let useCategorySeo: (
|
|
49
|
+
arg: unknown,
|
|
50
|
+
) => ReturnType<
|
|
51
|
+
(typeof import("../../app/composables/useCategorySeo"))["useCategorySeo"]
|
|
52
|
+
>;
|
|
49
53
|
|
|
50
54
|
describe("useCategorySeo", () => {
|
|
51
55
|
beforeEach(async () => {
|
|
52
56
|
vi.clearAllMocks();
|
|
53
57
|
// Provide globals for auto-imported functions (when not transformed in unit env)
|
|
54
58
|
const vue = await import("vue");
|
|
55
|
-
(globalThis as
|
|
56
|
-
(globalThis as
|
|
57
|
-
(globalThis as
|
|
59
|
+
(globalThis as Record<string, unknown>).computed = vue.computed;
|
|
60
|
+
(globalThis as Record<string, unknown>).ref = vue.ref;
|
|
61
|
+
(globalThis as Record<string, unknown>).useRuntimeConfig = () => ({
|
|
58
62
|
public: { site: { name: "My Store" }, storeUrl: "https://example.com" },
|
|
59
63
|
});
|
|
60
|
-
(globalThis as
|
|
61
|
-
(globalThis as
|
|
64
|
+
(globalThis as Record<string, unknown>).useHead = useHead;
|
|
65
|
+
(globalThis as Record<string, unknown>).useSeoMeta = useSeoMeta;
|
|
62
66
|
|
|
63
67
|
// Dynamic import after globals are ready
|
|
64
68
|
useCategorySeo = (await import("../../app/composables/useCategorySeo"))
|
|
@@ -66,7 +70,17 @@ describe("useCategorySeo", () => {
|
|
|
66
70
|
});
|
|
67
71
|
|
|
68
72
|
it("computes core SEO refs and injects head tags", () => {
|
|
69
|
-
const category = ref<
|
|
73
|
+
const category = ref<{
|
|
74
|
+
translated?: {
|
|
75
|
+
metaTitle?: string;
|
|
76
|
+
metaDescription?: string;
|
|
77
|
+
breadcrumb?: string[];
|
|
78
|
+
name?: string;
|
|
79
|
+
};
|
|
80
|
+
seoUrl?: string;
|
|
81
|
+
active?: boolean;
|
|
82
|
+
media?: { url?: string };
|
|
83
|
+
}>({
|
|
70
84
|
translated: {
|
|
71
85
|
metaTitle: "Pizza & Pasta",
|
|
72
86
|
metaDescription: "Leckere Pizza und Pasta bestellen",
|
|
@@ -96,7 +110,7 @@ describe("useCategorySeo", () => {
|
|
|
96
110
|
useHead as unknown as ReturnType<typeof vi.fn>,
|
|
97
111
|
).toHaveBeenCalledTimes(1);
|
|
98
112
|
const headArg = (useHead as unknown as ReturnType<typeof vi.fn>).mock
|
|
99
|
-
.calls[0][0]
|
|
113
|
+
.calls[0]![0]!;
|
|
100
114
|
|
|
101
115
|
// Canonical link
|
|
102
116
|
const link = headArg.link?.[0];
|
|
@@ -116,7 +130,7 @@ describe("useCategorySeo", () => {
|
|
|
116
130
|
});
|
|
117
131
|
|
|
118
132
|
it("sets robots to noindex when category is inactive", () => {
|
|
119
|
-
const category = ref
|
|
133
|
+
const category = ref({
|
|
120
134
|
translated: { name: "Salate" },
|
|
121
135
|
active: false,
|
|
122
136
|
seoUrl: "/c/salate",
|
package/tsconfig.json
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
-
// https://v3.nuxtjs.org/concepts/typescript
|
|
3
2
|
"extends": "./.nuxt/tsconfig.json",
|
|
3
|
+
"include": [
|
|
4
|
+
".nuxt/nuxt.d.ts",
|
|
5
|
+
".nuxt/nuxt.node.d.ts",
|
|
6
|
+
"app/**/*",
|
|
7
|
+
"server/**/*",
|
|
8
|
+
"shared/**/*",
|
|
9
|
+
"modules/**/*",
|
|
10
|
+
"layers/**/*",
|
|
11
|
+
"test/**/*",
|
|
12
|
+
"tests/**/*",
|
|
13
|
+
"*.d.ts",
|
|
14
|
+
"nuxt.config.*",
|
|
15
|
+
".config/nuxt.*",
|
|
16
|
+
"content.config.*"
|
|
17
|
+
],
|
|
18
|
+
"exclude": [
|
|
19
|
+
"node_modules",
|
|
20
|
+
],
|
|
21
|
+
"compilerOptions": {
|
|
22
|
+
"typeRoots": ["./api-types", "./node_modules/@types", "./node_modules"]
|
|
23
|
+
}
|
|
4
24
|
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { defineNuxtRouteMiddleware, navigateTo } from "#app";
|
|
2
|
-
|
|
3
|
-
export default defineNuxtRouteMiddleware((to) => {
|
|
4
|
-
// Only handle category pages
|
|
5
|
-
if (!to.path.startsWith("/c/")) return;
|
|
6
|
-
|
|
7
|
-
// Skip if it already has a trailing slash or it's just "/c/"
|
|
8
|
-
if (to.path.endsWith("/") || to.path === "/c/") return;
|
|
9
|
-
|
|
10
|
-
// Preserve query and hash while adding the trailing slash
|
|
11
|
-
return navigateTo(
|
|
12
|
-
{
|
|
13
|
-
path: `${to.path}/`,
|
|
14
|
-
query: to.query,
|
|
15
|
-
hash: to.hash,
|
|
16
|
-
},
|
|
17
|
-
{ redirectCode: 301 },
|
|
18
|
-
);
|
|
19
|
-
});
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { createAdminAPIClient } from "@shopware/api-client";
|
|
2
|
-
import type { operations } from "@shopware/api-client/admin-api-types";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Creates a Shopware Admin API client
|
|
6
|
-
*/
|
|
7
|
-
export function createAdminApiClient(baseURL: string, accessToken: string) {
|
|
8
|
-
return createAdminAPIClient<operations>({
|
|
9
|
-
baseURL,
|
|
10
|
-
accessToken,
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Fetches sales channels using the Admin API client
|
|
16
|
-
*/
|
|
17
|
-
export async function getSalesChannels(client: any) {
|
|
18
|
-
const response = await client.invoke({
|
|
19
|
-
method: "GET",
|
|
20
|
-
path: "/api/v3/sales-channel",
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
return response.data;
|
|
24
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
createAdminApiClient,
|
|
4
|
-
getSalesChannels,
|
|
5
|
-
} from "../../server/utils/shopware/adminApiClient";
|
|
6
|
-
|
|
7
|
-
describe("Shopware Admin API Client", () => {
|
|
8
|
-
describe("createAdminApiClient", () => {
|
|
9
|
-
it("should create a client with correct configuration", () => {
|
|
10
|
-
const endpoint = "https://shopware.example.com/api";
|
|
11
|
-
const accessToken = "test-token";
|
|
12
|
-
|
|
13
|
-
const client = createAdminApiClient(endpoint, accessToken);
|
|
14
|
-
|
|
15
|
-
expect(client).toBeDefined();
|
|
16
|
-
// The client should have the invoke method
|
|
17
|
-
expect(client).toHaveProperty("invoke");
|
|
18
|
-
expect(typeof client.invoke).toBe("function");
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe("getSalesChannels", () => {
|
|
23
|
-
it("should fetch sales channels successfully", async () => {
|
|
24
|
-
// Mock the client
|
|
25
|
-
const mockClient = {
|
|
26
|
-
invoke: vi.fn().mockResolvedValue({
|
|
27
|
-
data: [
|
|
28
|
-
{ id: "1", name: "Storefront", typeId: "storefront" },
|
|
29
|
-
{ id: "2", name: "Headless", typeId: "headless" },
|
|
30
|
-
],
|
|
31
|
-
}),
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const result = await getSalesChannels(mockClient);
|
|
35
|
-
|
|
36
|
-
expect(result).toEqual([
|
|
37
|
-
{ id: "1", name: "Storefront", typeId: "storefront" },
|
|
38
|
-
{ id: "2", name: "Headless", typeId: "headless" },
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
|
-
expect(mockClient.invoke).toHaveBeenCalledWith({
|
|
42
|
-
method: "GET",
|
|
43
|
-
path: "/api/v3/sales-channel",
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("should handle errors when fetching sales channels", async () => {
|
|
48
|
-
const mockClient = {
|
|
49
|
-
invoke: vi.fn().mockRejectedValue(new Error("API error")),
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
await expect(getSalesChannels(mockClient)).rejects.toThrow("API error");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("should handle empty response gracefully", async () => {
|
|
56
|
-
const mockClient = {
|
|
57
|
-
invoke: vi.fn().mockResolvedValue({
|
|
58
|
-
data: [],
|
|
59
|
-
}),
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const result = await getSalesChannels(mockClient);
|
|
63
|
-
expect(result).toEqual([]);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
});
|