import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { extractRecipeFromHtml, hasRecipeMarkup } from '../../src/lib/server/parsers/json-ld-recipe'; const here = dirname(fileURLToPath(import.meta.url)); function load(name: string): string { return readFileSync(join(here, '../fixtures', name), 'utf8'); } describe('extractRecipeFromHtml', () => { it('extracts a recipe from Chefkoch HTML', () => { const html = load('chefkoch-schupfnudeln.html'); const r = extractRecipeFromHtml(html); expect(r).not.toBeNull(); expect(r!.title.toLowerCase()).toContain('schupfnudel'); expect(r!.ingredients.length).toBeGreaterThan(2); expect(r!.steps.length).toBeGreaterThan(0); }); it('extracts a recipe from Emmi kocht einfach HTML', () => { const html = load('emmi-bolognese.html'); const r = extractRecipeFromHtml(html); expect(r).not.toBeNull(); expect(r!.title.toLowerCase()).toContain('bolognese'); expect(r!.ingredients.length).toBeGreaterThan(0); expect(r!.steps.length).toBeGreaterThan(0); }); it('returns null when no Recipe JSON-LD present', () => { const html = '

no recipe

'; expect(extractRecipeFromHtml(html)).toBeNull(); }); it('returns null when JSON-LD has only non-Recipe types', () => { const html = ` `; expect(extractRecipeFromHtml(html)).toBeNull(); }); }); describe('extractRecipeFromHtml — Microdata fallback', () => { it('extracts title, ingredients and HowToStep instructions', () => { const html = `

Königsberger Klopse

Klassische Königsberger Klopse.

4 Ostpreußisch
  1. Hackfleisch und Zwiebel vermengen.
  2. Klopse formen und in Salzwasser garen.
`; const r = extractRecipeFromHtml(html); expect(r).not.toBeNull(); expect(r!.title).toBe('Königsberger Klopse'); expect(r!.ingredients.length).toBe(3); expect(r!.ingredients[0].raw_text).toContain('Hackfleisch'); expect(r!.steps.length).toBe(2); expect(r!.steps[1].text).toContain('Klopse formen'); expect(r!.prep_time_min).toBe(20); expect(r!.cook_time_min).toBe(25); expect(r!.servings_default).toBe(4); expect(r!.cuisine).toBe('Ostpreußisch'); expect(r!.image_path).toBe('/img/klopse.jpg'); }); it('handles plain-text recipeInstructions without HowToStep', () => { const html = `
Test 1 Apfel
  1. Schälen.
  2. Essen.
`; const r = extractRecipeFromHtml(html); expect(r).not.toBeNull(); expect(r!.steps.length).toBe(2); expect(r!.steps[0].text).toBe('Schälen.'); }); it('splits a single HowToStep containing "1.
2.
3." into separate steps', () => { const html = `
Multi-step x

1. Teig kneten.
2. Gehen lassen.
3. Backen.

`; const r = extractRecipeFromHtml(html); expect(r).not.toBeNull(); expect(r!.steps.length).toBe(3); expect(r!.steps[0].text).toBe('Teig kneten.'); expect(r!.steps[1].text).toBe('Gehen lassen.'); expect(r!.steps[2].text).toBe('Backen.'); }); it('handles HowToSection wrapping multiple HowToStep itemListElements', () => { const html = `
Sections x
Erst schneiden.
Dann kochen.
`; const r = extractRecipeFromHtml(html); expect(r!.steps.length).toBe(2); expect(r!.steps[0].text).toBe('Erst schneiden.'); expect(r!.steps[1].text).toBe('Dann kochen.'); }); it('ignores alt/title content in step text', () => { const html = `
WithIcon x
Teig Icon Teig kneten verarbeiten.
`; const r = extractRecipeFromHtml(html); expect(r!.steps[0].text).not.toMatch(/Icon Teig kneten/); expect(r!.steps[0].text).toMatch(/Teig.*verarbeiten/); }); it('prefers JSON-LD when both are present', () => { const html = `
From Microdata
`; const r = extractRecipeFromHtml(html); expect(r?.title).toBe('From JSON-LD'); }); }); describe('hasRecipeMarkup', () => { it('detects JSON-LD Recipe', () => { const html = ` `; expect(hasRecipeMarkup(html)).toBe(true); }); it('detects schema.org/Recipe microdata', () => { const html = `
Königsberger Klopse
`; expect(hasRecipeMarkup(html)).toBe(true); }); it('also detects http:// variant of schema.org/Recipe', () => { const html = `
`; expect(hasRecipeMarkup(html)).toBe(true); }); it('returns false for pages without any recipe markup', () => { const html = '

nothing here

'; expect(hasRecipeMarkup(html)).toBe(false); }); });