diff --git a/client/lib/pangolin_theme.dart b/client/lib/pangolin_theme.dart index ef5b738..f1f1a5c 100644 --- a/client/lib/pangolin_theme.dart +++ b/client/lib/pangolin_theme.dart @@ -143,15 +143,45 @@ extension PangolinContext on BuildContext { PangolinScheme get pangolin => Theme.of(this).extension()!; } -/// ── Font families (via google_fonts at runtime) ───────────────────── +/// ── Font families ─────────────────────────────────────────────────── +/// 生产用 google_fonts 运行时加载;测试态设 [useBundled]=true,改用本地 +/// FontLoader 注册的同名 family(避免 google_fonts 在无网/禁拉取时抛异常, +/// 让 golden 截图回归可跑)。封装函数是 PangolinText/_build 的唯一字体入口。 class PangolinFonts { PangolinFonts._(); - // String constants used as display labels only. - // Actual font loading is done via google_fonts in PangolinText getters. static const display = 'Sora'; static const sans = 'Manrope'; static const cjk = 'Noto Sans SC'; static const mono = 'JetBrains Mono'; + + /// 测试钩子:true 时用 bundled family(test/fonts/ 经 FontLoader 注册),不走 google_fonts。 + static bool useBundled = false; + + static TextStyle sora({double? fontSize, FontWeight? fontWeight, double? height, double? letterSpacing}) => + useBundled + ? TextStyle(fontFamily: display, fontSize: fontSize, fontWeight: fontWeight, height: height, letterSpacing: letterSpacing) + : GoogleFonts.sora(fontSize: fontSize, fontWeight: fontWeight, height: height, letterSpacing: letterSpacing); + + static TextStyle manrope( + {double? fontSize, FontWeight? fontWeight, double? height, double? letterSpacing, List? fontFeatures}) => + useBundled + ? TextStyle( + fontFamily: sans, + fontSize: fontSize, + fontWeight: fontWeight, + height: height, + letterSpacing: letterSpacing, + fontFeatures: fontFeatures) + : GoogleFonts.manrope( + fontSize: fontSize, fontWeight: fontWeight, height: height, letterSpacing: letterSpacing, fontFeatures: fontFeatures); + + static TextStyle jetBrainsMono({double? fontSize, FontWeight? fontWeight, List? fontFeatures}) => + useBundled + ? TextStyle(fontFamily: mono, fontSize: fontSize, fontWeight: fontWeight, fontFeatures: fontFeatures) + : GoogleFonts.jetBrainsMono(fontSize: fontSize, fontWeight: fontWeight, fontFeatures: fontFeatures); + + static TextTheme manropeTextTheme(TextTheme base) => + useBundled ? base.apply(fontFamily: sans) : GoogleFonts.manropeTextTheme(base); } /// ── Type scale ────────────────────────────────────────────────────── @@ -161,27 +191,27 @@ class PangolinFonts { class PangolinText { PangolinText._(); - static TextStyle get displayXl => GoogleFonts.sora( + static TextStyle get displayXl => PangolinFonts.sora( fontSize: 48, fontWeight: FontWeight.w700, height: 1.15, letterSpacing: -0.96); - static TextStyle get display => GoogleFonts.sora( + static TextStyle get display => PangolinFonts.sora( fontSize: 36, fontWeight: FontWeight.w700, height: 1.15, letterSpacing: -0.72); - static TextStyle get h1 => GoogleFonts.sora( + static TextStyle get h1 => PangolinFonts.sora( fontSize: 30, fontWeight: FontWeight.w600, height: 1.3, letterSpacing: -0.3); - static TextStyle get h2 => GoogleFonts.sora( + static TextStyle get h2 => PangolinFonts.sora( fontSize: 24, fontWeight: FontWeight.w600, height: 1.3, letterSpacing: -0.24); - static TextStyle get h3 => GoogleFonts.manrope( + static TextStyle get h3 => PangolinFonts.manrope( fontSize: 20, fontWeight: FontWeight.w600, height: 1.3); - static TextStyle get bodyLg => GoogleFonts.manrope( + static TextStyle get bodyLg => PangolinFonts.manrope( fontSize: 18, fontWeight: FontWeight.w400, height: 1.65); - static TextStyle get body => GoogleFonts.manrope( + static TextStyle get body => PangolinFonts.manrope( fontSize: 16, fontWeight: FontWeight.w400, height: 1.5); - static TextStyle get sm => GoogleFonts.manrope( + static TextStyle get sm => PangolinFonts.manrope( fontSize: 14, fontWeight: FontWeight.w400, height: 1.5); - static TextStyle get caption => GoogleFonts.manrope( + static TextStyle get caption => PangolinFonts.manrope( fontSize: 12, fontWeight: FontWeight.w500, height: 1.5); - static TextStyle get overline => GoogleFonts.manrope( + static TextStyle get overline => PangolinFonts.manrope( fontSize: 12, fontWeight: FontWeight.w600, letterSpacing: 0.96); - static TextStyle get mono => GoogleFonts.jetBrainsMono( + static TextStyle get mono => PangolinFonts.jetBrainsMono( fontSize: 14, fontWeight: FontWeight.w400, fontFeatures: const [FontFeature.tabularFigures()]); } @@ -210,8 +240,8 @@ class PangolinTheme { brightness: b, colorScheme: scheme, scaffoldBackgroundColor: s.bg, - // Use google_fonts Manrope as app-wide base font - textTheme: GoogleFonts.manropeTextTheme( + // Manrope app-wide base font(生产 google_fonts / 测试 bundled) + textTheme: PangolinFonts.manropeTextTheme( ThemeData(brightness: b).textTheme, ), extensions: [s], @@ -219,7 +249,7 @@ class PangolinTheme { style: FilledButton.styleFrom( backgroundColor: s.accent, foregroundColor: s.fgOnAccent, - textStyle: GoogleFonts.manrope(fontWeight: FontWeight.w600, fontSize: 15), + textStyle: PangolinFonts.manrope(fontWeight: FontWeight.w600, fontSize: 15), shape: const StadiumBorder(), padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14), ), diff --git a/client/test/flutter_test_config.dart b/client/test/flutter_test_config.dart new file mode 100644 index 0000000..e33d336 --- /dev/null +++ b/client/test/flutter_test_config.dart @@ -0,0 +1,39 @@ +// flutter_test_config.dart — 全测试前置(Flutter 自动应用于 test/ 下所有测试)。 +// +// 启用 PangolinFonts.useBundled,并用 FontLoader 注册 test/fonts/ 的真实字体, +// 让 widget/golden 测试用真字体渲染、且不触发 google_fonts 运行时拉取(避免抛异常)。 +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pangolin_vpn/pangolin_theme.dart'; + +Future testExecutable(FutureOr Function() testMain) async { + TestWidgetsFlutterBinding.ensureInitialized(); + PangolinFonts.useBundled = true; + await _load('Sora', const ['test/fonts/Sora-Regular.ttf', 'test/fonts/Sora-SemiBold.ttf', 'test/fonts/Sora-Bold.ttf']); + await _load('Manrope', const [ + 'test/fonts/Manrope-Regular.ttf', + 'test/fonts/Manrope-Medium.ttf', + 'test/fonts/Manrope-SemiBold.ttf', + 'test/fonts/Manrope-Bold.ttf', + ]); + await _load('JetBrains Mono', const ['test/fonts/JetBrainsMono-Regular.ttf']); + // Lucide 图标字体(family 须带 package 前缀以匹配 PangolinIcons 的 fontPackage)。 + await _load('packages/lucide_icons/Lucide', const ['packages/lucide_icons_patched/fonts/lucide.ttf']); + await testMain(); +} + +Future _load(String family, List paths) async { + final loader = FontLoader(family); + for (final p in paths) { + final file = File(p); + if (file.existsSync()) { + final bytes = file.readAsBytesSync(); + loader.addFont(Future.value(ByteData.view(bytes.buffer, bytes.offsetInBytes, bytes.lengthInBytes))); + } + } + await loader.load(); +} diff --git a/client/test/golden/components_golden_test.dart b/client/test/golden/components_golden_test.dart index 1b3995e..d06598a 100644 --- a/client/test/golden/components_golden_test.dart +++ b/client/test/golden/components_golden_test.dart @@ -1,21 +1,9 @@ // components_golden_test.dart — 关键组件 golden(明/暗两主题) // // 覆盖:连接键三态、智能选择推荐卡、免费额度卡。 -// 首次生成基准图:`flutter test --update-goldens test/golden`。 -// -// TODO: 字体依赖待解决。 -// google_fonts 在测试环境中使用内部变体字体族名(如 `Manrope_400italic0`), -// 难以通过 FontLoader 预加载。需要: -// 1. 确认 google_fonts v6.3.3 各变体的实际族名 -// 2. 在 setUpAll 中用 FontLoader 加载 test/fonts/*.ttf -// 3. 调用 `flutter test --update-goldens test/golden` 生成基准图 -// 在此之前,跳过 golden 测试以避免 CI 失败; -// 组件行为已由 test/widget/ 覆盖。 -// ignore_for_file: directives_ordering -@Skip('Golden tests need bundled fonts + baseline generation. ' - 'See TODO above for setup steps.') -library; - +// 重新生成基准图:`flutter test --update-goldens test/golden`。 +// 字体由 test/flutter_test_config.dart 经 FontLoader 注册 + PangolinFonts.useBundled +// 提供真实 Sora/Manrope/JetBrainsMono,无需 google_fonts 运行时拉取。 import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pangolin_vpn/l10n/strings_zh.dart'; diff --git a/client/test/golden/goldens/connect_connecting_dark.png b/client/test/golden/goldens/connect_connecting_dark.png index d6bbcd0..dcc6bca 100644 Binary files a/client/test/golden/goldens/connect_connecting_dark.png and b/client/test/golden/goldens/connect_connecting_dark.png differ diff --git a/client/test/golden/goldens/connect_connecting_light.png b/client/test/golden/goldens/connect_connecting_light.png index 8cf161f..0087cb9 100644 Binary files a/client/test/golden/goldens/connect_connecting_light.png and b/client/test/golden/goldens/connect_connecting_light.png differ diff --git a/client/test/golden/goldens/connect_off_dark.png b/client/test/golden/goldens/connect_off_dark.png index 5d6258a..52f6002 100644 Binary files a/client/test/golden/goldens/connect_off_dark.png and b/client/test/golden/goldens/connect_off_dark.png differ diff --git a/client/test/golden/goldens/connect_off_light.png b/client/test/golden/goldens/connect_off_light.png index 73cb14a..c248e86 100644 Binary files a/client/test/golden/goldens/connect_off_light.png and b/client/test/golden/goldens/connect_off_light.png differ diff --git a/client/test/golden/goldens/connect_on_dark.png b/client/test/golden/goldens/connect_on_dark.png index c99f327..af66fd6 100644 Binary files a/client/test/golden/goldens/connect_on_dark.png and b/client/test/golden/goldens/connect_on_dark.png differ diff --git a/client/test/golden/goldens/connect_on_light.png b/client/test/golden/goldens/connect_on_light.png index 480ab6d..6541a37 100644 Binary files a/client/test/golden/goldens/connect_on_light.png and b/client/test/golden/goldens/connect_on_light.png differ diff --git a/client/test/golden/goldens/quota_low_dark.png b/client/test/golden/goldens/quota_low_dark.png index 73abd41..4dd0255 100644 Binary files a/client/test/golden/goldens/quota_low_dark.png and b/client/test/golden/goldens/quota_low_dark.png differ diff --git a/client/test/golden/goldens/quota_low_light.png b/client/test/golden/goldens/quota_low_light.png index fa52497..995a65f 100644 Binary files a/client/test/golden/goldens/quota_low_light.png and b/client/test/golden/goldens/quota_low_light.png differ diff --git a/client/test/golden/goldens/quota_unlocked_dark.png b/client/test/golden/goldens/quota_unlocked_dark.png index 833b1da..ca46693 100644 Binary files a/client/test/golden/goldens/quota_unlocked_dark.png and b/client/test/golden/goldens/quota_unlocked_dark.png differ diff --git a/client/test/golden/goldens/quota_unlocked_light.png b/client/test/golden/goldens/quota_unlocked_light.png index 7e182a9..2ac71ce 100644 Binary files a/client/test/golden/goldens/quota_unlocked_light.png and b/client/test/golden/goldens/quota_unlocked_light.png differ diff --git a/client/test/golden/goldens/smart_card_dark.png b/client/test/golden/goldens/smart_card_dark.png index 133aec3..22cadd9 100644 Binary files a/client/test/golden/goldens/smart_card_dark.png and b/client/test/golden/goldens/smart_card_dark.png differ diff --git a/client/test/golden/goldens/smart_card_light.png b/client/test/golden/goldens/smart_card_light.png index 76c04ca..2624b30 100644 Binary files a/client/test/golden/goldens/smart_card_light.png and b/client/test/golden/goldens/smart_card_light.png differ diff --git a/client/tool/visual-diff.md b/client/tool/visual-diff.md index 3c38665..9a73e12 100644 --- a/client/tool/visual-diff.md +++ b/client/tool/visual-diff.md @@ -26,10 +26,13 @@ node ~/.claude/skills/design-distill/tools/diff.mjs