diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 6cc9579..b26272c 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -46,7 +46,9 @@ Faktoren bleiben als hochpräzise Dezimalstrings erhalten. Neue Zielmengen werde Änderungen werden lokal im Browser gesichert, ohne das veröffentlichte Rezept zu verändern. „Rezept speichern“ validiert und überträgt ausdrücklich auf den Server. Der Browser warnt beim Verlassen mit noch nicht auf den Server übertragenen Änderungen. Konflikte mit einer zwischenzeitlich geänderten Fassung werden als 409 zurückgegeben. Lokale Entwürfe bleiben dann erhalten. -Zutaten und Schritte lassen sich per Drag-Handle sowie per Auf/Ab-Buttons sortieren. So ist die Funktion auch ohne Maus bedienbar. Bilder werden sofort in die lokale Medienablage hochgeladen und können dort später entfernt werden, solange kein Rezept sie verwendet. +Zutaten werden als kompakte Liste mit Name, Menge und Einheit erfasst. Abschnitt, Hinweis und die optionale Kennzeichnung lassen sich pro Zeile aufklappen; ausgefüllte Details bleiben als kurze Zusammenfassung sichtbar. Enter im Zutatenname fügt die nächste Zeile hinzu und fokussiert sie direkt. Eine bereits leere Zeile wird wiederverwendet. Neue Zutaten übernehmen den letzten Abschnitt. + +Zutaten und Schritte lassen sich per Drag-Handle sowie per Auf/Ab-Buttons sortieren; bei Zutaten befinden sich die Buttons in den aufgeklappten Details. So ist die Funktion auch ohne Maus bedienbar. Bilder werden sofort in die lokale Medienablage hochgeladen und können dort später entfernt werden, solange kein Rezept sie verwendet. ## Tests diff --git a/docs/VERIFICATION.md b/docs/VERIFICATION.md index 1a2dc35..7e6f0b8 100644 --- a/docs/VERIFICATION.md +++ b/docs/VERIFICATION.md @@ -60,10 +60,14 @@ Ein erster Docker-Prüflauf deckte einen wiederverwendeten, durch den Containern ## Visuelle Inspektion -16 reale Browser-Screenshots liegen in [`docs/screenshots/`](screenshots/): Bibliothek, Rezeptseite, Kochmodus und Favoriten in Desktop/Mobile/Tablet sowie Editor in hellem und dunklem Design. +19 reale Browser-Screenshots liegen in [`docs/screenshots/`](screenshots/): Bibliothek, Rezeptseite, Kochmodus und Favoriten in Desktop/Mobile/Tablet sowie Editor und kompakte Zutatenliste in hellem und dunklem Design. Tatsächlich angesehen: Desktop- und Mobile-Bibliothek, Tablet-Bibliothek, mobile Rezeptseite, mobiler Kochmodus, Desktop-Editor, mobiler Dark-Mode-Editor und dunkle Favoritenansicht. Dabei wurden eine falsche Suchfeld-Flexrichtung, ein zu schmaler mobiler Infobereich und eine Timer-Anzeigeabweichung nach Start korrigiert. Die aktualisierten Bilder wurden erneut kontrolliert. Browserprüfungen bestätigen fehlenden horizontalen Überlauf in den geprüften Bibliotheks-, Rezept- und Editoransichten. +### Nachprüfung: kompakter Zutateneditor + +Nach der gestalterischen Überarbeitung wurden TypeScript-Prüfung und Produktionsbuild erneut erfolgreich ausgeführt. Die vier betroffenen Browserabläufe bestanden: vollständige Rezeptanlage/-bearbeitung auf Desktop sowie der Zutateneditor auf Desktop, Smartphone und Tablet. Geprüft wurden aufklappbare Details, Mengen, optionale Zutaten, Fokus nach Hinzufügen, Enter zum Weitererfassen, Reihenfolge per Buttons und Desktop-Drag-and-drop, Entwurfswiederherstellung sowie fehlender horizontaler Überlauf mit und ohne Live-Vorschau. Ein zunächst falsch auf `textbox` statt `combobox` gerichteter Testselektor für die Abschnittsauswahl wurde korrigiert; anschließend bestanden alle vier Tests. Die neuen Screenshots der hellen Desktop-/Smartphone-Ansicht und der dunklen Tablet-Ansicht wurden visuell kontrolliert. + ## Präzise Grenzen - Kein Test auf physischem Smartphone, Safari oder Firefox; keine tatsächliche iOS-/Android-Installation über das Betriebssystem-Installationsmenü. diff --git a/docs/screenshots/editor-dark-desktop.png b/docs/screenshots/editor-dark-desktop.png index 95d814a..67576dd 100644 Binary files a/docs/screenshots/editor-dark-desktop.png and b/docs/screenshots/editor-dark-desktop.png differ diff --git a/docs/screenshots/editor-dark-mobile.png b/docs/screenshots/editor-dark-mobile.png index 1635290..d3ff5cb 100644 Binary files a/docs/screenshots/editor-dark-mobile.png and b/docs/screenshots/editor-dark-mobile.png differ diff --git a/docs/screenshots/editor-dark-tablet.png b/docs/screenshots/editor-dark-tablet.png index 1d91b9c..ebd59a0 100644 Binary files a/docs/screenshots/editor-dark-tablet.png and b/docs/screenshots/editor-dark-tablet.png differ diff --git a/docs/screenshots/editor-desktop.png b/docs/screenshots/editor-desktop.png index f77bc34..b84fa0b 100644 Binary files a/docs/screenshots/editor-desktop.png and b/docs/screenshots/editor-desktop.png differ diff --git a/docs/screenshots/ingredients-light-desktop.png b/docs/screenshots/ingredients-light-desktop.png new file mode 100644 index 0000000..be72779 Binary files /dev/null and b/docs/screenshots/ingredients-light-desktop.png differ diff --git a/docs/screenshots/ingredients-light-mobile.png b/docs/screenshots/ingredients-light-mobile.png new file mode 100644 index 0000000..2463da0 Binary files /dev/null and b/docs/screenshots/ingredients-light-mobile.png differ diff --git a/docs/screenshots/ingredients-light-tablet.png b/docs/screenshots/ingredients-light-tablet.png new file mode 100644 index 0000000..961a7e0 Binary files /dev/null and b/docs/screenshots/ingredients-light-tablet.png differ diff --git a/src/app/globals.css b/src/app/globals.css index fb4f240..3597249 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1878,16 +1878,6 @@ input[type='checkbox'] { .image-upload label { margin: 0; } -.editor-ingredient { - display: flex; - gap: 12px; - border: 1px solid var(--line); - background: var(--bg); - padding: 17px; - border-radius: 12px; - margin-bottom: 14px; - align-items: flex-start; -} .reorder { display: flex; flex-direction: column; @@ -1904,44 +1894,6 @@ input[type='checkbox'] { cursor: grab; display: flex; } -.ingredient-fields { - flex: 1; - min-width: 0; -} -.ingredient-main-fields { - display: grid; - grid-template-columns: 80px 90px minmax(100px, 1fr); - gap: 10px; -} -.ingredient-main-fields label { - font-size: 10px; - margin-bottom: 10px; -} -.ingredient-secondary-fields { - display: flex; - gap: 8px; - align-items: center; - margin-bottom: 8px; -} -.ingredient-secondary-fields > input { - font-size: 10px; - padding: 7px; - flex: 1; - min-width: 50px; -} -.ingredient-secondary-fields .checkbox-label { - font-size: 10px; - margin: 0; -} -.ingredient-fields > small { - font-size: 8px; - overflow-wrap: anywhere; -} -.editor-ingredient > .icon-button { - width: 25px; - height: 30px; - margin-top: 24px; -} .editor-step { border: 1px solid var(--line); border-radius: 14px; @@ -2779,38 +2731,9 @@ input[type='checkbox'] { .image-upload > img { height: 180px; } - .editor-ingredient { - padding: 12px; - gap: 6px; - position: relative; - } - .ingredient-main-fields { - grid-template-columns: 1fr 1fr; - gap: 8px; - padding-right: 15px; - } - .ingredient-main-fields > label:nth-child(3) { - grid-column: 1/-1; - } - .ingredient-secondary-fields { - flex-wrap: wrap; - } - .ingredient-secondary-fields > input { - flex-basis: 100%; - } - .editor-ingredient > .icon-button { - position: absolute; - top: 6px; - right: 3px; - margin: 0; - width: 24px; - } .reorder { padding-top: 20px; } - .ingredient-fields small { - font-size: 7px; - } .editor-step { padding: 15px; } diff --git a/src/components/editor.tsx b/src/components/editor.tsx index 12af77d..e0eeb0a 100644 --- a/src/components/editor.tsx +++ b/src/components/editor.tsx @@ -15,18 +15,11 @@ import { Check, X, } from 'lucide-react'; -import { - type Recipe, - type SavedRecipe, - type Ingredient, - type Step, - emptyRecipe, - units, - recipeSchema, -} from '@/lib/recipe'; +import { type Recipe, type SavedRecipe, type Step, emptyRecipe, recipeSchema } from '@/lib/recipe'; import { api, readLocal, writeLocal, toast } from '@/lib/client'; import { uploadImage } from './admin'; import { renderStep, ingredientLabel } from '@/lib/calculator'; +import { IngredientEditor } from './ingredient-editor'; export function RecipeEditor({ initial }: { initial?: SavedRecipe }) { const router = useRouter(), key = `hamham:draft:${initial?.id ?? 'new'}`; @@ -78,11 +71,6 @@ export function RecipeEditor({ initial }: { initial?: SavedRecipe }) { dirty.current = true; setRecipe((r) => ({ ...r, [key]: value })); }; - const updateIngredient = (n: number, patch: Partial) => - change( - 'ingredients', - recipe.ingredients.map((i, index) => (index === n ? { ...i, ...patch } : i)), - ); const updateStep = (n: number, patch: Partial) => change( 'steps', @@ -384,134 +372,26 @@ export function RecipeEditor({ initial }: { initial?: SavedRecipe }) { )} {tab === 'ingredients' && ( -
-

Alles, was du brauchst.

-

- Menge leer lassen für „nach Geschmack“. Sortieren per Ziehen oder Pfeiltasten. -

-
- {recipe.ingredients.map((i, n) => ( -
e.preventDefault()} - onDrop={() => { - if (dragging.current !== null) move('ingredients', dragging.current, n); - }} - > - {reorder('ingredients', n)} -
-
- - - -
-
- updateIngredient(n, { section: e.target.value })} - placeholder="Abschnitt, z. B. Sauce" - /> - updateIngredient(n, { note: e.target.value })} - placeholder="Hinweis, z. B. fein gehackt" - /> - -
- Referenz: {`{{${i.id}}}`} -
- -
- ))} -
- -
+ change( + 'ingredients', + recipe.ingredients.filter((i) => i.id !== id), + ); + }} + /> )} {tab === 'steps' && (
diff --git a/src/components/ingredient-editor.css b/src/components/ingredient-editor.css new file mode 100644 index 0000000..8f46d3c --- /dev/null +++ b/src/components/ingredient-editor.css @@ -0,0 +1,332 @@ +.ingredient-composer { + container: ingredient-editor / inline-size; +} +.ingredient-composer-heading { + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; + margin: 4px 0 32px; +} +.ingredient-composer-heading .eyebrow { + font-size: 8px; + letter-spacing: 1.5px; + color: var(--accent); + margin-bottom: 12px; +} +.ingredient-composer-heading h2 { + margin-bottom: 10px; +} +.ingredient-composer-heading p { + color: var(--muted); + font-size: 13px; + margin: 0; +} +.ingredient-count { + display: inline-flex; + gap: 7px; + align-items: center; + flex-shrink: 0; + padding: 8px 13px; + border-radius: 30px; + background: var(--soft-green); + color: var(--green); + font-size: 11px; +} +.ingredient-table-head, +.ingredient-entry-line { + display: grid; + grid-template-columns: 24px minmax(120px, 1fr) 88px 88px 38px 38px; + align-items: center; + gap: 10px; + padding: 0 8px; +} +.ingredient-table-head { + padding-bottom: 12px; + font-size: 9px; + letter-spacing: 1px; + text-transform: uppercase; + color: var(--muted); +} +.ingredient-table-head span:nth-child(2) { + padding-left: 10px; +} +.ingredient-table-head span:nth-child(3), +.ingredient-table-head span:nth-child(4) { + text-align: center; +} +.ingredient-entries { + border-top: 1px solid var(--line); +} +.ingredient-entry { + padding: 12px 0; + border-bottom: 1px solid var(--line); + transition: + background 0.18s, + opacity 0.18s; +} +.ingredient-entry:focus-within, +.ingredient-entry.expanded { + background: var(--bg); +} +.ingredient-entry.dragging { + opacity: 0.45; +} +.ingredient-entry.drop-target { + box-shadow: inset 0 2px var(--green); +} +.ingredient-grip { + display: flex; + justify-content: center; + align-items: center; + min-height: 44px; + color: var(--muted); + cursor: grab; +} +.ingredient-grip:active { + cursor: grabbing; +} +.ingredient-entry-name { + min-width: 0; +} +.ingredient-entry-name > input { + font-size: 14px; + font-weight: 500; + background: transparent; + border-color: transparent; + padding: 10px; + min-height: 44px; +} +.ingredient-entry-name > input:hover { + border-color: var(--line); +} +.ingredient-entry-name > input:focus { + background: var(--surface); +} +.ingredient-entry-line > input, +.ingredient-entry-line > select { + background: var(--surface-2); + border-color: transparent; + font-size: 13px; + padding: 10px 8px; + min-height: 44px; +} +.ingredient-entry-amount { + text-align: center; + -moz-appearance: textfield; +} +.ingredient-entry-amount::-webkit-inner-spin-button { + -webkit-appearance: none; +} +.ingredient-entry-unit { + text-align: center; +} +.ingredient-detail-toggle, +.ingredient-remove { + display: grid; + place-items: center; + width: 38px; + height: 44px; + border-radius: 8px; + color: var(--muted); +} +.ingredient-detail-toggle:hover, +.ingredient-detail-toggle[aria-expanded='true'] { + background: var(--soft-green); + color: var(--green); +} +.ingredient-detail-toggle svg { + transition: transform 0.18s; +} +.ingredient-detail-toggle[aria-expanded='true'] svg { + transform: rotate(180deg); +} +.ingredient-remove:hover:not(:disabled) { + color: var(--accent); + background: var(--sand); +} +.ingredient-entry-summary { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px 10px; + font-size: 10px; + color: var(--muted); + padding: 0 10px 4px; + overflow-wrap: anywhere; +} +.ingredient-section-label, +.ingredient-optional-label { + font-size: 9px; + padding: 2px 7px; + border-radius: 5px; + background: var(--soft-green); + color: var(--green); +} +.ingredient-optional-label { + background: var(--sand); + color: var(--ink); +} +.ingredient-entry-details { + padding: 18px 18px 4px 52px; +} +.ingredient-detail-fields { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} +.ingredient-detail-fields label { + margin-bottom: 16px; + font-size: 11px; + color: var(--muted); +} +.ingredient-detail-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} +.ingredient-detail-footer .checkbox-label { + margin: 0; + font-size: 11px; +} +.ingredient-order-actions { + display: flex; + align-items: center; + gap: 5px; + font-size: 10px; + color: var(--muted); +} +.ingredient-order-actions > span { + margin-right: 7px; +} +.ingredient-order-actions button { + display: grid; + place-items: center; + width: 36px; + height: 40px; + border-radius: 8px; +} +.ingredient-order-actions button:hover:not(:disabled) { + background: var(--surface-2); + color: var(--green); +} +.ingredient-add { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 14px 18px; + margin: 18px 0 14px; + border-radius: 10px; + background: var(--soft-green); + color: var(--green); + font-size: 12px; + font-weight: 500; + text-align: left; + transition: background 0.18s; +} +.ingredient-add > span { + display: grid; + place-items: center; + width: 28px; + height: 28px; + background: var(--surface); + border-radius: 50%; +} +.ingredient-add:hover { + background: var(--surface-2); +} +.ingredient-add kbd { + margin-left: auto; + font: inherit; + font-size: 16px; + opacity: 0.6; +} +.editor-panel > .ingredient-composer-hint { + margin: 0; + font-size: 11px; + color: var(--muted); + line-height: 1.7; +} +@container ingredient-editor (max-width: 560px) { + .ingredient-composer-heading { + align-items: flex-start; + flex-direction: column; + gap: 14px; + margin-bottom: 22px; + } + .ingredient-composer-heading h2 { + font-size: 27px; + } + .ingredient-composer-heading .eyebrow { + font-size: 7px; + } + .ingredient-count { + font-size: 10px; + padding: 5px 10px; + } + .ingredient-table-head { + display: none; + } + .ingredient-entry-line { + grid-template-columns: 20px minmax(0, 1fr) 72px 36px 36px; + gap: 6px; + padding: 0 4px; + } + .ingredient-grip { + grid-column: 1; + grid-row: 1; + } + .ingredient-entry-name { + grid-column: 2 / -1; + grid-row: 1; + } + .ingredient-entry-name > input { + font-size: 16px; + padding-left: 6px; + } + .ingredient-entry-line > input, + .ingredient-entry-line > select { + font-size: 16px; + } + .ingredient-entry-amount { + grid-column: 2; + grid-row: 2; + } + .ingredient-entry-unit { + grid-column: 3; + grid-row: 2; + } + .ingredient-detail-toggle { + grid-column: 4; + grid-row: 2; + width: 36px; + } + .ingredient-remove { + grid-column: 5; + grid-row: 2; + width: 36px; + } + .ingredient-entry-summary { + padding-left: 6px; + } + .ingredient-entry-details { + padding: 18px 12px 4px 30px; + } + .ingredient-detail-fields { + grid-template-columns: 1fr; + gap: 0; + } + .ingredient-detail-fields input { + font-size: 16px; + } + .ingredient-detail-footer { + align-items: flex-start; + flex-direction: column; + gap: 6px; + } + .ingredient-order-actions { + align-self: flex-end; + } +} diff --git a/src/components/ingredient-editor.tsx b/src/components/ingredient-editor.tsx new file mode 100644 index 0000000..46eddce --- /dev/null +++ b/src/components/ingredient-editor.tsx @@ -0,0 +1,275 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { ArrowDown, ArrowUp, ChevronDown, GripVertical, Leaf, Plus, Trash2 } from 'lucide-react'; +import { type Ingredient, units } from '@/lib/recipe'; +import './ingredient-editor.css'; + +export function IngredientEditor({ + ingredients, + onChange, + onRemove, +}: { + ingredients: Ingredient[]; + onChange: (ingredients: Ingredient[]) => void; + onRemove: (id: string) => void; +}) { + const [expanded, setExpanded] = useState([]); + const [dragging, setDragging] = useState(null); + const [dropTarget, setDropTarget] = useState(null); + const [announcement, setAnnouncement] = useState(''); + const names = useRef(new Map()); + const pendingFocus = useRef(null); + const sections = Array.from(new Set(ingredients.map((i) => i.section).filter(Boolean))); + const count = ingredients.filter((i) => i.name.trim()).length; + + useEffect(() => { + if (pendingFocus.current) { + names.current.get(pendingFocus.current)?.focus(); + pendingFocus.current = null; + } + }, [ingredients]); + + function update(id: string, patch: Partial) { + onChange(ingredients.map((i) => (i.id === id ? { ...i, ...patch } : i))); + } + + function add() { + const empty = ingredients.find((i) => !i.name.trim()); + if (empty) { + names.current.get(empty.id)?.focus(); + return; + } + const id = crypto.randomUUID(); + pendingFocus.current = id; + onChange([ + ...ingredients, + { + id, + name: '', + amount: null, + unit: 'g', + section: ingredients.at(-1)?.section ?? '', + note: '', + optional: false, + }, + ]); + } + + function move(id: string, destination: number) { + const from = ingredients.findIndex((i) => i.id === id); + if (from < 0 || destination < 0 || destination >= ingredients.length || from === destination) + return; + const next = [...ingredients]; + next.splice(destination, 0, next.splice(from, 1)[0]); + onChange(next); + setAnnouncement( + `${ingredients[from].name || 'Zutat'} an Position ${destination + 1} verschoben.`, + ); + } + + return ( +
+
+
+ DIE GUTEN DINGE KOMMEN ZUSAMMEN +

Alles, was du brauchst.

+

Zutat für Zutat zu deinem Lieblingsgericht.

+
+ + + {count} {count === 1 ? 'Zutat' : 'Zutaten'} + +
+ +
+ {ingredients.map((i, n) => { + const open = expanded.includes(i.id); + const detailId = `ingredient-details-${i.id}`; + return ( +
{ + if (!dragging) return; + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + setDropTarget(i.id); + }} + onDrop={(e) => { + e.preventDefault(); + if (dragging) move(dragging, n); + setDragging(null); + setDropTarget(null); + }} + > +
+ { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', i.id); + setDragging(i.id); + }} + onDragEnd={() => { + setDragging(null); + setDropTarget(null); + }} + > + + +
+ { + if (element) names.current.set(i.id, element); + else names.current.delete(i.id); + }} + aria-label={`Zutat ${n + 1}`} + value={i.name} + onChange={(e) => update(i.id, { name: e.target.value })} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.nativeEvent.isComposing) { + e.preventDefault(); + if (i.name.trim()) add(); + } + }} + placeholder={n === 0 ? 'Was kommt in dein Rezept?' : 'Nächste Zutat …'} + /> + {!open && (i.section || i.note || i.optional) && ( +
+ {i.section && {i.section}} + {i.note && {i.note}} + {i.optional && optional} +
+ )} +
+ + update(i.id, { amount: e.target.value === '' ? null : +e.target.value }) + } + /> + + + +
+ +
+ ); + })} +
+ +

+ Keine feste Menge? Lass das Mengenfeld einfach leer. Details findest du am kleinen Pfeil. +

+ + {sections.map((s) => ( + + + {announcement} + +
+ ); +} diff --git a/tests/e2e/public.spec.ts b/tests/e2e/public.spec.ts index 9a01c91..c435125 100644 --- a/tests/e2e/public.spec.ts +++ b/tests/e2e/public.spec.ts @@ -10,16 +10,57 @@ test('editor remains usable on the current viewport in dark mode', async ({ page await page.getByRole('button', { name: 'Theme wechseln', exact: true }).click(); await page.getByRole('button', { name: 'Zutaten', exact: true }).click(); await page.getByRole('textbox', { name: 'Zutat 1', exact: true }).fill('Kartoffeln'); + await page.getByRole('spinbutton', { name: 'Menge 1', exact: true }).fill('400'); + await page.getByRole('button', { name: 'Details für Zutat 1', exact: true }).click(); + await page.getByRole('combobox', { name: 'Abschnitt 1', exact: true }).fill('Für das Gemüse'); + await page + .getByRole('textbox', { name: 'Hinweis 1', exact: true }) + .fill('geschält und gewürfelt'); + await page.getByRole('button', { name: 'Details für Zutat 1', exact: true }).click(); await page.getByRole('button', { name: 'Zutat hinzufügen' }).click(); + await expect(page.getByRole('textbox', { name: 'Zutat 2', exact: true })).toBeFocused(); await page.getByRole('textbox', { name: 'Zutat 2', exact: true }).fill('Karotten'); + await page.getByRole('spinbutton', { name: 'Menge 2', exact: true }).fill('200'); + await page.getByRole('button', { name: 'Details für Zutat 2', exact: true }).click(); + await page.getByRole('checkbox', { name: 'Optionale Zutat' }).check(); await page.getByRole('button', { name: 'Zutat 2 nach oben' }).click(); await expect(page.getByRole('textbox', { name: 'Zutat 1', exact: true })).toHaveValue('Karotten'); + await page.getByRole('button', { name: 'Details für Zutat 1', exact: true }).click(); + await page.getByRole('textbox', { name: 'Zutat 2', exact: true }).press('Enter'); + await expect(page.getByRole('textbox', { name: 'Zutat 3', exact: true })).toBeFocused(); + await page.getByRole('textbox', { name: 'Zutat 3', exact: true }).fill('Zwiebel'); + await page.getByRole('spinbutton', { name: 'Menge 3', exact: true }).fill('1'); + await page.getByRole('combobox', { name: 'Einheit 3', exact: true }).selectOption('Stück'); + await page.getByRole('textbox', { name: 'Zutat 3', exact: true }).press('Enter'); + await page.getByRole('textbox', { name: 'Zutat 4', exact: true }).fill('Olivenöl'); + await page.getByRole('spinbutton', { name: 'Menge 4', exact: true }).fill('2'); + await page.getByRole('combobox', { name: 'Einheit 4', exact: true }).selectOption('EL'); + page.on('dialog', (dialog) => dialog.accept()); + await page.reload(); + await page.getByRole('button', { name: 'Zutaten', exact: true }).click(); + await expect(page.getByRole('textbox', { name: 'Zutat 1', exact: true })).toHaveValue('Karotten'); + await expect(page.locator('.ingredient-entry').nth(0)).toContainText('optional'); + await expect(page.locator('.ingredient-entry').nth(1)).toContainText('geschält und gewürfelt'); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); await page.screenshot({ path: `docs/screenshots/editor-dark-${info.project.name}.png`, fullPage: true, animations: 'disabled', }); + await page.getByRole('button', { name: 'Theme wechseln', exact: true }).click(); + await page.screenshot({ + path: `docs/screenshots/ingredients-light-${info.project.name}.png`, + fullPage: true, + animations: 'disabled', + }); + if (info.project.name === 'desktop') { + await page.locator('.ingredient-grip').first().dragTo(page.locator('.ingredient-entry').nth(1)); + await expect(page.getByRole('textbox', { name: 'Zutat 1', exact: true })).toHaveValue( + 'Kartoffeln', + ); + } + await page.getByRole('button', { name: 'Live-Vorschau', exact: true }).click(); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); }); test('public library, shareable search, favorites, themes and responsive layouts', async ({ page,