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
- 500 g Hackfleisch gemischt
- 1 Zwiebel, fein gewürfelt
- 2 EL Kapern
-
Hackfleisch und Zwiebel vermengen.
-
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 = `
`;
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
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);
});
});