create-nuxt-base 1.2.0 → 2.1.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 (46) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/nuxt-base-template/.github/workflows/test.yml +90 -0
  3. package/nuxt-base-template/app/components/Modal/ModalBackupCodes.vue +2 -1
  4. package/nuxt-base-template/app/components/Upload/TusFileUpload.vue +7 -7
  5. package/nuxt-base-template/app/interfaces/user.interface.ts +5 -12
  6. package/nuxt-base-template/app/layouts/default.vue +1 -1
  7. package/nuxt-base-template/app/middleware/admin.global.ts +2 -2
  8. package/nuxt-base-template/app/middleware/auth.global.ts +2 -2
  9. package/nuxt-base-template/app/middleware/guest.global.ts +2 -2
  10. package/nuxt-base-template/app/pages/app/index.vue +1 -1
  11. package/nuxt-base-template/app/pages/app/settings/security.vue +54 -43
  12. package/nuxt-base-template/app/pages/auth/2fa.vue +2 -3
  13. package/nuxt-base-template/app/pages/auth/forgot-password.vue +2 -1
  14. package/nuxt-base-template/app/pages/auth/login.vue +6 -4
  15. package/nuxt-base-template/app/pages/auth/register.vue +85 -61
  16. package/nuxt-base-template/app/pages/auth/reset-password.vue +2 -1
  17. package/nuxt-base-template/docs/pages/docs.vue +1 -1
  18. package/nuxt-base-template/nuxt.config.ts +50 -1
  19. package/nuxt-base-template/package-lock.json +1311 -2920
  20. package/nuxt-base-template/package.json +27 -2
  21. package/nuxt-base-template/playwright.config.ts +1 -1
  22. package/nuxt-base-template/tests/e2e/auth.spec.ts +467 -0
  23. package/nuxt-base-template/tests/unit/auth/auth.spec.ts +439 -0
  24. package/nuxt-base-template/tests/unit/auth/error-translation.spec.ts +279 -0
  25. package/nuxt-base-template/tests/unit/mocks/auth-client.mock.ts +165 -0
  26. package/nuxt-base-template/tests/unit/mocks/nuxt-imports.ts +105 -0
  27. package/nuxt-base-template/tests/unit/setup.ts +56 -0
  28. package/nuxt-base-template/vitest.config.ts +25 -0
  29. package/package.json +1 -1
  30. package/nuxt-base-template/app/components/Transition/TransitionFade.vue +0 -27
  31. package/nuxt-base-template/app/components/Transition/TransitionFadeScale.vue +0 -27
  32. package/nuxt-base-template/app/components/Transition/TransitionSlide.vue +0 -12
  33. package/nuxt-base-template/app/components/Transition/TransitionSlideBottom.vue +0 -12
  34. package/nuxt-base-template/app/components/Transition/TransitionSlideRevert.vue +0 -12
  35. package/nuxt-base-template/app/composables/use-better-auth.ts +0 -597
  36. package/nuxt-base-template/app/composables/use-file.ts +0 -71
  37. package/nuxt-base-template/app/composables/use-share.ts +0 -38
  38. package/nuxt-base-template/app/composables/use-tus-upload.ts +0 -278
  39. package/nuxt-base-template/app/composables/use-tw.ts +0 -1
  40. package/nuxt-base-template/app/interfaces/upload.interface.ts +0 -58
  41. package/nuxt-base-template/app/lib/auth-client.ts +0 -229
  42. package/nuxt-base-template/app/lib/auth-state.ts +0 -206
  43. package/nuxt-base-template/app/plugins/auth-interceptor.client.ts +0 -151
  44. package/nuxt-base-template/app/utils/crypto.ts +0 -44
  45. package/nuxt-base-template/tests/iam.spec.ts +0 -247
  46. /package/nuxt-base-template/tests/{init.spec.ts → e2e/init.spec.ts} +0 -0
@@ -28,8 +28,13 @@
28
28
  "generate-types": "openapi-ts",
29
29
  "preview": "nuxt preview",
30
30
  "postinstall": "nuxt prepare",
31
- "test": "npm run test:e2e",
31
+ "prepare": "simple-git-hooks",
32
+ "test": "npm run test:unit",
33
+ "test:unit": "vitest run",
34
+ "test:unit:watch": "vitest",
35
+ "test:unit:coverage": "vitest run --coverage",
32
36
  "test:e2e": "playwright test",
37
+ "test:all": "npm run test:unit && npm run test:e2e",
33
38
  "lint": "oxlint app/",
34
39
  "lint:fix": "oxlint --fix app/",
35
40
  "format": "oxfmt",
@@ -41,6 +46,7 @@
41
46
  "@better-auth/passkey": "1.4.10",
42
47
  "@hey-api/client-fetch": "0.13.1",
43
48
  "@lenne.tech/bug.lt": "latest",
49
+ "@lenne.tech/nuxt-extensions": "1.1.0",
44
50
  "@nuxt/image": "2.0.0",
45
51
  "@nuxt/ui": "4.3.0",
46
52
  "@pinia/nuxt": "0.11.3",
@@ -60,17 +66,36 @@
60
66
  "@tailwindcss/typography": "0.5.19",
61
67
  "@tailwindcss/vite": "4.1.18",
62
68
  "@types/node": "25.0.6",
69
+ "@vitejs/plugin-vue": "^6.0.3",
70
+ "@vue/test-utils": "^2.4.6",
63
71
  "dayjs-nuxt": "2.1.11",
72
+ "happy-dom": "^20.3.7",
64
73
  "jsdom": "27.4.0",
74
+ "lint-staged": "^16.2.7",
65
75
  "nuxt": "4.2.2",
66
76
  "oxfmt": "latest",
67
77
  "oxlint": "latest",
68
78
  "rimraf": "6.1.2",
79
+ "simple-git-hooks": "^2.13.1",
69
80
  "tailwindcss": "4.1.18",
70
- "typescript": "5.9.3"
81
+ "typescript": "5.9.3",
82
+ "vitest": "^3.2.4"
71
83
  },
72
84
  "engines": {
73
85
  "node": ">=22",
74
86
  "npm": ">=10"
87
+ },
88
+ "simple-git-hooks": {
89
+ "pre-commit": "npx lint-staged",
90
+ "pre-push": "npm run test:unit"
91
+ },
92
+ "lint-staged": {
93
+ "app/**/*.{ts,vue}": [
94
+ "oxlint --fix",
95
+ "oxfmt"
96
+ ],
97
+ "tests/**/*.ts": [
98
+ "oxlint --fix"
99
+ ]
75
100
  }
76
101
  }
@@ -28,7 +28,7 @@ export default defineConfig<ConfigOptions>({
28
28
  reporter: 'html',
29
29
  /* Retry on CI only */
30
30
  retries: isCI ? 2 : 0,
31
- testDir: './tests',
31
+ testDir: './tests/e2e',
32
32
  timeout: isWindows ? 60000 : undefined,
33
33
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
34
34
  use: {
@@ -0,0 +1,467 @@
1
+ import { expect, test } from '@nuxt/test-utils/playwright';
2
+ import type { Page } from '@playwright/test';
3
+ import {
4
+ waitForHydration,
5
+ gotoAndWaitForHydration,
6
+ waitForURLAndHydration,
7
+ fillInput,
8
+ generateTestUser,
9
+ generateTOTP,
10
+ extractTOTPSecret,
11
+ } from '@lenne.tech/nuxt-extensions/testing';
12
+
13
+ /**
14
+ * Authentication E2E Tests
15
+ *
16
+ * These tests require running servers:
17
+ * - API: npm start (in nest-server-starter)
18
+ * - Frontend: npm run dev (in nuxt-base-template)
19
+ *
20
+ * Run tests with: npm run test:e2e
21
+ *
22
+ * NOTE: These tests are for manual E2E testing only.
23
+ * For CI/CD pipelines, use vitest unit tests instead.
24
+ */
25
+
26
+ // =============================================================================
27
+ // Project-Specific Test Utilities
28
+ // =============================================================================
29
+
30
+ /**
31
+ * Register a new user via UI
32
+ */
33
+ async function registerUser(
34
+ page: Page,
35
+ user: { email: string; password: string; name: string },
36
+ ): Promise<void> {
37
+ await gotoAndWaitForHydration(page, '/auth/register');
38
+ await page.locator('input[name="name"]').waitFor({ state: 'visible', timeout: 10000 });
39
+
40
+ // Fill form fields
41
+ await fillInput(page, 'input[name="name"]', user.name);
42
+ await fillInput(page, 'input[name="email"]', user.email);
43
+ await fillInput(page, 'input[name="password"]', user.password);
44
+ await fillInput(page, 'input[name="confirmPassword"]', user.password);
45
+
46
+ // Click submit button
47
+ await page.getByRole('button', { name: 'Konto erstellen' }).click();
48
+
49
+ // Wait for passkey prompt to appear and dismiss it
50
+ const laterButton = page.getByRole('button', { name: 'Später einrichten' });
51
+ await laterButton.waitFor({ state: 'visible', timeout: 10000 });
52
+ await laterButton.click();
53
+
54
+ await waitForURLAndHydration(page, /\/app/, { timeout: 15000 });
55
+ }
56
+
57
+ /**
58
+ * Login with email and password via UI
59
+ */
60
+ async function loginWithEmail(
61
+ page: Page,
62
+ email: string,
63
+ password: string,
64
+ ): Promise<void> {
65
+ await gotoAndWaitForHydration(page, '/auth/login');
66
+ await page.locator('input[name="email"]').waitFor({ state: 'visible', timeout: 10000 });
67
+
68
+ await fillInput(page, 'input[name="email"]', email);
69
+ await fillInput(page, 'input[name="password"]', password);
70
+
71
+ await page.getByRole('button', { name: 'Anmelden', exact: true }).click();
72
+ }
73
+
74
+ /**
75
+ * Logout via UI
76
+ */
77
+ async function logout(page: Page): Promise<void> {
78
+ // Logout button has aria-label="Logout"
79
+ const logoutButton = page.getByLabel('Logout');
80
+ await logoutButton.waitFor({ state: 'visible', timeout: 5000 });
81
+ await logoutButton.click();
82
+ await page.waitForURL(/\/auth\/login/, { timeout: 5000 });
83
+ await waitForHydration(page);
84
+ }
85
+
86
+ /**
87
+ * Enable 2FA and return the TOTP secret
88
+ *
89
+ * NOTE: This function currently has a CORS issue in E2E tests.
90
+ * The authClient sends requests directly to localhost:3000/iam instead of
91
+ * using the Nuxt proxy at /api/iam. This needs to be fixed in nuxt-extensions.
92
+ */
93
+ async function enable2FA(page: Page, password: string): Promise<string> {
94
+ await gotoAndWaitForHydration(page, '/app/settings/security');
95
+
96
+ // Wait for the 2FA section to be visible
97
+ const enable2FAButton = page.getByRole('button', { name: '2FA aktivieren' });
98
+ await enable2FAButton.waitFor({ state: 'visible', timeout: 10000 });
99
+
100
+ // Fill password field
101
+ const passwordInput = page.locator('input[type="password"]');
102
+ await passwordInput.click();
103
+ await page.keyboard.type(password, { delay: 5 });
104
+
105
+ // Click the button
106
+ await enable2FAButton.click();
107
+
108
+ // Wait for QR code (may take a moment for API call)
109
+ const qrImage = page.locator('img[alt="TOTP QR Code"]');
110
+ await expect(qrImage).toBeVisible({ timeout: 15000 });
111
+
112
+ const qrUrl = await qrImage.getAttribute('src');
113
+ const secret = qrUrl ? extractTOTPSecret(decodeURIComponent(qrUrl)) : null;
114
+ expect(secret).not.toBeNull();
115
+
116
+ // Verify TOTP
117
+ const totpCode = generateTOTP(secret!);
118
+ await fillInput(page, 'input[placeholder="000000"]', totpCode);
119
+ await page.getByRole('button', { name: 'Verifizieren' }).click();
120
+
121
+ // Close backup codes dialog (use specific heading selector)
122
+ await expect(page.getByRole('heading', { name: 'Backup-Codes' })).toBeVisible({ timeout: 10000 });
123
+ await page.keyboard.press('Escape');
124
+
125
+ return secret!;
126
+ }
127
+
128
+ /**
129
+ * Cleanup Virtual Authenticator
130
+ */
131
+ async function cleanupAuthenticator(
132
+ cdpSession: any,
133
+ authenticatorId: string,
134
+ ): Promise<void> {
135
+ await cdpSession.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId });
136
+ await cdpSession.send('WebAuthn.disable');
137
+ }
138
+
139
+ // =============================================================================
140
+ // API Availability Check
141
+ // =============================================================================
142
+
143
+ let apiAvailable = false;
144
+
145
+ test.beforeAll(async ({ request }) => {
146
+ // Check API and Frontend availability
147
+ let frontendAvailable = false;
148
+
149
+ try {
150
+ const apiResponse = await request.get('http://localhost:3000/');
151
+ apiAvailable = apiResponse.ok();
152
+ } catch {
153
+ apiAvailable = false;
154
+ }
155
+
156
+ try {
157
+ const frontendResponse = await request.get('http://localhost:3001/');
158
+ frontendAvailable = frontendResponse.ok();
159
+ } catch {
160
+ frontendAvailable = false;
161
+ }
162
+
163
+ if (!apiAvailable || !frontendAvailable) {
164
+ console.error('');
165
+ console.error('╔══════════════════════════════════════════════════════════════╗');
166
+ console.error('║ E2E TESTS REQUIRE RUNNING SERVERS ║');
167
+ console.error('╠══════════════════════════════════════════════════════════════╣');
168
+ if (!apiAvailable) {
169
+ console.error('║ ✗ API Server (localhost:3000) - NOT RUNNING ║');
170
+ } else {
171
+ console.error('║ ✓ API Server (localhost:3000) - Running ║');
172
+ }
173
+ if (!frontendAvailable) {
174
+ console.error('║ ✗ Frontend (localhost:3001) - NOT RUNNING ║');
175
+ } else {
176
+ console.error('║ ✓ Frontend (localhost:3001) - Running ║');
177
+ }
178
+ console.error('╠══════════════════════════════════════════════════════════════╣');
179
+ console.error('║ NOTE: These tests are for manual E2E testing only. ║');
180
+ console.error('║ For CI/CD pipelines, use vitest unit tests. ║');
181
+ console.error('╚══════════════════════════════════════════════════════════════╝');
182
+ console.error('');
183
+ }
184
+
185
+ apiAvailable = apiAvailable && frontendAvailable;
186
+ });
187
+
188
+ // =============================================================================
189
+ // Test 1: All Three Auth Methods with Intermediate Logout/Login
190
+ // =============================================================================
191
+
192
+ test.describe.serial('Test 1: All Auth Methods with Logout/Login', () => {
193
+ const testUser = generateTestUser('auth-all');
194
+ let totpSecret: string | null = null;
195
+
196
+ test('1.1 Register new user', async ({ page }) => {
197
+ test.skip(!apiAvailable, 'Servers not running');
198
+
199
+ await registerUser(page, testUser);
200
+ await expect(page).toHaveURL(/\/app/);
201
+ console.info(` Registered: ${testUser.email}`);
202
+ });
203
+
204
+ test('1.2 Logout after registration', async ({ page }) => {
205
+ test.skip(!apiAvailable, 'Servers not running');
206
+
207
+ await loginWithEmail(page, testUser.email, testUser.password);
208
+ await waitForURLAndHydration(page, /\/app/);
209
+ await logout(page);
210
+ await expect(page).toHaveURL(/\/auth\/login/);
211
+ });
212
+
213
+ test('1.3 Login with Email & Password', async ({ page }) => {
214
+ test.skip(!apiAvailable, 'Servers not running');
215
+
216
+ await loginWithEmail(page, testUser.email, testUser.password);
217
+ await waitForURLAndHydration(page, /\/app/, { timeout: 10000 });
218
+ // Use first() to avoid strict mode violation (email appears multiple times in UI)
219
+ await expect(page.getByText(testUser.email).first()).toBeVisible();
220
+ });
221
+
222
+ test('1.4 Enable 2FA', async ({ page }) => {
223
+ test.skip(!apiAvailable, 'Servers not running');
224
+
225
+ await loginWithEmail(page, testUser.email, testUser.password);
226
+ await waitForURLAndHydration(page, /\/app/);
227
+ totpSecret = await enable2FA(page, testUser.password);
228
+ });
229
+
230
+ test('1.5 Logout and login with 2FA', async ({ page }) => {
231
+ test.skip(!apiAvailable, 'Servers not running');
232
+ test.skip(!totpSecret, 'TOTP secret not available');
233
+
234
+ await loginWithEmail(page, testUser.email, testUser.password);
235
+ await waitForURLAndHydration(page, /\/auth\/2fa/, { timeout: 10000 });
236
+
237
+ const totpCode = generateTOTP(totpSecret!);
238
+ await page.locator('input').fill(totpCode);
239
+ await page.getByRole('button', { name: /verifizieren|bestätigen/i }).click();
240
+
241
+ await waitForURLAndHydration(page, /\/app/, { timeout: 10000 });
242
+ });
243
+
244
+ test('1.6 Add Passkey with Virtual Authenticator', async ({ page, context }) => {
245
+ test.skip(!apiAvailable, 'Servers not running');
246
+ test.skip(!totpSecret, 'TOTP secret not available');
247
+
248
+ // Login with 2FA
249
+ await loginWithEmail(page, testUser.email, testUser.password);
250
+ await waitForURLAndHydration(page, /\/auth\/2fa/, { timeout: 10000 });
251
+
252
+ const totpCode = generateTOTP(totpSecret!);
253
+ await page.locator('input').fill(totpCode);
254
+ await page.getByRole('button', { name: /verifizieren|bestätigen/i }).click();
255
+ await waitForURLAndHydration(page, /\/app/, { timeout: 10000 });
256
+
257
+ // Setup Virtual Authenticator
258
+ const cdpSession = await context.newCDPSession(page);
259
+ await cdpSession.send('WebAuthn.enable');
260
+
261
+ const { authenticatorId } = await cdpSession.send(
262
+ 'WebAuthn.addVirtualAuthenticator',
263
+ {
264
+ options: {
265
+ protocol: 'ctap2',
266
+ transport: 'internal',
267
+ hasResidentKey: true,
268
+ hasUserVerification: true,
269
+ isUserVerified: true,
270
+ },
271
+ },
272
+ );
273
+
274
+ try {
275
+ await gotoAndWaitForHydration(page, '/app/settings/security');
276
+ await page.getByRole('button', { name: 'Passkey hinzufügen' }).click();
277
+ await page.getByPlaceholder('Name für den Passkey').fill('Test-Passkey');
278
+ await page.getByRole('button', { name: 'Hinzufügen' }).click();
279
+
280
+ await expect(page.getByText('Test-Passkey')).toBeVisible({ timeout: 15000 });
281
+ } finally {
282
+ await cleanupAuthenticator(cdpSession, authenticatorId);
283
+ }
284
+ });
285
+
286
+ test('1.7 Login with Passkey', async ({ page, context }) => {
287
+ test.skip(!apiAvailable, 'Servers not running');
288
+ test.skip(!totpSecret, 'TOTP secret not available');
289
+
290
+ // Setup Virtual Authenticator
291
+ const cdpSession = await context.newCDPSession(page);
292
+ await cdpSession.send('WebAuthn.enable');
293
+
294
+ const { authenticatorId } = await cdpSession.send(
295
+ 'WebAuthn.addVirtualAuthenticator',
296
+ {
297
+ options: {
298
+ protocol: 'ctap2',
299
+ transport: 'internal',
300
+ hasResidentKey: true,
301
+ hasUserVerification: true,
302
+ isUserVerified: true,
303
+ },
304
+ },
305
+ );
306
+
307
+ try {
308
+ // First add passkey while logged in via 2FA
309
+ await loginWithEmail(page, testUser.email, testUser.password);
310
+ await waitForURLAndHydration(page, /\/auth\/2fa/, { timeout: 10000 });
311
+
312
+ const totpCode = generateTOTP(totpSecret!);
313
+ await page.locator('input').fill(totpCode);
314
+ await page.getByRole('button', { name: /verifizieren|bestätigen/i }).click();
315
+ await waitForURLAndHydration(page, /\/app/, { timeout: 10000 });
316
+
317
+ // Add passkey
318
+ await gotoAndWaitForHydration(page, '/app/settings/security');
319
+ await page.getByRole('button', { name: 'Passkey hinzufügen' }).click();
320
+ await page.getByPlaceholder('Name für den Passkey').fill('Login-Passkey');
321
+ await page.getByRole('button', { name: 'Hinzufügen' }).click();
322
+ await expect(page.getByText('Login-Passkey')).toBeVisible({ timeout: 15000 });
323
+
324
+ // Logout
325
+ await logout(page);
326
+
327
+ // Login with passkey
328
+ await gotoAndWaitForHydration(page, '/auth/login');
329
+ await page.getByRole('button', { name: 'Mit Passkey anmelden' }).click();
330
+ await waitForURLAndHydration(page, /\/app/, { timeout: 15000 });
331
+ } finally {
332
+ await cleanupAuthenticator(cdpSession, authenticatorId);
333
+ }
334
+ });
335
+ });
336
+
337
+ // =============================================================================
338
+ // Test 2: Register -> 2FA -> Passkey (without logout)
339
+ // =============================================================================
340
+
341
+ test.describe.serial('Test 2: Register -> 2FA -> Passkey (no logout)', () => {
342
+ const testUser = generateTestUser('2fa-then-passkey');
343
+
344
+ test('Register, enable 2FA, then add Passkey without logout', async ({ page, context }) => {
345
+ test.skip(!apiAvailable, 'Servers not running');
346
+
347
+ // Register
348
+ await registerUser(page, testUser);
349
+
350
+ // Enable 2FA
351
+ await enable2FA(page, testUser.password);
352
+
353
+ // Add Passkey
354
+ const cdpSession = await context.newCDPSession(page);
355
+ await cdpSession.send('WebAuthn.enable');
356
+
357
+ const { authenticatorId } = await cdpSession.send(
358
+ 'WebAuthn.addVirtualAuthenticator',
359
+ {
360
+ options: {
361
+ protocol: 'ctap2',
362
+ transport: 'internal',
363
+ hasResidentKey: true,
364
+ hasUserVerification: true,
365
+ isUserVerified: true,
366
+ },
367
+ },
368
+ );
369
+
370
+ try {
371
+ await gotoAndWaitForHydration(page, '/app/settings/security');
372
+ await page.getByRole('button', { name: 'Passkey hinzufügen' }).click();
373
+ await page.getByPlaceholder('Name für den Passkey').fill('After-2FA-Passkey');
374
+ await page.getByRole('button', { name: 'Hinzufügen' }).click();
375
+
376
+ await expect(page.getByText('After-2FA-Passkey')).toBeVisible({ timeout: 15000 });
377
+ await expect(page.getByText('2FA ist aktiviert')).toBeVisible();
378
+ } finally {
379
+ await cleanupAuthenticator(cdpSession, authenticatorId);
380
+ }
381
+ });
382
+ });
383
+
384
+ // =============================================================================
385
+ // Test 3: Register -> Passkey -> 2FA (without logout)
386
+ // =============================================================================
387
+
388
+ test.describe.serial('Test 3: Register -> Passkey -> 2FA (no logout)', () => {
389
+ const testUser = generateTestUser('passkey-then-2fa');
390
+
391
+ test('Register, add Passkey, then enable 2FA without logout', async ({ page, context }) => {
392
+ test.skip(!apiAvailable, 'Servers not running');
393
+
394
+ // Register
395
+ await registerUser(page, testUser);
396
+
397
+ // Add Passkey first
398
+ const cdpSession = await context.newCDPSession(page);
399
+ await cdpSession.send('WebAuthn.enable');
400
+
401
+ const { authenticatorId } = await cdpSession.send(
402
+ 'WebAuthn.addVirtualAuthenticator',
403
+ {
404
+ options: {
405
+ protocol: 'ctap2',
406
+ transport: 'internal',
407
+ hasResidentKey: true,
408
+ hasUserVerification: true,
409
+ isUserVerified: true,
410
+ },
411
+ },
412
+ );
413
+
414
+ try {
415
+ await gotoAndWaitForHydration(page, '/app/settings/security');
416
+ await page.getByRole('button', { name: 'Passkey hinzufügen' }).click();
417
+ await page.getByPlaceholder('Name für den Passkey').fill('Before-2FA-Passkey');
418
+ await page.getByRole('button', { name: 'Hinzufügen' }).click();
419
+
420
+ await expect(page.getByText('Before-2FA-Passkey')).toBeVisible({ timeout: 15000 });
421
+
422
+ // Enable 2FA
423
+ await enable2FA(page, testUser.password);
424
+
425
+ // Verify both are active
426
+ await gotoAndWaitForHydration(page, '/app/settings/security');
427
+ await expect(page.getByText('2FA ist aktiviert')).toBeVisible();
428
+ await expect(page.getByText('Before-2FA-Passkey')).toBeVisible();
429
+ } finally {
430
+ await cleanupAuthenticator(cdpSession, authenticatorId);
431
+ }
432
+ });
433
+ });
434
+
435
+ // =============================================================================
436
+ // Test 4: Error Translations
437
+ // =============================================================================
438
+
439
+ test.describe('Test 4: Error Translations', () => {
440
+ test('4.1 Invalid credentials shows German error message', async ({ page }) => {
441
+ test.skip(!apiAvailable, 'Servers not running');
442
+
443
+ await loginWithEmail(page, 'invalid@test.com', 'WrongPassword123!');
444
+
445
+ // Wait for toast notification (use more specific selector)
446
+ const toast = page.locator('li[role="alert"]');
447
+ await expect(toast).toBeVisible({ timeout: 10000 });
448
+ await expect(toast).toContainText('Ungültige Anmeldedaten');
449
+ });
450
+
451
+ test('4.2 Error translations are loaded from backend', async ({ page }) => {
452
+ test.skip(!apiAvailable, 'Servers not running');
453
+
454
+ await gotoAndWaitForHydration(page, '/auth/login');
455
+
456
+ // Check translations endpoint
457
+ const response = await page.request.get('http://localhost:3001/api/i18n/errors/de');
458
+ expect([200, 304]).toContain(response.status());
459
+
460
+ if (response.status() === 200) {
461
+ const data = await response.json();
462
+ expect(data).toHaveProperty('errors');
463
+ expect(data.errors).toHaveProperty('LTNS_0010');
464
+ console.info(` Error translations loaded: ${Object.keys(data.errors).length} codes`);
465
+ }
466
+ });
467
+ });