From 6dab36339a9a7adca97b21d2be8549e804e26b4c Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:50:01 +0200 Subject: [PATCH] test(e2e): Foto-Import Happy-Path und Offline-Icon --- tests/e2e/remote/photo-import.spec.ts | 80 ++++++++++++++++++ .../fixtures/photo-recipe/sample-printed.jpg | Bin 0 -> 519 bytes 2 files changed, 80 insertions(+) create mode 100644 tests/e2e/remote/photo-import.spec.ts create mode 100644 tests/fixtures/photo-recipe/sample-printed.jpg diff --git a/tests/e2e/remote/photo-import.spec.ts b/tests/e2e/remote/photo-import.spec.ts new file mode 100644 index 0000000..4f0cc33 --- /dev/null +++ b/tests/e2e/remote/photo-import.spec.ts @@ -0,0 +1,80 @@ +import { test, expect } from '@playwright/test'; +import { resolve } from 'node:path'; + +// Wir stubben den Extract-Endpoint server-side nicht (das Feature liegt auf +// dev hinter dem echten Gemini-Key), sondern auf context-Ebene: Playwright +// fängt alle Requests auf /api/recipes/extract-from-photo ab und liefert +// einen deterministischen JSON-Body zurück. Keine Gemini-Kosten in CI. + +test('Foto-Import Happy-Path mit gestubtem Extract-Endpoint', async ({ + page, + context +}) => { + await context.route('**/api/recipes/extract-from-photo', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + recipe: { + id: null, + title: 'E2E Testrezept', + description: 'Aus dem Bild herbeigezaubert.', + source_url: null, + source_domain: null, + image_path: null, + servings_default: 2, + servings_unit: 'Portionen', + prep_time_min: 5, + cook_time_min: 10, + total_time_min: null, + cuisine: null, + category: null, + ingredients: [ + { + position: 1, + quantity: 1, + unit: 'Stk', + name: 'E2E-Apfel', + note: null, + raw_text: '1 Stk E2E-Apfel', + section_heading: null + } + ], + steps: [{ position: 1, text: 'Apfel waschen.' }], + tags: [] + } + }) + }); + }); + + await page.goto('/new/from-photo'); + await expect(page.getByRole('heading', { name: 'Rezept aus Foto' })).toBeVisible(); + + const fixture = resolve(__dirname, '../../fixtures/photo-recipe/sample-printed.jpg'); + await page.locator('input[type="file"]').setInputFiles(fixture); + + await expect(page.getByText('Aus Foto erstellt')).toBeVisible({ timeout: 5000 }); + // Titel-Feld (das erste text-input im Editor) + await expect(page.locator('input[type="text"]').first()).toHaveValue( + 'E2E Testrezept' + ); +}); + +test('Camera-Icon im Header wird disabled, wenn der Client offline geht', async ({ + page, + context +}) => { + await page.goto('/'); + const icon = page.locator('[aria-label="Rezept aus Foto erstellen"]'); + // Nur relevant, wenn der Dev-Server einen Gemini-Key hat — andernfalls ist + // das Icon per Graceful-Degradation gar nicht gerendert und der Test wird + // hier early-skipped. (Im Prod und Dev mit Key gilt der zweite Pfad.) + if ((await icon.count()) === 0) { + test.skip(true, 'Dev-Env hat keinen GEMINI_API_KEY gesetzt.'); + return; + } + await expect(icon).toBeVisible(); + await context.setOffline(true); + await page.waitForFunction(() => !navigator.onLine); + await expect(icon).toHaveClass(/disabled/); +}); diff --git a/tests/fixtures/photo-recipe/sample-printed.jpg b/tests/fixtures/photo-recipe/sample-printed.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0297e2997dc0f7eaef10707c330bc916e76294e4 GIT binary patch literal 519 zcmd^&O%8%E6olWi(83E*8fw9+foRZTB;f`wJgGNv;esRhbC?S5kdB|aR1L* G?jCP5w