import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/app/app_controller.dart'; import 'package:jj_mkvmaker/app/settings.dart'; import 'package:jj_mkvmaker/main.dart'; import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart'; import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart'; import 'package:jj_mkvmaker/services/platform_services.dart'; import 'package:jj_mkvmaker/ui/theme.dart'; /// X-plore 비교 5-1 ⑦ 밝은 테마 (관리자 지시 10/21): 처음 값 어둡게 (예전 그대로) · 밝게 · 기기 따라 · 바꾸면 상태는 그대로 다시 그림 double _lum(Color c) { double ch(double v) => v <= 0.03928 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4).toDouble(); return 0.2126 * ch(c.r) + 0.7152 * ch(c.g) + 0.0722 * ch(c.b); } /// WCAG 대비 double contrast(Color a, Color b) { final x = _lum(a), y = _lum(b); return (max(x, y) + 0.05) / (min(x, y) + 0.05); } class _Probe extends StatefulWidget { const _Probe(); @override State<_Probe> createState() => _ProbeState(); } class _ProbeState extends State<_Probe> { int taps = 0; @override Widget build(BuildContext context) => GestureDetector( onTap: () => setState(() => taps++), child: Text('probe $taps', style: TextStyle(color: JjColors.text)), ); } void main() { tearDown(() => JjColors.use(Brightness.dark)); test('처음 값은 어둡게 (예전 색 그대로) · 모르는 값 · 없는 값도 어둡게 · 고른 값은 그대로', () { expect(AppSettings().display.brightness, 'dark'); expect(AppSettings.fromJson({}).display.brightness, 'dark'); expect(AppSettings.fromJson({'uiBrightness': 'neon'}).display.brightness, 'dark'); expect(AppSettings.fromJson({'uiBrightness': 'light'}).display.brightness, 'light'); expect(AppSettings.fromJson(AppSettings.fromJson({'uiBrightness': 'system'}).toJson()).display.brightness, 'system'); expect(JjColors.bg, const Color(0xFF141417)); expect(JjColors.accent, const Color(0xFF00C4CC)); expect(JjColors.textDim, const Color(0xFF8E8E99)); // 예전에 쓰던 Material 색 그대로 (어두운 화면이 바뀌지 않게) expect([JjColors.warning, JjColors.alert, JjColors.amber, JjColors.amberText, JjColors.positive], [Colors.orangeAccent, Colors.redAccent, Colors.amber, Colors.amber, Colors.greenAccent].map((c) => Color(c.toARGB32())).toList()); expect(resolveBrightness('dark', Brightness.light), Brightness.dark); expect(resolveBrightness('light', Brightness.dark), Brightness.light); expect(resolveBrightness('system', Brightness.light), Brightness.light); expect(resolveBrightness('?', Brightness.light), Brightness.dark); }); test('밝은 색: 바탕 · 칸 위 글자 대비 4.5:1 이상 (글 · 흐린 글 · 강조 · 경고 · 성공 · 강조 위 글자)', () { const l = JjPalette.light; for (final bg in [l.bg, l.panel, l.panelHigh]) { for (final (name, fg) in [('text', l.text), ('textDim', l.textDim), ('accent', l.accent), ('danger', l.danger), ('success', l.success), ('warning', l.warning), ('alert', l.alert), ('amberText', l.amberText), ('positive', l.positive)]) { expect(contrast(fg, bg), greaterThanOrEqualTo(4.5), reason: '$name on ${bg.toARGB32().toRadixString(16)}'); } } expect(contrast(l.onAccent, l.accent), greaterThanOrEqualTo(4.5)); // 아이콘 (폴더 · 별) 은 3:1 for (final bg in [l.bg, l.panel, l.panelHigh]) { expect(contrast(l.amber, bg), greaterThanOrEqualTo(3)); } expect(buildTheme(Brightness.light).brightness, Brightness.light); expect(buildTheme(Brightness.light).scaffoldBackgroundColor, l.bg); expect(buildTheme(Brightness.dark).scaffoldBackgroundColor, JjPalette.dark.bg); }); testWidgets('색을 바꾸면 JjColors 를 직접 읽는 곳도 다시 그림 · 상태는 그대로', (t) async { await t.pumpWidget(const MaterialApp(home: Scaffold(body: _Probe()))); await t.tap(find.text('probe 0')); await t.pump(); expect(t.widget(find.text('probe 1')).style!.color, JjPalette.dark.text); expect(JjColors.use(Brightness.light), isTrue); expect(JjColors.use(Brightness.light), isFalse, reason: '같으면 바꾸지 않음'); rebuildAllElements(t.binding.rootElement!); await t.pump(); expect(t.widget(find.text('probe 1')).style!.color, JjPalette.light.text, reason: '눌러 둔 수 (상태) 는 그대로'); }); testWidgets('앱: 설정 "화면 밝기" 를 밝게 → 바로 밝은 테마 · 기기 따라는 기기 밝기를 따라 바뀜 · 어둡게로 돌아옴', (t) async { t.view.physicalSize = const Size(1400, 900); t.view.devicePixelRatio = 1; addTearDown(t.view.reset); addTearDown(t.platformDispatcher.clearPlatformBrightnessTestValue); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); await t.runAsync(c.init); await t.pumpWidget(JjMkvMakerApp(controller: c)); await t.pump(); BuildContext ctx() => t.element(find.byType(Scaffold).first); expect(Theme.of(ctx()).brightness, Brightness.dark); expect(JjColors.isLight, isFalse); await t.runAsync(() => c.updateSettings((x) => x.display.brightness = 'light')); await t.pump(); await t.pump(); expect(JjColors.isLight, isTrue); expect(Theme.of(ctx()).brightness, Brightness.light); expect(Theme.of(ctx()).scaffoldBackgroundColor, JjPalette.light.bg); // 기기 따라 t.platformDispatcher.platformBrightnessTestValue = Brightness.dark; await t.runAsync(() => c.updateSettings((x) => x.display.brightness = 'system')); await t.pump(); await t.pump(); expect(JjColors.isLight, isFalse); t.platformDispatcher.platformBrightnessTestValue = Brightness.light; await t.pump(); await t.pump(); expect(JjColors.isLight, isTrue, reason: '기기 밝기가 바뀌면 따라감'); await t.runAsync(() => c.updateSettings((x) => x.display.brightness = 'dark')); await t.pump(); await t.pump(); expect(JjColors.isLight, isFalse); expect(Theme.of(ctx()).brightness, Brightness.dark); await t.pumpWidget(const SizedBox()); }); }