使用自定义图标字体的黄金测试 class

Golden tests with custom icon font class

我有一个自定义的图标字体 .ttf,我在我的应用程序中使用它作为 IconData,允许像使用 Flutter 的内置 material 图标一样使用。

我的自定义字体class:

class MyIcons {
    MyIcons._();

    static const iconFontFamily = 'MyIcons';
    static const iconFontPackage = 'my_icon_package';

    /// edit_outline
    static const IconData edit_outline = IconData(0xe000, fontFamily: iconFontFamily, fontPackage: iconFontPackage);


  // etc
}

用法:

Icon(
  MyIcons.edit_outline,
  size: 24,
)

并且在应用程序中一切正常。但是现在我正在尝试生成黄金测试文件以确保我的图标在更新 .ttf 后按预期工作,但图标总是只替换为测试 Ahem 字体方块。

如果我使用 Flutter 的默认图标,并在 pubspec.yaml 中设置 uses-material-design: true,这允许默认图标在 golden 测试文件中正确呈现,但是无论我尝试什么我都不能渲染我自己的图标。

我尝试过但未成功的其他事情:

有办法吗?

是的,这是可能的,但您需要像在 flutter 测试中的任何其他自定义字体一样手动加载图标字体。

我使用了 Flutter Gallery 演示应用中的部分解决方案: https://github.com/flutter/gallery/blob/master/golden_test/testing/font_loader.dart

对于 material 个图标,我使用了此处找到的解决方案:

问题:https://github.com/flutter/flutter/issues/75391

代码:https://github.com/flutter/flutter/pull/74131/files

一些陷阱是:

  • 似乎字体加载必须在测试函数之外完成,所以先执行它然后 运行 测试。
  • 确保文件名和字体系列在所有地方都是正确的。

假设有一个正确设置的名为 'Matter' 的自定义字体和一个正确设置的自定义图标字体存在于项目根目录的“字体”目录中 -->

演示代码:

import 'dart:io';
import 'dart:typed_data';

import 'package:[your_path_to_icon_definition_file]/custom_icons.dart';
import 'package:file/file.dart' as f;
import 'package:file/local.dart' as l;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as path;
import 'package:platform/platform.dart' as p;

Future<void> main() async {
  await loadFonts();
  run();
}

void run() {
  group('mobile', () {
    testWidgets('Test font and icon loading', (tester) async {
      await tester.pumpWidget(MaterialApp(
        theme: ThemeData(
          fontFamily: 'Matter', // Define the custom font family.
        ),
        home: Scaffold(
          appBar: AppBar(
            title: Text('AppBar'),
          ),
          body: Column(
            children: [
              Text('test'),
              Icon(CustomIcons.camera), // Custom icon file from for example fluttericon.com
              Icon(Icons.add), // material icons ('uses-material-design: true' Must exist in pubspec.yaml)
            ],
          ),
        ),
      ));

      expect(find.byType(MaterialApp), findsOneWidget);

      await expectLater(
        find.byType(MaterialApp),
        matchesGoldenFile('goldens/material_app.png'),
      );
    });
  });
}

/// Load fonts to make sure they show up in golden tests.
Future<void> loadFonts() async {
  await _load(await loadFontsFromFontsDir());
  await _loadMaterialIconFont();
}

// Loads the cached material icon font.
// Only necessary for golden tests. Relies on the tool updating cached assets before
// running tests.
Future<void> _loadMaterialIconFont() async {
  const f.FileSystem fs = l.LocalFileSystem();
  const p.Platform platform = p.LocalPlatform();
  final flutterRoot = fs.directory(platform.environment['FLUTTER_ROOT']);

  final iconFont = flutterRoot.childFile(
    fs.path.join(
      'bin',
      'cache',
      'artifacts',
      'material_fonts',
      'MaterialIcons-Regular.otf',
    ),
  );

  final bytes = Future<ByteData>.value(iconFont.readAsBytesSync().buffer.asByteData());

  await (FontLoader('MaterialIcons')..addFont(bytes)).load();
}

/// Assumes a fonts dir in root of project
Map<String, List<Future<ByteData>>> loadFontsFromFontsDir() {
  final fontFamilyToData = <String, List<Future<ByteData>>>{};
  final currentDir = path.dirname(Platform.script.path);
  final fontsDirectory = path.join(
    currentDir,
    'fonts',
  );
  for (var file in Directory(fontsDirectory).listSync()) {
    if (file is File) {
      final fontFamily = path.basenameWithoutExtension(file.path).split('-').first;
      (fontFamilyToData[fontFamily] ??= []).add(file.readAsBytes().then((bytes) => ByteData.view(bytes.buffer)));
    }
  }
  return fontFamilyToData;
}

Future<void> _load(Map<String, List<Future<ByteData>>> fontFamilyToData) async {
  final waitList = <Future<void>>[];
  for (final entry in fontFamilyToData.entries) {
    print('entry.key=${entry.key}');
    final loader = FontLoader(entry.key);
    for (final data in entry.value) {
      loader.addFont(data);
    }
    waitList.add(loader.load());
  }
  await Future.wait(waitList);
}

导致:

多亏了

,我最终解决了问题

有一种方法可以两全其美,您可以拥有独立的字体包,而不必在使用它的应用程序中声明打包的字体文件。

例如,我们有一个公司 branding/typography 包,我们在多个应用程序中使用它,其中包含我们所有的预配置 TextStyle 声明,以及另一个自定义生成的独立包 IconData 存储在 *.ttf 文件中(如 FontAwesome)。

包装方:

pubspec.yaml


flutter:
  uses-material-design: true
  assets:
    - assets/fonts/
  fonts:
    - family: MyFont
      fonts:
        - asset: assets/fonts/MyFont.ttf
          weight: 400

    # etc

打包后TextStyle:

class BrandStyles {
  static const _packageName = '<package_name>';

  static const headline1Style = TextStyle(
    color: Colors.black,
    fontFamily: 'MyFont',
    fontSize: 60.0,
    fontStyle: FontStyle.normal,
    fontWeight: FontWeight.w400,
    height: 1.16,
    letterSpacing: 0,
    package: _packageName,
  );


  // etc

}

黄金测试

void main() {
  final widget = MaterialApp(
    theme: ThemeData(
      textTheme: TextTheme(
        // use custom extension method to remove `package` value
        headline1: BrandStyles.headline1Style.trimFontPackage(),
      ),
    ),
    home: Scaffold(
      body: SafeArea(child: StylesExample()),
    ),
  );

  setUp(() async {
    TestWidgetsFlutterBinding.ensureInitialized();
    final file = File('path/to/packaged/asset/MyFont.ttf').readAsBytesSync();
    final bytes = Future<ByteData>.value(file.buffer.asByteData());

    await (FontLoader('MyFont')..addFont(bytes)).load();
  });

  testWidgets('Golden typography test', (WidgetTester tester) async {
    await tester.pumpWidget(widget);
    await expectLater(
        find.byType(MaterialApp), matchesGoldenFile('goldens/typography.png'));
  });
}

extension StylingExtensions on TextStyle {
  
  TextStyle trimFontPackage() {
    return TextStyle(
      inherit: inherit,
      color: color,
      backgroundColor: backgroundColor,
      fontSize: fontSize,
      fontWeight: fontWeight,
      fontStyle: fontStyle,
      letterSpacing: letterSpacing,
      wordSpacing: wordSpacing,
      textBaseline: textBaseline,
      height: height,
      locale: locale,
      foreground: foreground,
      background: background,
      shadows: shadows,
      fontFeatures: fontFeatures,
      decoration: decoration,
      decorationColor: decorationColor,
      decorationStyle: decorationStyle,
      decorationThickness: decorationThickness,
      debugLabel: debugLabel,
      /// `replaceAll` only required if loading multiple fonts, 
      /// otherwise set value to your single `fontFamily` name
      fontFamily: fontFamily.replaceAll('packages/<package_name>/', ''),
    );
  }
}

或者,如果像我一样,您对自定义图标有同样的问题,同样可以在您的自定义 IconData 黄金测试中使用类似的扩展方法来完成,删除 fontPackage 值:

extension IconExtensions on IconData {
  IconData convertToGolden() => IconData(
        this.codePoint,
        fontFamily: this.fontFamily,
      );
}

您的应用端

pubspec.yaml


# ...

dependencies:
  flutter:
    sdk: flutter

  <package_name>:
    git:
      url: <url_to_hosted_package>.git
      ref: <release_tag>

main.dart


class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData.light().copyWith(
        textTheme: TextTheme(
          headline1: BrandStyles.headline1Style,
        ),
      ),
    );
  }

}

现在不再需要在您的应用程序中声明您的字体 pubspec.yaml,甚至不需要在与您的实施应用程序相同的 project/repository 中拥有样式包。