test(e2e): Foto-Import Happy-Path und Offline-Icon
This commit is contained in:
80
tests/e2e/remote/photo-import.spec.ts
Normal file
80
tests/e2e/remote/photo-import.spec.ts
Normal file
@@ -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/);
|
||||||
|
});
|
||||||
BIN
tests/fixtures/photo-recipe/sample-printed.jpg
vendored
Normal file
BIN
tests/fixtures/photo-recipe/sample-printed.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 519 B |
Reference in New Issue
Block a user