import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:melo/shared/theme.dart'; /// Relative Helligkeit nach WCAG 2.1, 1.4.3. double _luminanz(Color farbe) { double kanal(double wert) => wert <= 0.03928 ? wert / 12.92 : math.pow((wert + 0.055) / 1.055, 2.4) as double; return 0.2126 * kanal(farbe.r) + 0.7152 * kanal(farbe.g) + 0.0722 * kanal(farbe.b); } /// Legt [vordergrund] (ggf. halbdurchsichtig) auf [hintergrund]. Color _ueber(Color vordergrund, Color hintergrund) { final a = vordergrund.a; return Color.from( alpha: 1, red: vordergrund.r * a + hintergrund.r * (1 - a), green: vordergrund.g * a + hintergrund.g * (1 - a), blue: vordergrund.b * a + hintergrund.b * (1 - a), ); } /// Kontrastverhältnis nach WCAG 2.1 — 1 (unlesbar) bis 21 (Schwarz auf Weiß). double kontrast(Color vordergrund, Color hintergrund) { final a = _luminanz(_ueber(vordergrund, hintergrund)); final b = _luminanz(hintergrund); final hell = math.max(a, b); final dunkel = math.min(a, b); return (hell + 0.05) / (dunkel + 0.05); } void main() { // Die hellste Fläche ist der ungünstigste Fall: helle Schrift hat dort // den geringsten Abstand zum Untergrund. Wer hier besteht, besteht überall. const flaechen = { 'Hintergrund': MeloTheme.black, 'Fläche': MeloTheme.surface, 'Karte': MeloTheme.surfaceHigh, }; group('Textfarben erfüllen WCAG 4,5:1 auf jeder Fläche', () { const textfarben = { 'text1': MeloTheme.text1, 'text2': MeloTheme.text2, 'text3': MeloTheme.text3, }; for (final flaeche in flaechen.entries) { for (final text in textfarben.entries) { test('${text.key} auf ${flaeche.key}', () { final wert = kontrast(text.value, flaeche.value); expect(wert, greaterThanOrEqualTo(4.5), reason: '${text.key} auf ${flaeche.key} nur ' '${wert.toStringAsFixed(2)}:1'); }); } } }); test('die Rot-Markierung ist auf dem Hintergrund erkennbar', () { // 3:1 genügt: Rot trägt keinen Fließtext, es markiert (aktiver Reiter, // Fortschritt). Für Text wäre 4,5:1 nötig — deshalb steht auf Rot Weiß. expect(kontrast(MeloTheme.red, MeloTheme.black), greaterThanOrEqualTo(3.0)); }); test('Colors.white38 wäre zu blass — genau deshalb gibt es text3', () { // Festhalten, warum die alte Schreibweise ersetzt wurde: sie verfehlt // die Grenze. Fällt dieser Test, ist die Rechnung kaputt, nicht das Theme. expect(kontrast(Colors.white38, MeloTheme.surfaceHigh), lessThan(4.5)); expect(kontrast(MeloTheme.text3, MeloTheme.surfaceHigh), greaterThanOrEqualTo(4.5)); }); test('die Rechnung selbst stimmt: Schwarz auf Weiß ergibt 21:1', () { expect(kontrast(const Color(0xFF000000), const Color(0xFFFFFFFF)), closeTo(21.0, 0.01)); }); group('Abstände rasten auf 8 (4 nur für Feinheiten)', () { test('alle Stufen sind Vielfache von 4', () { for (final wert in [ MeloSpace.xs, MeloSpace.sm, MeloSpace.md, MeloSpace.lg, MeloSpace.xl, ]) { expect(wert % 4, 0, reason: '$wert passt nicht ins Raster'); } }); test('ab der Standardstufe sind es Vielfache von 8', () { for (final wert in [MeloSpace.sm, MeloSpace.md, MeloSpace.lg, MeloSpace.xl]) { expect(wert % 8, 0, reason: '$wert passt nicht ins 8er-Raster'); } }); test('die Stufen steigen streng', () { const stufen = [ MeloSpace.xs, MeloSpace.sm, MeloSpace.md, MeloSpace.lg, MeloSpace.xl, ]; for (var i = 1; i < stufen.length; i++) { expect(stufen[i], greaterThan(stufen[i - 1])); } }); }); group('Typo-Skala', () { final skala = MeloTheme.dark.textTheme; test('kein Text ist kleiner als 12 — darunter wird es unlesbar', () { for (final stil in [ skala.displaySmall, skala.headlineSmall, skala.titleMedium, skala.bodyLarge, skala.bodyMedium, skala.bodySmall, skala.labelLarge, ]) { expect(stil!.fontSize, greaterThanOrEqualTo(12)); } }); test('Fließtext hat mindestens 1,35 Zeilenhöhe', () { expect(skala.bodyLarge!.height, greaterThanOrEqualTo(1.35)); expect(skala.bodyMedium!.height, greaterThanOrEqualTo(1.35)); }); test('die Größen nehmen von Überschrift zu Beiwerk ab', () { final groessen = [ skala.displaySmall!.fontSize!, skala.headlineSmall!.fontSize!, skala.titleMedium!.fontSize!, skala.bodyLarge!.fontSize!, skala.bodyMedium!.fontSize!, ]; for (var i = 1; i < groessen.length; i++) { expect(groessen[i], lessThan(groessen[i - 1])); } }); }); }