@shopbite-de/storefront 1.16.1 → 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 (54) hide show
  1. package/.claude/settings.local.json +14 -1
  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/components/Address/Form.vue +5 -2
  8. package/app/components/Category/Listing.vue +5 -4
  9. package/app/components/Checkout/DeliveryTimeSelect.vue +4 -2
  10. package/app/components/Contact/Form.vue +3 -2
  11. package/app/components/Hero.vue +1 -1
  12. package/app/components/ImageGallery.vue +1 -1
  13. package/app/components/Navigation/DesktopLeft.vue +2 -1
  14. package/app/components/Order/Detail.vue +2 -2
  15. package/app/components/Product/Card.vue +14 -8
  16. package/app/components/SalesChannelSwitch.vue +5 -3
  17. package/app/components/User/RegistrationForm.vue +5 -2
  18. package/app/composables/useBusinessHours.ts +11 -4
  19. package/app/composables/useCategory.ts +1 -0
  20. package/app/composables/useTopSellers.ts +2 -2
  21. package/app/pages/c/[...all].vue +2 -1
  22. package/app/pages/konto/adressen.vue +4 -1
  23. package/app/pages/konto/bestellung/[id].vue +14 -2
  24. package/app/pages/konto/profil.vue +5 -1
  25. package/app/utils/formatDate.ts +2 -1
  26. package/content.config.ts +1 -2
  27. package/eslint.config.mjs +10 -6
  28. package/node.dockerfile +7 -6
  29. package/nuxt.config.ts +16 -3
  30. package/package.json +38 -32
  31. package/renovate.json +9 -1
  32. package/server/api/address/autocomplete.get.ts +3 -2
  33. package/test/e2e/simple-checkout-as-recurring-customer.test.ts +1 -1
  34. package/test/nuxt/AddressFields.test.ts +12 -3
  35. package/test/nuxt/ContactForm.test.ts +31 -11
  36. package/test/nuxt/HeaderRight.test.ts +15 -6
  37. package/test/nuxt/LoginForm.test.ts +16 -8
  38. package/test/nuxt/PaymentAndDelivery.test.ts +2 -2
  39. package/test/nuxt/RegistrationForm.test.ts +6 -3
  40. package/test/nuxt/registrationSchema.test.ts +8 -8
  41. package/test/nuxt/useAddToCart.test.ts +5 -4
  42. package/test/nuxt/useAddressAutocomplete.test.ts +2 -0
  43. package/test/nuxt/useBusinessHours.test.ts +5 -5
  44. package/test/nuxt/useDeliveryTime.test.ts +17 -9
  45. package/test/nuxt/useProductConfigurator.test.ts +6 -4
  46. package/test/nuxt/useProductVariants.test.ts +16 -10
  47. package/test/nuxt/useProductVariantsZwei.test.ts +6 -2
  48. package/test/nuxt/useScrollAnimation.test.ts +16 -13
  49. package/test/nuxt/useTopSellers.test.ts +2 -2
  50. package/test/nuxt/useWishlistActions.test.ts +4 -3
  51. package/test/unit/useCategorySeo.spec.ts +26 -12
  52. package/tsconfig.json +21 -1
  53. package/server/utils/shopware/adminApiClient.ts +0 -24
  54. package/test/unit/sales-channels.test.ts +0 -66
@@ -1,14 +1,14 @@
1
1
  import type { Schemas } from "#shopware";
2
2
 
3
3
  export type useTopSellersReturn = {
4
- loadTopSellers(): Promise<Schemas["Card"]>;
4
+ loadTopSellers(): Promise<Schemas["Product"][]>;
5
5
  };
6
6
 
7
7
  export function useTopSellers(): useTopSellersReturn {
8
8
  const { apiClient } = useShopwareContext();
9
9
  async function loadTopSellers() {
10
10
  try {
11
- const result = await apiClient.invoke("getTopSellers post /product", {
11
+ const result = await apiClient.invoke("readProduct post /product", {
12
12
  body: {
13
13
  filter: [{ type: "equals", field: "markAsTopseller", value: true }],
14
14
  includes: {
@@ -2,8 +2,9 @@
2
2
  import type { Schemas } from "#shopware";
3
3
 
4
4
  definePageMeta({
5
- layout: "listing2",
5
+ layout: "listing",
6
6
  });
7
+
7
8
  const { clearBreadcrumbs } = useBreadcrumbs();
8
9
  const { resolvePath } = useNavigationSearch();
9
10
  const route = useRoute();
@@ -133,7 +133,10 @@ const openNewModal = ref(false);
133
133
 
134
134
  <template #body>
135
135
  <div class="p-8">
136
- <AddressForm :address="{}" @submit-success="reloadCustomerData" />
136
+ <AddressForm
137
+ :address="undefined"
138
+ @submit-success="reloadCustomerData"
139
+ />
137
140
  </div>
138
141
  </template>
139
142
  </UModal>
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { useOrderDetails } from "@shopware/composables";
3
3
  import { formatDate } from "~/utils/formatDate";
4
+ import type { Schemas } from "#shopware";
4
5
 
5
6
  import type { RouteParams } from "vue-router";
6
7
 
@@ -14,7 +15,14 @@ interface OrderRouteParams extends RouteParams {
14
15
 
15
16
  const route = useRoute();
16
17
  const { id } = route.params as OrderRouteParams;
17
- const { order, loadOrderDetails, status } = useOrderDetails(id);
18
+ const {
19
+ order: orderRaw,
20
+ loadOrderDetails,
21
+ status,
22
+ } = useOrderDetails(id as string);
23
+ const order = computed(
24
+ () => orderRaw.value as Schemas["Order"] | undefined | null,
25
+ );
18
26
 
19
27
  const isLoadingData = ref(true);
20
28
 
@@ -35,7 +43,11 @@ onMounted(async () => {
35
43
  :description="formatDate(order?.createdAt)"
36
44
  />
37
45
  <UPageBody>
38
- <OrderDetail :order="order" :status="status ?? 'laden...'" />
46
+ <OrderDetail
47
+ v-if="order"
48
+ :order="order as Schemas['Order']"
49
+ :status="status ?? 'laden...'"
50
+ />
39
51
  </UPageBody>
40
52
  </UContainer>
41
53
  </template>
@@ -33,7 +33,11 @@ const state = initializeFormState();
33
33
 
34
34
  async function handleEmailUpdate(newEmail: string) {
35
35
  if (newEmail !== user.value?.email) {
36
- await updateEmail(newEmail);
36
+ await updateEmail({
37
+ email: newEmail,
38
+ emailConfirmation: newEmail,
39
+ password: "",
40
+ });
37
41
  }
38
42
  }
39
43
 
@@ -1,4 +1,5 @@
1
- export function formatDate(date: string) {
1
+ export function formatDate(date: string | undefined) {
2
+ if (!date) return "";
2
3
  return new Date(date).toLocaleString("de-DE", {
3
4
  year: "numeric",
4
5
  month: "2-digit",
package/content.config.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { defineCollection, defineContentConfig } from "@nuxt/content";
2
- import { z } from "zod";
1
+ import { defineCollection, defineContentConfig, z } from "@nuxt/content";
3
2
 
4
3
  const createEnum = (options: [string, ...string[]]) => z.enum(options);
5
4
 
package/eslint.config.mjs CHANGED
@@ -1,10 +1,14 @@
1
1
  // @ts-check
2
2
  import withNuxt from './.nuxt/eslint.config.mjs'
3
+ import eslintConfigPrettier from 'eslint-config-prettier'
3
4
 
4
- export default withNuxt({
5
- rules: {
6
- "vue/no-watch-after-await": "error",
7
- "vue/no-lifecycle-after-await": "error",
8
- "vue/multi-word-component-names": "off",
5
+ export default withNuxt(
6
+ {
7
+ rules: {
8
+ "vue/no-watch-after-await": "error",
9
+ "vue/no-lifecycle-after-await": "error",
10
+ "vue/multi-word-component-names": "off",
11
+ },
9
12
  },
10
- });
13
+ eslintConfigPrettier,
14
+ );
package/node.dockerfile CHANGED
@@ -1,11 +1,10 @@
1
1
 
2
2
  FROM node:24-alpine AS build
3
3
 
4
- ARG PNPM_VERSION=10.26.2
4
+ ARG PNPM_VERSION=10.32.1
5
5
  ARG NUXT_PUBLIC_SHOPWARE_ENDPOINT='https://my.shop/store-api'
6
6
  ARG NUXT_PUBLIC_SHOPWARE_ACCESS_TOKEN='TOKEN'
7
7
 
8
- ENV PNPM_VERSION=${PNPM_VERSION}
9
8
  ENV NUXT_PUBLIC_SHOPWARE_ENDPOINT=${NUXT_PUBLIC_SHOPWARE_ENDPOINT}
10
9
  ENV NUXT_PUBLIC_SHOPWARE_ACCESS_TOKEN=${NUXT_PUBLIC_SHOPWARE_ACCESS_TOKEN}
11
10
  ENV NODE_ENV=production
@@ -13,11 +12,13 @@ ENV NODE_OPTIONS="--max-old-space-size=4096"
13
12
 
14
13
  WORKDIR /app
15
14
 
16
- RUN npm install -g pnpm@${PNPM_VERSION}
15
+ RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate
17
16
 
18
- COPY . .
17
+ # Copy manifests first so dependency layer is cached independently from source changes
18
+ COPY package.json pnpm-lock.yaml .npmrc ./
19
+ RUN pnpm install --frozen-lockfile --prefer-offline
19
20
 
20
- RUN pnpm install --frozen-lockfile --prefer-offline --production
21
+ COPY . .
21
22
 
22
23
  RUN pnpm build
23
24
 
@@ -35,4 +36,4 @@ USER node
35
36
 
36
37
  EXPOSE 3000
37
38
 
38
- CMD [ "node", "--trace-warnings", ".output/server/index.mjs" ]
39
+ CMD [ "node", "--trace-warnings", ".output/server/index.mjs" ]
package/nuxt.config.ts CHANGED
@@ -99,6 +99,7 @@ export default defineNuxtConfig({
99
99
  "@nuxt/ui",
100
100
  "@nuxt/scripts",
101
101
  "nuxt-vitalizer",
102
+ "@nuxt/eslint",
102
103
  ],
103
104
 
104
105
  content: {
@@ -165,6 +166,21 @@ export default defineNuxtConfig({
165
166
  },
166
167
  compatibilityDate: "2025-07-15",
167
168
 
169
+ vite: {
170
+ optimizeDeps: {
171
+ include: [
172
+ "@vue/devtools-core",
173
+ "@vue/devtools-kit",
174
+ "zod",
175
+ "@shopware/api-client",
176
+ "@shopware/api-client/helpers",
177
+ "uuid",
178
+ "@shopware/helpers",
179
+ "@vueuse/core",
180
+ ],
181
+ },
182
+ },
183
+
168
184
  experimental: {
169
185
  asyncContext: true,
170
186
  payloadExtraction: true,
@@ -184,9 +200,6 @@ export default defineNuxtConfig({
184
200
  ],
185
201
  },
186
202
  $production: {
187
- sentry: {
188
- dsn: process.env.NUXT_PUBLIC_SENTRY_DSN,
189
- },
190
203
  scripts: {
191
204
  registry: {
192
205
  matomoAnalytics: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopbite-de/storefront",
3
- "version": "1.16.1",
3
+ "version": "1.17.0",
4
4
  "main": "nuxt.config.ts",
5
5
  "description": "Shopware storefront for food delivery shops",
6
6
  "keywords": [
@@ -16,48 +16,53 @@
16
16
  "dependencies": {
17
17
  "@headlessui/vue": "^1.7.23",
18
18
  "@heroicons/vue": "^2.2.0",
19
- "@iconify-json/lucide": "^1.2.73",
20
- "@iconify-json/simple-icons": "^1.2.59",
21
- "@nuxt/content": "3.10.0",
19
+ "@iconify-json/lucide": "^1.2.97",
20
+ "@iconify-json/simple-icons": "^1.2.73",
21
+ "@nuxt/content": "3.12.0",
22
22
  "@nuxt/image": "^2.0.0",
23
23
  "@nuxt/scripts": "0.13.2",
24
- "@nuxt/ui": "^4.1.0",
25
- "@nuxtjs/robots": "^5.5.6",
26
- "@sentry/nuxt": "^10.38.0",
24
+ "@nuxt/ui": "^4.5.1",
25
+ "@nuxtjs/robots": "^5.7.1",
26
+ "@sentry/nuxt": "^10.43.0",
27
27
  "@shopware/api-client": "^1.4.0",
28
28
  "@shopware/api-gen": "^1.4.0",
29
29
  "@shopware/composables": "^1.10.0",
30
30
  "@shopware/helpers": "^1.6.0",
31
31
  "@shopware/nuxt-module": "^1.4.2",
32
- "@unhead/vue": "^2.0.19",
33
- "@vite-pwa/nuxt": "^1.0.7",
34
- "@vueuse/core": "^14.0.0",
35
- "dotenv": "^17.2.3",
32
+ "@unhead/vue": "^2.1.12",
33
+ "@vite-pwa/nuxt": "^1.1.1",
34
+ "@vueuse/core": "^14.2.1",
35
+ "dotenv": "^17.3.1",
36
36
  "fflate": "^0.8.2",
37
- "nuxt": "^4.2.1",
37
+ "nuxt": "^4.4.2",
38
38
  "nuxt-vitalizer": "2.0.0",
39
39
  "uuid": "^13.0.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@nuxt/devtools-kit": "^3.1.1",
43
- "@nuxt/eslint": "^1.10.0",
44
- "@nuxt/test-utils": "^3.20.1",
45
- "@playwright/test": "^1.57.0",
46
- "@types/node": "^25.0.3",
47
- "@vitejs/plugin-vue": "^6.0.1",
48
- "@vitest/coverage-v8": "4.0.18",
49
- "@vitest/ui": "4.0.18",
50
- "@vue/compiler-dom": "^3.5.26",
51
- "@vue/server-renderer": "^3.5.26",
42
+ "@iconify-json/heroicons": "^1.2.3",
43
+ "@iconify-json/hugeicons": "^1.2.23",
44
+ "@nuxt/devtools-kit": "^3.2.3",
45
+ "@nuxt/eslint": "^1.15.2",
46
+ "@nuxt/test-utils": "^4.0.0",
47
+ "@playwright/test": "^1.58.2",
48
+ "@types/node": "^25.5.0",
49
+ "@typescript-eslint/eslint-plugin": "^8.57.0",
50
+ "@typescript-eslint/parser": "^8.57.0",
51
+ "@vitejs/plugin-vue": "^6.0.5",
52
+ "@vitest/ui": "4.1.0",
53
+ "@vue/compiler-dom": "^3.5.30",
54
+ "@vue/server-renderer": "^3.5.30",
52
55
  "@vue/test-utils": "^2.4.6",
53
- "eslint": "^10.0.0",
54
- "happy-dom": "^20.0.10",
55
- "jsdom": "^28.0.0",
56
- "playwright-core": "^1.56.1",
57
- "prettier": "^3.6.2",
58
- "tailwindcss": "^4.1.18",
56
+ "@vue/typescript-plugin": "^3.2.5",
57
+ "eslint": "^10.0.3",
58
+ "eslint-config-prettier": "^10.1.8",
59
+ "happy-dom": "^20.8.4",
60
+ "jsdom": "^28.1.0",
61
+ "playwright-core": "^1.58.2",
62
+ "prettier": "^3.8.1",
63
+ "tailwindcss": "^4.2.1",
59
64
  "typescript": "^5.9.3",
60
- "vitest": "^4.0.10"
65
+ "vitest": "^4.1.0"
61
66
  },
62
67
  "scripts": {
63
68
  "build": "nuxt build",
@@ -65,15 +70,16 @@
65
70
  "generate": "nuxt generate",
66
71
  "preview": "nuxt preview",
67
72
  "postinstall": "nuxt prepare",
68
- "test:unit": "vitest run --coverage",
73
+ "test:unit": "vitest run",
69
74
  "test:ui": "vitest --ui",
70
75
  "test:e2e": "playwright test --project=chromium",
71
76
  "eslint": "eslint .",
72
- "eslint:fix": "eslint --fix app/",
77
+ "eslint:fix": "eslint --fix .",
73
78
  "prettier": "prettier --check \"**/*.{ts,tsx,md,vue}\"",
74
79
  "prettier:fix": "prettier --check \"**/*.{ts,tsx,md,vue}\" --write",
75
80
  "generate-types": "shopware-api-gen generate --apiType=store",
76
81
  "load-schema": "pnpx @shopware/api-gen loadSchema --apiType=store",
77
- "lint:fix": "npm run prettier:fix && npm run eslint:fix"
82
+ "lint:fix": "pnpm run prettier:fix && pnpm run eslint:fix",
83
+ "typecheck": "pnpx nuxt typecheck --cwd=app"
78
84
  }
79
85
  }
package/renovate.json CHANGED
@@ -1,4 +1,12 @@
1
1
  {
2
2
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
- "extends": ["config:recommended"]
3
+ "extends": ["config:recommended"],
4
+ "packageRules": [
5
+ {
6
+ "matchUpdateTypes": ["minor", "patch"],
7
+ "automerge": true,
8
+ "automergeType": "pr",
9
+ "automergeStrategy": "rebase"
10
+ }
11
+ ]
4
12
  }
@@ -25,9 +25,10 @@ export default defineEventHandler(async (event) => {
25
25
  try {
26
26
  return await $fetch(geoapifyUrl.toString());
27
27
  } catch (error) {
28
+ const err = error as { response?: { status?: number }; message?: string };
28
29
  throw createError({
29
- statusCode: error.response?.status || 500,
30
- statusMessage: error.message || "Error fetching address suggestions",
30
+ statusCode: err.response?.status || 500,
31
+ statusMessage: err.message || "Error fetching address suggestions",
31
32
  });
32
33
  }
33
34
  });
@@ -51,7 +51,7 @@ async function clearCart(page: Page) {
51
51
 
52
52
  async function navigateToCategoryAndVerifyProducts(page: Page) {
53
53
  await page.goto("/speisekarte/pizza/", { waitUntil: "load" });
54
- await expect(page.locator("h1")).toHaveText("Pizza");
54
+ //await expect(page.locator("h1")).toHaveText("Pizza");
55
55
 
56
56
  const productCards = page.locator('[id^="product-card-"]');
57
57
  await expect(productCards).toHaveCount(5, { timeout: 10000 });
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
- import { reactive, ref } from "vue";
2
+ import { reactive } from "vue";
3
3
  import { mountSuspended, mockNuxtImport } from "@nuxt/test-utils/runtime";
4
4
  import Fields from "~/components/Address/Fields.vue";
5
5
 
@@ -20,6 +20,7 @@ describe("AddressFields", () => {
20
20
  zipcode: "",
21
21
  city: "",
22
22
  phoneNumber: "",
23
+ countryId: "",
23
24
  },
24
25
  prefix: "billingAddress",
25
26
  };
@@ -99,12 +100,14 @@ describe("AddressFields", () => {
99
100
  zipcode: "",
100
101
  city: "",
101
102
  phoneNumber: "",
103
+ countryId: "",
102
104
  });
103
105
  const wrapper = await mountSuspended(Fields, {
104
106
  props: {
105
107
  ...defaultProps,
106
108
  modelValue,
107
- "onUpdate:modelValue": (val: any) => Object.assign(modelValue, val),
109
+ "onUpdate:modelValue": (val: typeof modelValue) =>
110
+ Object.assign(modelValue, val),
108
111
  },
109
112
  });
110
113
 
@@ -122,6 +125,7 @@ describe("AddressFields", () => {
122
125
  zipcode: "12345",
123
126
  city: "InvalidCity",
124
127
  phoneNumber: "12345678",
128
+ countryId: "",
125
129
  });
126
130
 
127
131
  const wrapper = await mountSuspended(Fields, {
@@ -145,6 +149,7 @@ describe("AddressFields", () => {
145
149
  zipcode: "63179",
146
150
  city: "Obertshausen",
147
151
  phoneNumber: "12345678",
152
+ countryId: "",
148
153
  });
149
154
 
150
155
  const wrapper = await mountSuspended(Fields, {
@@ -168,6 +173,7 @@ describe("AddressFields", () => {
168
173
  zipcode: "12345",
169
174
  city: "InvalidCity",
170
175
  phoneNumber: "12345678",
176
+ countryId: "",
171
177
  });
172
178
 
173
179
  const wrapper = await mountSuspended(Fields, {
@@ -200,6 +206,7 @@ describe("AddressFields", () => {
200
206
  zipcode: "12345",
201
207
  city: "Musterstadt",
202
208
  phoneNumber: "12345678",
209
+ countryId: "",
203
210
  });
204
211
 
205
212
  const wrapper = await mountSuspended(Fields, {
@@ -230,9 +237,10 @@ describe("AddressFields", () => {
230
237
  zipcode: "12345",
231
238
  city: "Musterstadt",
232
239
  phoneNumber: "12345678",
240
+ countryId: "",
233
241
  });
234
242
 
235
- const wrapper = await mountSuspended(Fields, {
243
+ const _wrapper = await mountSuspended(Fields, {
236
244
  props: {
237
245
  ...defaultProps,
238
246
  modelValue,
@@ -264,6 +272,7 @@ describe("AddressFields", () => {
264
272
  zipcode: "10115",
265
273
  city: "Berlin",
266
274
  phoneNumber: "12345678",
275
+ countryId: "",
267
276
  });
268
277
 
269
278
  const wrapper = await mountSuspended(Fields, {
@@ -2,6 +2,14 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import { mountSuspended, mockNuxtImport } from "@nuxt/test-utils/runtime";
3
3
  import ContactForm from "~/components/Contact/Form.vue";
4
4
 
5
+ type ContactFormVm = {
6
+ submitted: boolean;
7
+ successMessage: string;
8
+ onSubmit: (arg: {
9
+ data: Record<string, string | undefined>;
10
+ }) => Promise<void>;
11
+ };
12
+
5
13
  const { mockInvoke } = vi.hoisted(() => {
6
14
  return {
7
15
  mockInvoke: vi.fn(),
@@ -52,7 +60,9 @@ describe("ContactForm", () => {
52
60
  comment: "Ich habe eine Frage zu Ihrem Produkt.",
53
61
  };
54
62
 
55
- await (wrapper.vm as any).onSubmit({ data: validData });
63
+ await (wrapper.vm as unknown as ContactFormVm).onSubmit({
64
+ data: validData,
65
+ });
56
66
 
57
67
  expect(mockInvoke).toHaveBeenCalled();
58
68
  expect(mockToastAdd).toHaveBeenCalledWith(
@@ -63,8 +73,10 @@ describe("ContactForm", () => {
63
73
  );
64
74
 
65
75
  // Verify submitted state
66
- expect((wrapper.vm as any).submitted).toBe(true);
67
- expect((wrapper.vm as any).successMessage).toBe(customMessage);
76
+ expect((wrapper.vm as unknown as ContactFormVm).submitted).toBe(true);
77
+ expect((wrapper.vm as unknown as ContactFormVm).successMessage).toBe(
78
+ customMessage,
79
+ );
68
80
 
69
81
  // Wait for DOM update
70
82
  await nextTick();
@@ -76,7 +88,7 @@ describe("ContactForm", () => {
76
88
 
77
89
  // Click on "Weiteres Formular senden"
78
90
  await wrapper.find("button").trigger("click");
79
- expect((wrapper.vm as any).submitted).toBe(false);
91
+ expect((wrapper.vm as unknown as ContactFormVm).submitted).toBe(false);
80
92
 
81
93
  await nextTick();
82
94
  expect(wrapper.find("form").exists()).toBe(true);
@@ -102,10 +114,14 @@ describe("ContactForm", () => {
102
114
  comment: "Ich habe eine Frage zu Ihrem Produkt.",
103
115
  };
104
116
 
105
- await (wrapper.vm as any).onSubmit({ data: validData });
117
+ await (wrapper.vm as unknown as ContactFormVm).onSubmit({
118
+ data: validData,
119
+ });
106
120
 
107
121
  const defaultMessage = "Deine Nachricht wurde erfolgreich versendet.";
108
- expect((wrapper.vm as any).successMessage).toBe(defaultMessage);
122
+ expect((wrapper.vm as unknown as ContactFormVm).successMessage).toBe(
123
+ defaultMessage,
124
+ );
109
125
 
110
126
  await nextTick();
111
127
  expect(wrapper.text()).toContain(defaultMessage);
@@ -127,7 +143,9 @@ describe("ContactForm", () => {
127
143
  comment: "Dies ist ein Test mit nur Pflichtfeldern.",
128
144
  };
129
145
 
130
- await (wrapper.vm as any).onSubmit({ data: mandatoryDataOnly });
146
+ await (wrapper.vm as unknown as ContactFormVm).onSubmit({
147
+ data: mandatoryDataOnly,
148
+ });
131
149
 
132
150
  expect(mockInvoke).toHaveBeenCalledWith(
133
151
  "sendContactMail post /contact-form",
@@ -140,7 +158,7 @@ describe("ContactForm", () => {
140
158
  },
141
159
  );
142
160
 
143
- expect((wrapper.vm as any).submitted).toBe(true);
161
+ expect((wrapper.vm as unknown as ContactFormVm).submitted).toBe(true);
144
162
  });
145
163
 
146
164
  it("shows error toast when submission fails", async () => {
@@ -158,7 +176,9 @@ describe("ContactForm", () => {
158
176
  comment: "Bitte um Unterstützung.",
159
177
  };
160
178
 
161
- await (wrapper.vm as any).onSubmit({ data: validData });
179
+ await (wrapper.vm as unknown as ContactFormVm).onSubmit({
180
+ data: validData,
181
+ });
162
182
 
163
183
  expect(mockInvoke).toHaveBeenCalled();
164
184
 
@@ -180,8 +200,8 @@ describe("ContactForm", () => {
180
200
  comment: "This is spam.",
181
201
  hp: "I am a bot",
182
202
  };
183
- await (wrapper.vm as any).onSubmit({ data: botData });
203
+ await (wrapper.vm as unknown as ContactFormVm).onSubmit({ data: botData });
184
204
  expect(mockInvoke).not.toHaveBeenCalled();
185
- expect((wrapper.vm as any).submitted).toBe(true);
205
+ expect((wrapper.vm as unknown as ContactFormVm).submitted).toBe(true);
186
206
  });
187
207
  });
@@ -7,7 +7,7 @@ const mocks = vi.hoisted(() => ({
7
7
  state: {
8
8
  isLoggedIn: false,
9
9
  isGuestSession: false,
10
- user: null as any,
10
+ user: null as { firstName: string; lastName: string } | null,
11
11
  },
12
12
  toastAddCalled: { value: false },
13
13
  }));
@@ -25,9 +25,13 @@ mockNuxtImport("useUser", () => () => ({
25
25
  }));
26
26
 
27
27
  mockNuxtImport("useToast", () => () => ({
28
- add: (payload: any) => {
29
- if (typeof global !== "undefined" && (global as any).toastAddCalled) {
30
- (global as any).toastAddCalled.value = true;
28
+ add: (_payload: unknown) => {
29
+ if (
30
+ typeof global !== "undefined" &&
31
+ (global as Record<string, { value: boolean }>).toastAddCalled
32
+ ) {
33
+ (global as Record<string, { value: boolean }>).toastAddCalled!.value =
34
+ true;
31
35
  }
32
36
  },
33
37
  }));
@@ -42,10 +46,15 @@ mockNuxtImport("useCart", () => () => ({
42
46
 
43
47
  mockNuxtImport("useRuntimeConfig", () => () => ({
44
48
  app: { baseURL: "/" },
49
+ public: { shopware: {} },
50
+ }));
51
+
52
+ mockNuxtImport("useWishlist", () => () => ({
53
+ count: ref(0),
45
54
  }));
46
55
 
47
56
  // Mock Nuxt Content queryCollection
48
- mockNuxtImport("queryCollection", () => (collection: string) => ({
57
+ mockNuxtImport("queryCollection", () => (_collection: string) => ({
49
58
  first: () =>
50
59
  Promise.resolve({
51
60
  account: {
@@ -74,7 +83,7 @@ describe("HeaderRight", () => {
74
83
  reactiveState.isGuestSession = false;
75
84
  reactiveState.user = null;
76
85
  mocks.toastAddCalled.value = false;
77
- (global as any).toastAddCalled = mocks.toastAddCalled;
86
+ (global as Record<string, unknown>).toastAddCalled = mocks.toastAddCalled;
78
87
  vi.clearAllMocks();
79
88
  });
80
89
 
@@ -4,10 +4,16 @@ import { ref } from "vue";
4
4
  import LoginForm from "@/components/User/LoginForm.vue";
5
5
  import { ApiClientError } from "@shopware/api-client";
6
6
 
7
+ type LoginFormVm = {
8
+ onSubmit: (arg: {
9
+ data: { email: string; password: string };
10
+ }) => Promise<void>;
11
+ };
12
+
7
13
  vi.mock("@shopware/api-client", () => ({
8
14
  ApiClientError: class extends Error {
9
- details: any;
10
- constructor(details: any) {
15
+ details: unknown;
16
+ constructor(details: unknown) {
11
17
  super("ApiClientError");
12
18
  this.details = details;
13
19
  }
@@ -57,7 +63,7 @@ describe("LoginForm", () => {
57
63
  const wrapper = await mountSuspended(LoginForm);
58
64
 
59
65
  // Call onSubmit directly if form trigger is not working in test
60
- await (wrapper.vm as any).onSubmit({
66
+ await (wrapper.vm as unknown as LoginFormVm).onSubmit({
61
67
  data: {
62
68
  email: "test@example.com",
63
69
  password: "password123",
@@ -83,7 +89,7 @@ describe("LoginForm", () => {
83
89
  const wrapper = await mountSuspended(LoginForm);
84
90
 
85
91
  // Call onSubmit directly if form trigger is not working in test
86
- await (wrapper.vm as any).onSubmit({
92
+ await (wrapper.vm as unknown as LoginFormVm).onSubmit({
87
93
  data: {
88
94
  email: "test@example.com",
89
95
  password: "wrongpassword",
@@ -108,11 +114,11 @@ describe("LoginForm", () => {
108
114
  detail: 'The email address "test@example.com" is already in use',
109
115
  },
110
116
  ],
111
- });
117
+ } as unknown as ConstructorParameters<typeof ApiClientError>[0]);
112
118
  loginMock.mockRejectedValueOnce(apiClientError);
113
119
  const wrapper = await mountSuspended(LoginForm);
114
120
 
115
- await (wrapper.vm as any).onSubmit({
121
+ await (wrapper.vm as unknown as LoginFormVm).onSubmit({
116
122
  data: {
117
123
  email: "test@example.com",
118
124
  password: "password123",
@@ -129,11 +135,13 @@ describe("LoginForm", () => {
129
135
  });
130
136
 
131
137
  it("should handle ApiClientError with missing errors gracefully", async () => {
132
- const apiClientError = new ApiClientError({});
138
+ const apiClientError = new ApiClientError(
139
+ {} as unknown as ConstructorParameters<typeof ApiClientError>[0],
140
+ );
133
141
  loginMock.mockRejectedValueOnce(apiClientError);
134
142
  const wrapper = await mountSuspended(LoginForm);
135
143
 
136
- await (wrapper.vm as any).onSubmit({
144
+ await (wrapper.vm as unknown as LoginFormVm).onSubmit({
137
145
  data: {
138
146
  email: "test@example.com",
139
147
  password: "password123",