from cookit_import.recipe import Reason, build_recipe, is_recipe_sheet, recipe_id from cookit_import.workbook import Sheet TEMPLATE = { "C1": "Testrezept", "F1": "Hauptgericht", "G1": "Rezeptindex", "C2": "Menge", "D2": "Maßeinheit", "E2": "Zutat", "F2": "Anleitungen", "G2": "Vorbereitungszeit", "B5": "Kalorien", "G6": "Kochzeit", "B8": "Rezept-Stichwörter", "G10": "Ergebnis (Portionen)", "B11": "Kommentare", "B12": "Löschen Sie dieses Rezeptarbeitsblatt nicht.", } def sheet(title="Testrezept", comments=None, **cells) -> Sheet: return Sheet(title, {**TEMPLATE, **cells}, comments or {}) def lines(recipe): return [[(l["amount"], l["unit"], l["name"]) for l in g["lines"]] for g in recipe["ingredientGroups"]] def test_recognises_recipe_sheets_and_skips_template_ones(): assert is_recipe_sheet(sheet()) assert not is_recipe_sheet(sheet(title="Leeres Rezept")) assert not is_recipe_sheet(sheet(title="Spinat-Feta-Pizza")) assert not is_recipe_sheet(Sheet("Speiseplan", {"C2": "Montag"})) def test_basic_fields(): r = build_recipe(sheet(G3=20, G7="15 Minuten", G11=4, B6=775, B9="Tomaten, Nudeln, tomaten, Käse", F3="Kochen.\n\nEssen.")) assert r["id"] == recipe_id("Testrezept") assert r["name"] == "Testrezept" assert r["course"] == "Hauptgericht" assert r["tags"] == ["Tomaten", "Nudeln", "Käse"] assert (r["prepMinutes"], r["cookMinutes"], r["servings"]) == (20, 15, 4) assert r["calories"] == {"kcal": 775, "basis": "perServing"} assert r["stepSections"] == [{"title": None, "steps": ["Kochen.", "Essen."]}] assert r["notes"] is None assert r["reviewReasons"] == [] def test_ids_are_stable_per_sheet(): assert recipe_id("Focaccia") == recipe_id("Focaccia") != recipe_id("Brot") def test_placeholder_course_and_missing_servings(): r = build_recipe(sheet(F1="Gang")) assert r["course"] is None assert Reason.NO_SERVINGS in r["reviewReasons"] def test_longer_name_wins_and_template_title_is_ignored(): assert build_recipe(sheet(title="Flammkuchen, low carb", C1="Flammkuchen"))["name"] == "Flammkuchen, low carb" assert build_recipe(sheet(title="Kohlrabi-Kartoffel-Auflauf", C1="Leeres Rezept"))["name"] == "Kohlrabi-Kartoffel-Auflauf" def test_blank_rows_separate_ingredient_groups_and_units_are_canonical(): r = build_recipe(sheet(G11=1, C3=300, D3="Gramm", E3="Wasser", C4=1, D4="Teelöffel", E4="Zucker", E6="Rosmarin")) assert lines(r) == [[(300, "gram", "Wasser"), (1, "teaspoon", "Zucker")], [(None, None, "Rosmarin")]] assert [g["name"] for g in r["ingredientGroups"]] == [None, None] def test_rescaling_helper_numbers_are_dropped_silently(): r = build_recipe(sheet(G11=4, C3=350, D3="Gramm", E3="Rigatoni", C33=350, D33=87.5, C34=1, D34=0.25, C10=125)) assert lines(r) == [[(350, "gram", "Rigatoni")]] assert r["reviewReasons"] == [] def test_oder_row_starts_alternative_group(): r = build_recipe(sheet(G11=8, C3=375, D3="Gramm", E3="Mehl", C5="ODER", C7=295, D7="Gramm", E7="Mehl", C8=80, D8="Gramm", E8="Proteinpulver")) assert [g["name"] for g in r["ingredientGroups"]] == [None, "Alternative"] assert Reason.ALTERNATIVE in r["reviewReasons"] def test_duplicate_ingredient_list_is_dropped(): first = dict(C3=100, D3="Gramm", E3="Mehl", C4=3, E4="Eier", C5=125, D5="Milliliter", E5="Milch") copy = dict(D20="100 g", E20="Mehl", D21="3", E21="Eier", D22="125 ml", E22="Milch") r = build_recipe(sheet(G11=2, **first, **copy)) assert len(r["ingredientGroups"]) == 1 assert Reason.DUPLICATE_LIST in r["reviewReasons"] def test_amount_typed_into_unit_column(): r = build_recipe(sheet(G11=2, D3="100 g", E3="Mehl", D4="1 TL", E4="Zucker")) assert lines(r) == [[(100, "gram", "Mehl"), (1, "teaspoon", "Zucker")]] def test_ranges_fractions_and_unknown_amounts(): r = build_recipe(sheet(G11=2, C3="2-3", D3="Esslöffel", E3="Kakao", C4="½", D4="Bund", E4="Schnittlauch", C5="opt. als Topping", E5="Sesam")) first, second, third = r["ingredientGroups"][0]["lines"] assert (first["amount"], first["amountMax"]) == (2, 3) assert second["amount"] == 0.5 assert (third["amount"], third["remark"]) == (None, "opt. als Topping") assert Reason.UNKNOWN_AMOUNT.format(amount="opt. als Topping") in r["reviewReasons"] def test_parenthesised_line_is_optional(): r = build_recipe(sheet(G11=4, C3="(2", D3="Teelöffel", E3="Oregano)")) line = r["ingredientGroups"][0]["lines"][0] assert (line["amount"], line["unit"], line["name"], line["remark"]) == (2, "teaspoon", "Oregano", "optional") def test_unknown_unit_moves_into_name(): r = build_recipe(sheet(G11=2, C3=2, D3="Würfel", E3="Brühe")) assert lines(r) == [[(2, None, "Würfel Brühe")]] assert Reason.UNKNOWN_UNIT.format(unit="Würfel") in r["reviewReasons"] def test_comment_group_markers_name_groups_and_other_comments_become_remarks(): comments = {"C3": "Zutaten für die Rouladen", "C5": "Ab hier: \nZutaten für die Sauce", "E4": "kleiner Espresso-Löffel"} r = build_recipe(sheet(G11=6, comments=comments, C3=6, E3="Rinderrouladen", C4=1, D4="Teelöffel", E4="Senf", C5=50, D5="Gramm", E5="Butter")) assert [g["name"] for g in r["ingredientGroups"]] == ["Rouladen", "Sauce"] assert r["ingredientGroups"][0]["lines"][1]["remark"] == "kleiner Espresso-Löffel" def test_pro_portion_comment_settles_calories_basis(): r = build_recipe(sheet(G11=4, B6=662, comments={"B6": "Pro Portion"})) assert r["calories"] == {"kcal": 662, "basis": "perServing"} assert r["reviewReasons"] == [] def test_high_calories_mean_whole_recipe(): r = build_recipe(sheet(G11=1, B6=3831)) assert r["calories"]["basis"] == "wholeRecipe" def test_comment_b12_and_stray_cells_go_to_notes_and_urls_to_sources(): r = build_recipe( sheet( G11=1, B12="Springform 26 cm", I12="ca.", J12="15", K12="Dampfnudeln", B17="https://sallys-blog.de/rezepte/focaccia", ) ) assert r["notes"] == "Springform 26 cm\n\nca. 15 Dampfnudeln" assert r["sources"] == ["https://sallys-blog.de/rezepte/focaccia"] assert Reason.STRAY_TEXT in r["reviewReasons"] def test_template_comment_is_not_a_note(): r = build_recipe(sheet(G11=1)) assert r["notes"] is None assert r["reviewReasons"] == [] def test_nutrition_rows_in_ingredient_columns_become_notes(): r = build_recipe(sheet(G11=1, C3=3, E3="Eier", D18="Kalorien/ 100g", E18="263.47", D19="Fett", E19="17.92")) assert lines(r) == [[(3, None, "Eier")]] assert r["notes"] == "Kalorien/ 100g 263.47\nFett 17.92" def test_instructions_split_over_cells_are_merged_and_flagged(): r = build_recipe(sheet(G11=6, F3="Teig rühren.", F19="Spätzle abschrecken.")) assert r["stepSections"][0]["steps"] == ["Teig rühren.", "Spätzle abschrecken."] assert Reason.MERGED_INSTRUCTIONS in r["reviewReasons"] def test_long_step_is_flagged(): r = build_recipe(sheet(G11=1, F3="Rühren. " * 100)) assert Reason.LONG_STEP in r["reviewReasons"] def test_unreadable_time_goes_to_notes(): r = build_recipe(sheet(G11=1, G3="über Nacht")) assert r["prepMinutes"] is None assert r["notes"] == "Vorbereitungszeit: über Nacht" assert Reason.UNREADABLE_TIME in r["reviewReasons"]