groups)->toBeEmpty() ->and(IncoIngredientParser::parse('')->groups)->toBeEmpty() ->and(IncoIngredientParser::parse(' ')->groups)->toBeEmpty(); }); it('parses a simple flat list of ingredients', function (): void { $raw = 'Viande de poulet 51%, eau, sel, farine de blé, huile de tournesol.'; $vo = IncoIngredientParser::parse($raw); expect($vo)->toBeInstanceOf(Ingredients::class) ->and($vo->groups)->not->toBeEmpty(); }); it('parses structured ingredients with sub-ingredients in parentheses and ratios', function (): void { $raw = 'Viande de poulet traitée en salaison : 70% (viande de poulet : 51%, eau, sel). Panure : 30% (farine de blé, eau, sel, levure).'; $vo = IncoIngredientParser::parse($raw); expect($vo->groups)->toBeArray(); $keys = array_keys($vo->groups); expect($keys)->toContain('Viande de poulet traitée en salaison') ->and($keys)->toContain('Panure'); }); it('handles Traces groups with uppercase normalization', function (): void { $raw = 'Viande de dinde (80%), sel. Traces : gluten, lait, œuf.'; $vo = IncoIngredientParser::parse($raw); expect($vo->groups)->toHaveKey('Traces'); $traces = $vo->groups['Traces']; expect($traces)->toBeArray() ->and($traces[0])->toBe(mb_strtoupper($traces[0], 'UTF-8')); }); it('replaces Informations en GRAS with Informations en MAJUSCULE', function (): void { $raw = 'Farine de blé. Informations en GRAS pour les allergènes.'; $vo = IncoIngredientParser::parse($raw); $groups = $vo->groups; $allValues = []; array_walk_recursive($groups, static function (string $val) use (&$allValues): void { $allValues[] = $val; }); $found = false; foreach ($allValues as $val) { if (str_contains($val, 'Informations en MAJUSCULE')) { $found = true; break; } } expect($found)->toBeTrue(); }); });