feat: support external and step images with structured cooking instructions
This commit is contained in:
1 parent
361862ad0b
commit
0ec1d45bdb
34 files changed
+898
-92
No files matched your search
@@ -36,6 +36,9 @@ test('admin creates and edits recipes with recoverable drafts and image upload',
|
||||
await page.getByRole('textbox', { name: 'Anleitung 1' }).fill('In Salzwasser weich kochen.');
|
||||
await page.getByRole('button', { name: 'Kartoffeln', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Timer hinzufügen' }).click();
|
||||
await page.getByText('Bilder zu Schritt 1 (0)', { exact: true }).click();
|
||||
await page.getByLabel('Schritt 1: Bild hochladen').setInputFiles('public/images/garden.jpg');
|
||||
await expect(page.getByRole('img', { name: 'Schritt 1: Bild 1', exact: true })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Live-Vorschau', exact: true }).click();
|
||||
await expect(page.locator('.editor-preview')).toContainText('400 g Kartoffeln');
|
||||
await page.screenshot({ path: 'docs/screenshots/editor-desktop.png', fullPage: true });
|
||||
@@ -58,11 +61,29 @@ test('admin creates and edits recipes with recoverable drafts and image upload',
|
||||
const download = await downloadPromise;
|
||||
const data = JSON.parse(await readFile((await download.path())!, 'utf8'));
|
||||
expect(data.recipes[0].title).toBe('Browser-Küchenrezept verbessert');
|
||||
const stepImage = data.recipes[0].steps[0].images[0];
|
||||
expect(stepImage).toMatch(/^\/media\/.+\.webp$/);
|
||||
expect(
|
||||
(
|
||||
await page.request.delete('/api/media', {
|
||||
headers: { origin: 'http://localhost:3100' },
|
||||
data: { name: stepImage.split('/').at(-1) },
|
||||
})
|
||||
).status(),
|
||||
).toBe(409);
|
||||
await page.getByRole('button', { name: 'Browser-Küchenrezept verbessert löschen' }).click();
|
||||
await page.getByRole('button', { name: 'Endgültig löschen' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Browser-Küchenrezept verbessert' })).toHaveCount(
|
||||
0,
|
||||
);
|
||||
expect(
|
||||
(
|
||||
await page.request.delete('/api/media', {
|
||||
headers: { origin: 'http://localhost:3100' },
|
||||
data: { name: stepImage.split('/').at(-1) },
|
||||
})
|
||||
).status(),
|
||||
).toBe(200);
|
||||
await page.getByRole('button', { name: 'Abmelden' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Willkommen zurück.' })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,112 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { pasta } from '../fixtures';
|
||||
import { exportRecipes } from '../../src/lib/import';
|
||||
const id = '10000000-0000-4000-8000-000000000001';
|
||||
test('external images and structured steps survive import, editing, scaling and cooking', async ({
|
||||
page,
|
||||
}, info) => {
|
||||
const headers = { origin: 'http://localhost:3100' };
|
||||
const errors: string[] = [];
|
||||
page.on('pageerror', (e) => errors.push(e.message));
|
||||
await page.route('https://images.example.org/**', async (route) => {
|
||||
if (route.request().url().includes('missing')) await route.fulfill({ status: 404, body: '' });
|
||||
else await route.fulfill({ contentType: 'image/jpeg', path: 'public/images/garden.jpg' });
|
||||
});
|
||||
const cover = 'https://images.example.org/cover.jpg?width=1200&quality=80';
|
||||
const stepImage = 'https://images.example.org/step.jpg';
|
||||
const recipe = {
|
||||
...pasta,
|
||||
id: undefined,
|
||||
title: `Bilderküche ${info.project.name}`,
|
||||
image: cover,
|
||||
images: ['https://images.example.org/gallery.jpg'],
|
||||
steps: pasta.steps.map((s, n) =>
|
||||
n === 0
|
||||
? {
|
||||
...s,
|
||||
text: 'Alles für das Pesto vorbereiten.\n\n- {{basilikum}} waschen und trocken schütteln.\n- {{kaese}} fein reiben.\n\n1. Einen großen Topf Wasser aufsetzen.\n2. Die Zutaten bereitstellen.',
|
||||
images: [stepImage, 'https://images.example.org/missing.jpg'],
|
||||
}
|
||||
: s,
|
||||
),
|
||||
};
|
||||
expect(
|
||||
(
|
||||
await page.request.post('/api/auth/login', {
|
||||
headers,
|
||||
data: { username: 'testadmin', password: process.env.TEST_PASSWORD },
|
||||
})
|
||||
).ok(),
|
||||
).toBe(true);
|
||||
const imported = await page.request.post('/api/import', {
|
||||
headers,
|
||||
data: { format: 'json', preview: false, raw: exportRecipes([recipe], 'json') },
|
||||
});
|
||||
expect(imported.status()).toBe(201);
|
||||
const [saved] = await imported.json();
|
||||
try {
|
||||
await page.goto(`/admin/rezepte/${saved.id}`);
|
||||
await expect(page.getByLabel('Titelbild-Adresse (HTTPS oder lokaler Bildpfad)')).toHaveValue(
|
||||
cover,
|
||||
);
|
||||
await page.getByRole('button', { name: 'Zubereitung', exact: true }).click();
|
||||
await page.getByText('Bilder zu Schritt 1 (2)', { exact: true }).click();
|
||||
await page
|
||||
.getByLabel('Schritt 1: Bild: Bildadresse', { exact: true })
|
||||
.fill('https://images.example.org/another.jpg');
|
||||
await page.getByRole('button', { name: 'Schritt 1: Bild verlinken', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Rezept speichern', exact: true }).click();
|
||||
await expect(page.getByRole('status')).toContainText('erfolgreich gespeichert');
|
||||
const exported = await (await page.request.get(`/api/export?id=${saved.id}`)).json();
|
||||
expect(exported.recipes[0].steps[0].images).toEqual([
|
||||
...recipe.steps[0].images,
|
||||
'https://images.example.org/another.jpg',
|
||||
]);
|
||||
const response = await page.goto(`/rezepte/${saved.id}`);
|
||||
expect(response?.headers()['content-security-policy']).toContain("img-src 'self' https:");
|
||||
const first = page.locator('.preparation-step').first();
|
||||
await expect(first.locator('.step-instructions > p')).toHaveCount(1);
|
||||
await expect(first.locator('.step-instructions > ul > li')).toHaveCount(2);
|
||||
await expect(first.locator('.step-instructions > ol > li')).toHaveCount(2);
|
||||
await first.locator('.step-images').scrollIntoViewIfNeeded();
|
||||
await expect(first.getByRole('img', { name: /Bild 2 – Bild nicht verfügbar/ })).toBeVisible();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.locator('.recipe-cover img').evaluate((img: HTMLImageElement) => img.naturalWidth),
|
||||
)
|
||||
.toBeGreaterThan(0);
|
||||
await expect(first.locator('.step-images img').first()).toHaveJSProperty('complete', true);
|
||||
expect(
|
||||
await first
|
||||
.locator('.step-images img')
|
||||
.first()
|
||||
.evaluate((img: HTMLImageElement) => img.naturalWidth),
|
||||
).toBeGreaterThan(0);
|
||||
await page.getByRole('spinbutton', { name: 'Portionen', exact: true }).fill('4');
|
||||
await expect(first.locator('.instruction-ingredient').first()).toHaveText('80 g Basilikum');
|
||||
await expect(first.locator('.step-supplies')).toContainText('80 g Basilikum');
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(
|
||||
true,
|
||||
);
|
||||
await page.screenshot({ path: info.outputPath('structured-steps.png'), fullPage: true });
|
||||
await page.getByRole('button', { name: 'Theme wechseln', exact: true }).click();
|
||||
await first.screenshot({ path: info.outputPath('structured-step-dark.png') });
|
||||
await page.goto(`/rezepte/${saved.id}/kochen?faktor=2`);
|
||||
await expect(page.locator('.cooking-instruction .instruction-ingredient').first()).toHaveText(
|
||||
'80 g Basilikum',
|
||||
);
|
||||
await expect(page.locator('.cooking-step .step-images img')).toHaveCount(2);
|
||||
await expect(page.locator('.cooking-step')).toHaveCSS('opacity', '1');
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(
|
||||
true,
|
||||
);
|
||||
await page.screenshot({ path: info.outputPath('structured-cooking.png'), fullPage: true });
|
||||
expect(errors).toEqual([]);
|
||||
} finally {
|
||||
await page.request.delete(`/api/recipes/${saved.id}`, { headers });
|
||||
}
|
||||
});
|
||||
test('editor remains usable on the current viewport in dark mode', async ({ page }, info) => {
|
||||
await page.goto('/admin');
|
||||
await page.getByLabel('Benutzername').fill('testadmin');
|
||||
@@ -146,6 +252,7 @@ test('guided cooking restores scaled progress and timestamp timers through navig
|
||||
await expect(page.getByRole('heading', { name: 'Das Pesto zum Leuchten bringen' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Pinienkerne rösten starten' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Pinienkerne rösten pausieren' })).toBeVisible();
|
||||
await expect(page.locator('.cooking-step')).toHaveCSS('opacity', '1');
|
||||
await page.screenshot({
|
||||
path: `docs/screenshots/cooking-${info.project.name}.png`,
|
||||
fullPage: true,
|
||||
|
||||
Reference in new issue
Block a user