utils/design_system/test/theme_test.dart
Krzysztof kuhy Rudnicki e6c9af81e9 Add design_system: the shared Flutter component layer
unified-design-system/ froze the token layer as prose -- 1220 lines of
README, tokens.md and annotated HTML, and zero importable code. Every
Flutter repo transcribed the token table by hand, so the "shared" palette
was enforced only by discipline, and had already drifted: 10 divergent
theme.dart copies (todo and untools byte-identical, the other 8 each
unique), ~318 raw color literals, 4 hand-rolled AppStatusColors extensions
with inconsistent field sets, ~35 inlined AlertDialogs, ~38 raw SnackBar
sites with no helper anywhere.

This promotes the token layer into an importable component layer, so the
design system is enforced by the compiler and the package manager instead.
The components own the tokens: a consumer has nothing local left to edit.

Scales are the six-repo consensus, which is also what tokens.md specifies;
drifted repos get corrected to this rather than the reverse. Themes are
built from explicit ColorSchemes, never fromSeed -- the palette is
hand-picked for contrast, and secondary/tertiary are set explicitly so
SegmentedButton cannot fall back to stock Material teal.

Widgets are extracted only where two or more repos already had a
structurally similar implementation; SectionHeader reconciles untools'
trailing-action variant with billsplit's padded one as an opt-in union, so
either caller can adopt it with no visual change.

63 tests, 102/102 lines covered, gated at 100% on pre-push.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XS1m7b1piBtAL22USLW6jL
2026-08-14 22:51:06 +02:00

125 lines
4.5 KiB
Dart

import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('buildDarkTheme', () {
final theme = buildDarkTheme();
test('uses the frozen dark palette', () {
final scheme = theme.colorScheme;
expect(scheme.brightness, Brightness.dark);
expect(scheme.surface, AppPalette.ink);
expect(scheme.surfaceContainerHigh, AppPalette.inkRaised1);
expect(scheme.surfaceContainerHighest, AppPalette.inkRaised2);
expect(scheme.onSurface, AppPalette.textOnDark);
expect(scheme.onSurfaceVariant, AppPalette.mutedOnDark);
expect(scheme.outline, AppPalette.lineDark);
expect(scheme.error, AppPalette.danger);
});
test('scaffold background matches the surface', () {
expect(theme.scaffoldBackgroundColor, theme.colorScheme.surface);
});
});
group('buildLightTheme', () {
final theme = buildLightTheme();
test('uses the frozen light palette', () {
final scheme = theme.colorScheme;
expect(scheme.brightness, Brightness.light);
expect(scheme.surface, AppPalette.paper);
expect(scheme.surfaceContainerHighest, AppPalette.paperRaised);
expect(scheme.onSurface, AppPalette.textOnLight);
expect(scheme.onSurfaceVariant, AppPalette.mutedOnLight);
expect(scheme.outline, AppPalette.lineLight);
expect(scheme.error, AppPalette.danger);
});
test('scaffold background matches the surface', () {
expect(theme.scaffoldBackgroundColor, theme.colorScheme.surface);
});
});
for (final entry in {
'dark': buildDarkTheme(),
'light': buildLightTheme(),
}.entries) {
group('${entry.key} theme', () {
final theme = entry.value;
test('is Material 3', () {
expect(theme.useMaterial3, isTrue);
});
test('has one accent: secondary and tertiary mirror primary', () {
final scheme = theme.colorScheme;
expect(scheme.primary, AppPalette.accent);
expect(scheme.secondary, AppPalette.accent);
expect(scheme.tertiary, AppPalette.accent);
});
test('draws dark text on every filled surface', () {
final scheme = theme.colorScheme;
expect(scheme.onPrimary, AppPalette.onFill);
expect(scheme.onSecondary, AppPalette.onFill);
expect(scheme.onTertiary, AppPalette.onFill);
expect(scheme.onError, AppPalette.onFill);
});
test('never leaks stock Material teal into container slots', () {
final scheme = theme.colorScheme;
expect(scheme.onSecondaryContainer, AppPalette.accent);
expect(scheme.onTertiaryContainer, AppPalette.accent);
expect(scheme.secondaryContainer, scheme.outline);
expect(scheme.tertiaryContainer, scheme.outline);
});
test('registers the status colors as a theme extension', () {
final status = theme.extension<AppStatusColors>();
expect(status, isNotNull);
expect(status!.success, AppPalette.success);
});
test('sizes text from the shared type scale', () {
final text = theme.textTheme;
expect(text.bodyLarge?.fontSize, AppTextSize.body);
expect(text.bodyMedium?.fontSize, AppTextSize.body);
expect(text.titleLarge?.fontSize, AppTextSize.title);
expect(text.titleMedium?.fontSize, AppTextSize.subtitle);
expect(text.labelMedium?.fontSize, AppTextSize.label);
expect(text.labelSmall?.fontSize, AppTextSize.caption);
});
test('inputs are filled, rounded, and show a 2px accent focus ring', () {
final input = theme.inputDecorationTheme;
expect(input.filled, isTrue);
expect(input.fillColor, theme.colorScheme.surfaceContainerHighest);
final focused = input.focusedBorder! as OutlineInputBorder;
expect(focused.borderSide.color, AppPalette.accent);
expect(focused.borderSide.width, 2.0);
expect(
focused.borderRadius,
BorderRadius.circular(AppRadius.sm),
);
for (final border in [input.border!, input.enabledBorder!]) {
expect(
(border as OutlineInputBorder).borderSide.color,
theme.colorScheme.outline,
);
}
expect(input.labelStyle?.color, theme.colorScheme.onSurfaceVariant);
});
test('dividers use the outline color at 1px', () {
expect(theme.dividerTheme.color, theme.colorScheme.outline);
expect(theme.dividerTheme.thickness, 1.0);
expect(theme.dividerTheme.space, AppSpacing.md);
});
});
}
}