frosty/test/utils_test.dart
2026-02-07 00:51:22 -08:00

339 lines
11 KiB
Dart

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:frosty/utils.dart';
void main() {
group('getReadableName', () {
test('returns display name when it equals username', () {
expect(getReadableName('testuser', 'testuser'), 'testuser');
});
test('returns display name when it contains English letters (even if case differs)', () {
// Display name contains English letters, so it's readable as-is
// The function doesn't add username because English letters make it readable
expect(getReadableName('TestUser', 'testuser'), 'TestUser');
});
test('returns display name only for numeric-only names', () {
expect(getReadableName('12345', 'somename'), '12345');
});
test('returns display name only when it contains English letters', () {
expect(getReadableName('TestName123', 'testname123'), 'TestName123');
expect(getReadableName('User_Name', 'user_name'), 'User_Name');
});
test('adds username in parentheses for non-English display names', () {
// Japanese characters
expect(getReadableName('テスト', 'testuser'), 'テスト (testuser)');
// Korean characters
expect(getReadableName('테스트', 'testuser'), '테스트 (testuser)');
// Chinese characters
expect(getReadableName('测试', 'testuser'), '测试 (testuser)');
// Russian characters
expect(getReadableName('Тест', 'testuser'), 'Тест (testuser)');
});
test('adds username for mixed non-English with numbers', () {
// Non-English + numbers (no English letters)
expect(getReadableName('テスト123', 'testuser'), 'テスト123 (testuser)');
});
test('returns display name for mixed English and non-English', () {
// Has English letters, so no parentheses
expect(getReadableName('TestテストUser', 'testuser'), 'TestテストUser');
});
});
group('adjustChatNameColor', () {
// Helper to create a testable widget tree with specific theme
Widget buildTestWidget({
required Color scaffoldBackground,
required Widget child,
Brightness brightness = Brightness.dark,
}) {
return MaterialApp(
theme: ThemeData(
brightness: brightness,
scaffoldBackgroundColor: scaffoldBackground,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: brightness,
surface: scaffoldBackground,
),
),
home: Scaffold(body: child),
);
}
testWidgets('brightens dark colors on dark backgrounds', (tester) async {
late Color adjustedColor;
const darkBackground = Color(0xFF121212);
const darkNameColor = Color(0xFF000000); // Pure black
await tester.pumpWidget(
buildTestWidget(
scaffoldBackground: darkBackground,
child: Builder(
builder: (context) {
adjustedColor = adjustChatNameColor(context, darkNameColor);
return const SizedBox();
},
),
),
);
// The adjusted color should be lighter than the original
final originalLuminance = darkNameColor.computeLuminance();
final adjustedLuminance = adjustedColor.computeLuminance();
expect(adjustedLuminance, greaterThan(originalLuminance));
});
testWidgets('darkens light colors on light backgrounds', (tester) async {
late Color adjustedColor;
const lightBackground = Color(0xFFFAFAFA);
const lightNameColor = Color(0xFFFFFFFF); // Pure white
await tester.pumpWidget(
buildTestWidget(
scaffoldBackground: lightBackground,
brightness: Brightness.light,
child: Builder(
builder: (context) {
adjustedColor = adjustChatNameColor(context, lightNameColor);
return const SizedBox();
},
),
),
);
// The adjusted color should be darker than the original
final originalLuminance = lightNameColor.computeLuminance();
final adjustedLuminance = adjustedColor.computeLuminance();
expect(adjustedLuminance, lessThan(originalLuminance));
});
testWidgets('preserves colors that already meet contrast ratio',
(tester) async {
late Color adjustedColor;
const darkBackground = Color(0xFF121212);
const goodContrastColor = Color(0xFFFFFFFF); // White on dark = good contrast
await tester.pumpWidget(
buildTestWidget(
scaffoldBackground: darkBackground,
child: Builder(
builder: (context) {
adjustedColor = adjustChatNameColor(context, goodContrastColor);
return const SizedBox();
},
),
),
);
// Color should be unchanged or very close to original
expect(adjustedColor, goodContrastColor);
});
testWidgets('uses custom target contrast ratio', (tester) async {
late Color adjusted45;
late Color adjusted30;
const darkBackground = Color(0xFF121212);
const greyColor = Color(0xFF555555);
await tester.pumpWidget(
buildTestWidget(
scaffoldBackground: darkBackground,
child: Builder(
builder: (context) {
adjusted45 = adjustChatNameColor(
context,
greyColor,
);
adjusted30 = adjustChatNameColor(
context,
greyColor,
targetContrast: 3.0,
);
return const SizedBox();
},
),
),
);
// Higher contrast requirement should result in a lighter color
final lum45 = adjusted45.computeLuminance();
final lum30 = adjusted30.computeLuminance();
expect(lum45, greaterThanOrEqualTo(lum30));
});
testWidgets('uses custom background color parameter', (tester) async {
late Color adjustedColor;
const customBackground = Color(0xFF00FF00); // Green background
await tester.pumpWidget(
buildTestWidget(
scaffoldBackground: Colors.black,
child: Builder(
builder: (context) {
// Pass custom background, ignoring theme
adjustedColor = adjustChatNameColor(
context,
const Color(0xFF00FF00), // Same as background
background: customBackground,
);
return const SizedBox();
},
),
),
);
// Color should be adjusted for contrast against green, not black
// Green on green needs adjustment
expect(adjustedColor, isNot(const Color(0xFF00FF00)));
});
testWidgets('handles transparent scaffold background', (tester) async {
late Color adjustedColor;
const darkColor = Color(0xFF000000);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: Colors.transparent,
colorScheme: const ColorScheme.dark(
surface: Color(0xFF1E1E1E),
),
),
home: Scaffold(
body: Builder(
builder: (context) {
adjustedColor = adjustChatNameColor(context, darkColor);
return const SizedBox();
},
),
),
),
);
// Should use colorScheme.surface as fallback
// Dark color on dark surface should be adjusted
expect(adjustedColor.computeLuminance(), greaterThan(0.0));
});
testWidgets('maintains hue while adjusting lightness', (tester) async {
late Color adjustedColor;
const darkBackground = Color(0xFF121212);
const blueColor = Color(0xFF0000AA); // Dark blue
await tester.pumpWidget(
buildTestWidget(
scaffoldBackground: darkBackground,
child: Builder(
builder: (context) {
adjustedColor = adjustChatNameColor(context, blueColor);
return const SizedBox();
},
),
),
);
// Should still be recognizably blue (hue preserved)
final originalHsl = HSLColor.fromColor(blueColor);
final adjustedHsl = HSLColor.fromColor(adjustedColor);
// Hue should be the same or very close
expect(adjustedHsl.hue, closeTo(originalHsl.hue, 1.0));
});
});
group('Color contrast calculation (indirect tests)', () {
// These tests verify the contrast behavior indirectly through adjustChatNameColor
testWidgets('common Twitch colors work on dark background', (tester) async {
final twitchColors = [
const Color(0xFFFF0000), // Red
const Color(0xFF0000FF), // Blue
const Color(0xFF00FF00), // Green
const Color(0xFFB22222), // Firebrick
const Color(0xFFFF69B4), // Hot pink
const Color(0xFFDAA520), // Golden rod
];
for (final color in twitchColors) {
late Color adjusted;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: const Color(0xFF121212),
),
home: Scaffold(
body: Builder(
builder: (context) {
adjusted = adjustChatNameColor(context, color);
return const SizedBox();
},
),
),
),
);
// All adjusted colors should have reasonable luminance
expect(
adjusted.computeLuminance(),
greaterThan(0.05),
reason: 'Color $color should be readable',
);
}
});
testWidgets('adjusted color meets WCAG AA 4.5:1 contrast ratio',
(tester) async {
late Color adjustedColor;
const darkBackground = Color(0xFF121212);
const darkBlue = Color(0xFF0000AA);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBackground,
),
home: Scaffold(
body: Builder(
builder: (context) {
adjustedColor = adjustChatNameColor(context, darkBlue);
return const SizedBox();
},
),
),
),
);
// Compute WCAG contrast ratio manually
double linearize(double c) => c <= 0.03928
? c / 12.92
: math.pow((c + 0.055) / 1.055, 2.4).toDouble();
double luminance(Color c) {
return 0.2126 * linearize(c.r) +
0.7152 * linearize(c.g) +
0.0722 * linearize(c.b);
}
final l1 = luminance(adjustedColor);
final l2 = luminance(darkBackground);
final hi = l1 > l2 ? l1 : l2;
final lo = l1 > l2 ? l2 : l1;
final ratio = (hi + 0.05) / (lo + 0.05);
expect(ratio, greaterThanOrEqualTo(4.5),
reason: 'Adjusted color should meet WCAG AA 4.5:1 contrast ratio');
});
});
}