the orphaned-cache-file sweep ran a synchronous recursive listSync on the UI isolate at startup against a cache that can hold 10k files; use the async listing instead. memoize adjustChatNameColor keyed by (color, background, target contrast) since chat repeats the same handful of palette colors thousands of times and low-contrast ones pay a 16-step search per call. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
378 lines
12 KiB
Dart
378 lines
12 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('memoized results are stable and keyed by background',
|
|
(tester) async {
|
|
late Color first;
|
|
late Color second;
|
|
late Color onLightBackground;
|
|
const darkBackground = Color(0xFF121212);
|
|
const lowContrastColor = Color(0xFF1F1F1F); // Near-black on dark bg
|
|
|
|
await tester.pumpWidget(
|
|
buildTestWidget(
|
|
scaffoldBackground: darkBackground,
|
|
child: Builder(
|
|
builder: (context) {
|
|
// Repeated calls with identical inputs must return the same
|
|
// adjusted color (second call is served from the memo cache).
|
|
first = adjustChatNameColor(context, lowContrastColor);
|
|
second = adjustChatNameColor(context, lowContrastColor);
|
|
// A different background must not be served the dark-background
|
|
// cache entry: near-black already passes on white, so it should
|
|
// come back unchanged.
|
|
onLightBackground = adjustChatNameColor(
|
|
context,
|
|
lowContrastColor,
|
|
background: const Color(0xFFFFFFFF),
|
|
);
|
|
return const SizedBox();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(second, first);
|
|
expect(
|
|
first.computeLuminance(),
|
|
greaterThan(lowContrastColor.computeLuminance()),
|
|
);
|
|
expect(onLightBackground, lowContrastColor);
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|
|
}
|