"""Tests der Markenfarben-Ermittlung.""" import io import pytest from PIL import Image from app.services.colors import ( DARK_BACKGROUND, MIN_CONTRAST, brand_colors, color_from_raster, color_from_svg, contrast_ratio, deterministic_color, hex_to_rgb, is_grayscale, lighten_for_dark_background, normalize_hex, ) def png(pixels: list[tuple[int, int, int, int]], size: tuple[int, int] = (8, 8)) -> bytes: """Baut ein kleines PNG aus einer Pixelliste.""" image = Image.new("RGBA", size) image.putdata([pixels[index % len(pixels)] for index in range(size[0] * size[1])]) buffer = io.BytesIO() image.save(buffer, format="PNG") return buffer.getvalue() # --- Umrechnungen -------------------------------------------------------------- @pytest.mark.parametrize( ("eingabe", "erwartet"), [ ("#E50914", "#e50914"), ("E50914", "#e50914"), ("#f00", "#ff0000"), ("#E50914FF", "#e50914"), ("kein-hex", None), ("#12345", None), ], ) def test_hexwerte_werden_normalisiert(eingabe: str, erwartet: str | None) -> None: assert normalize_hex(eingabe) == erwartet def test_kontrastverhaeltnis_nach_wcag() -> None: # Schwarz auf Weiß ist das Maximum von 21:1. assert round(contrast_ratio("#000000", "#ffffff"), 1) == 21.0 assert round(contrast_ratio("#ffffff", "#ffffff"), 1) == 1.0 def test_graustufen_werden_erkannt() -> None: assert is_grayscale(hex_to_rgb("#808080")) is True assert is_grayscale(hex_to_rgb("#ffffff")) is True assert is_grayscale(hex_to_rgb("#e50914")) is False # --- Aufhellung ---------------------------------------------------------------- @pytest.mark.parametrize( "farbe", ["#e50914", "#1d4ed8", "#166534", "#7c2d12", "#0058a3", "#e20074", "#312e81"], ) def test_aufgehellte_farbe_erreicht_den_mindestkontrast(farbe: str) -> None: aufgehellt = lighten_for_dark_background(farbe) assert contrast_ratio(aufgehellt, DARK_BACKGROUND) >= MIN_CONTRAST def test_bereits_helle_farbe_bleibt_unveraendert() -> None: # Ein helles Grün erfüllt den Kontrast bereits. assert lighten_for_dark_background("#1ed760") == "#1ed760" def test_farbpaar_faellt_auf_den_namenshash_zurueck() -> None: ohne = brand_colors(None, fallback_name="Knuspertronic") nochmal = brand_colors(None, fallback_name="Knuspertronic") assert ohne.color == nochmal.color # deterministisch assert ohne.color == deterministic_color("Knuspertronic") assert contrast_ratio(ohne.color_dark, DARK_BACKGROUND) >= MIN_CONTRAST def test_namensfarbe_ignoriert_gross_schreibung_und_leerzeichen() -> None: assert deterministic_color(" Netflix ") == deterministic_color("netflix") # --- SVG ----------------------------------------------------------------------- def test_svg_haeufigste_nicht_graustufe_gewinnt() -> None: svg = ( b'' b'' ) assert color_from_svg(svg) == "#e50914" def test_svg_versteht_style_und_rgb_schreibweise() -> None: svg = b'' assert color_from_svg(svg) == "#1db954" def test_svg_beruecksichtigt_verlaufsstopps() -> None: svg = b'' assert color_from_svg(svg) == "#833ab4" def test_svg_ohne_farbe_liefert_nichts() -> None: assert color_from_svg(b'') is None assert color_from_svg(b'') is None # --- Rastergrafiken ------------------------------------------------------------ def test_raster_findet_die_dominante_farbe() -> None: # Überwiegend Rot mit weißem Rand. bild = png([(229, 9, 20, 255)] * 12 + [(255, 255, 255, 255)] * 4) ergebnis = color_from_raster(bild) assert ergebnis is not None rot, gruen, blau = hex_to_rgb(ergebnis) assert rot > 200 and gruen < 60 and blau < 60 def test_raster_ignoriert_durchsichtige_pixel() -> None: """Ein grüner Bildinhalt auf vollständig durchsichtigem Blau bleibt grün.""" bild = png([(30, 215, 96, 255)] * 8 + [(0, 0, 255, 0)] * 8) ergebnis = color_from_raster(bild) assert ergebnis is not None _, gruen, blau = hex_to_rgb(ergebnis) assert gruen > 150 and blau < 150 def test_reines_graustufenbild_liefert_keine_markenfarbe() -> None: bild = png([(20, 20, 20, 255)] * 8 + [(240, 240, 240, 255)] * 8) assert color_from_raster(bild) is None def test_vollstaendig_durchsichtiges_bild_liefert_nichts() -> None: assert color_from_raster(png([(255, 0, 0, 0)])) is None def test_kaputte_datei_wirft_nicht() -> None: assert color_from_raster(b"kein bild") is None