@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.
Files changed (60) hide show
  1. package/.claude/settings.local.json +21 -0
  2. package/.env.example +0 -4
  3. package/.github/workflows/build.yaml +14 -8
  4. package/.github/workflows/ci.yaml +125 -42
  5. package/.nuxtrc +1 -1
  6. package/CLAUDE.md +106 -0
  7. package/app/app.vue +53 -34
  8. package/app/components/AddToWishlist.vue +0 -3
  9. package/app/components/Address/Form.vue +5 -2
  10. package/app/components/Category/Listing.vue +5 -4
  11. package/app/components/Checkout/DeliveryTimeSelect.vue +4 -2
  12. package/app/components/Contact/Form.vue +3 -2
  13. package/app/components/Hero.vue +1 -1
  14. package/app/components/ImageGallery.vue +1 -1
  15. package/app/components/Navigation/DesktopLeft.vue +2 -1
  16. package/app/components/Order/Detail.vue +2 -2
  17. package/app/components/Product/Card.vue +14 -8
  18. package/app/components/SalesChannelSwitch.vue +5 -3
  19. package/app/components/User/LoginForm.vue +1 -4
  20. package/app/components/User/RegistrationForm.vue +5 -2
  21. package/app/composables/useBusinessHours.ts +11 -4
  22. package/app/composables/useCategory.ts +1 -0
  23. package/app/composables/useTopSellers.ts +2 -2
  24. package/app/layouts/account.vue +0 -1
  25. package/app/pages/anmelden.vue +8 -4
  26. package/app/pages/c/[...all].vue +2 -1
  27. package/app/pages/konto/adressen.vue +4 -1
  28. package/app/pages/konto/bestellung/[id].vue +14 -2
  29. package/app/pages/konto/profil.vue +5 -1
  30. package/app/utils/formatDate.ts +2 -1
  31. package/content.config.ts +1 -2
  32. package/eslint.config.mjs +10 -6
  33. package/node.dockerfile +7 -6
  34. package/nuxt.config.ts +17 -3
  35. package/package.json +38 -32
  36. package/renovate.json +9 -1
  37. package/server/api/address/autocomplete.get.ts +3 -2
  38. package/test/e2e/simple-checkout-as-recurring-customer.test.ts +1 -1
  39. package/test/nuxt/AddressFields.test.ts +12 -3
  40. package/test/nuxt/ContactForm.test.ts +31 -11
  41. package/test/nuxt/HeaderRight.test.ts +15 -6
  42. package/test/nuxt/LoginForm.test.ts +41 -23
  43. package/test/nuxt/PaymentAndDelivery.test.ts +2 -2
  44. package/test/nuxt/RegistrationForm.test.ts +6 -3
  45. package/test/nuxt/registrationSchema.test.ts +8 -8
  46. package/test/nuxt/useAddToCart.test.ts +5 -4
  47. package/test/nuxt/useAddressAutocomplete.test.ts +2 -0
  48. package/test/nuxt/useBusinessHours.test.ts +5 -5
  49. package/test/nuxt/useDeliveryTime.test.ts +17 -9
  50. package/test/nuxt/useProductConfigurator.test.ts +6 -4
  51. package/test/nuxt/useProductVariants.test.ts +16 -10
  52. package/test/nuxt/useProductVariantsZwei.test.ts +6 -2
  53. package/test/nuxt/useScrollAnimation.test.ts +16 -13
  54. package/test/nuxt/useTopSellers.test.ts +2 -2
  55. package/test/nuxt/useWishlistActions.test.ts +4 -3
  56. package/test/unit/useCategorySeo.spec.ts +26 -12
  57. package/tsconfig.json +21 -1
  58. package/app/middleware/trailing-slash.global.ts +0 -19
  59. package/server/utils/shopware/adminApiClient.ts +0 -24
  60. 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 "../../app/composables/useTopSellers";
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
- "getTopSellers post /product",
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 any;
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 any[];
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 any[];
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: (arg: any) => any;
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 any).computed = vue.computed;
56
- (globalThis as any).ref = vue.ref;
57
- (globalThis as any).useRuntimeConfig = () => ({
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 any).useHead = useHead;
61
- (globalThis as any).useSeoMeta = useSeoMeta;
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<any>({
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<any>({
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
- });