diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0eea8367..cb64103d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,6 +23,13 @@ jobs: channel: any - run: flutter pub get - run: flutter test + - name: Archive golden test errors + if: failure() + uses: actions/upload-artifact@v4 + with: + name: 3-10-failed + path: test/ + retention-days: 7 test_channel: timeout-minutes: 10 @@ -39,3 +46,29 @@ jobs: channel: ${{ matrix.version }} - run: flutter pub get - run: flutter test + - name: Archive golden test errors + if: failure() + uses: actions/upload-artifact@v4 + with: + name: branches-tests-failed + path: test/ + retention-days: 7 + + test_windows: + timeout-minutes: 10 + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + channel: stable + - run: flutter pub get + - run: flutter test + - name: Archive golden test errors + if: failure() + uses: actions/upload-artifact@v4 + with: + name: branches-tests-failed + path: test/ + retention-days: 7 \ No newline at end of file diff --git a/lib/spot.dart b/lib/spot.dart index 0b12710c..9d6d9d32 100644 --- a/lib/spot.dart +++ b/lib/spot.dart @@ -17,6 +17,8 @@ export 'package:checks/context.dart' show Condition, Context, ContextExtension, Extracted, Rejection, Subject; export 'package:meta/meta.dart' show useResult; export 'package:spot/src/act/act.dart' show Act, act; +export 'package:spot/src/screenshot/load_fonts.dart' + show loadAppFonts, loadFont; export 'package:spot/src/screenshot/screenshot.dart' show ElementScreenshotExtension, diff --git a/lib/src/flutter/flutter_sdk.dart b/lib/src/flutter/flutter_sdk.dart new file mode 100644 index 00000000..00c291fc --- /dev/null +++ b/lib/src/flutter/flutter_sdk.dart @@ -0,0 +1,21 @@ +import 'dart:io'; +import 'package:dartx/dartx_io.dart'; + +/// Returns the Flutter SDK root directory based on the current flutter +/// executable running the tests. +Directory flutterSdkRoot() { + final flutterTesterExe = Platform.executable; + final String flutterRoot; + if (Platform.isWindows) { + flutterRoot = flutterTesterExe.split(r'\bin\cache\')[0]; + } else { + flutterRoot = flutterTesterExe.split('/bin/cache/')[0]; + } + return Directory(flutterRoot); +} + +/// The Flutter executable in the Flutter SDK +String get flutterExe { + final exe = Platform.isWindows ? '.bat' : ''; + return flutterSdkRoot().file('bin/flutter$exe').absolute.path; +} diff --git a/lib/src/screenshot/load_fonts.dart b/lib/src/screenshot/load_fonts.dart new file mode 100644 index 00000000..d9fde1cb --- /dev/null +++ b/lib/src/screenshot/load_fonts.dart @@ -0,0 +1,279 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dartx/dartx_io.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/src/flutter/flutter_sdk.dart'; + +/// Loads all font from the apps FontManifest and embedded in the Flutter SDK +/// +/// ## What is loaded? +/// ### App Fonts (FontManifest) +/// - All fonts defined in the pubspec.yaml +/// - All fonts of dependencies that define fonts in their pubspec.yaml +/// +/// ### Embedded Flutter SDK Fonts +/// - Roboto +/// - RobotoCondensed +/// - MaterialIcons +/// +/// ## Why load Roboto by default? +/// +/// Widget test run with [TargetPlatform.android] by default. [MaterialApp] sets +/// the Roboto fontFamily as default for [TargetPlatform.android] (see +/// [Typography]). Loading the Roboto fontFamily therefore allows showing text +/// in the default scenario of a Flutter app. +/// Fortunately, Robot is part of the Flutter SDK and can be loaded right away. +/// +/// ## Custom fonts +/// +/// Apps that use custom fonts, should declare them in the pubspec.yaml file (https://docs.flutter.dev/cookbook/design/fonts#declare-the-font-in-the-pubspec-yaml-file). +/// Those fonts are automatically added to the FontManifest.json file during build. +/// +/// The [loadAppFonts] function loads all font defined in the FontManifest.json file. +/// +/// ## Depending on System fonts +/// +/// Some apps do not ship their fonts, but use a system font e.g. "Segoe UI" +/// on [TargetPlatform.windows] or "Apple Color Emoji" on [TargetPlatform.iOS]. +/// +/// Those system fonts are not loaded by [loadAppFonts], load them individually +/// with [loadFont]. +/// +/// ## Emojis +/// +/// Why are emojis not rendered after calling [loadAppFonts]? +/// +/// Emojis are not part of the Roboto font. +/// Each operating system provides their own font that handles +/// emoji glyphs. In Flutter apps, those emoji fonts are automatically loaded +/// by Skia (the rendering engine of Flutter) from the operating system as fallbacks +/// when it encounters an emoji character that is covered by the defined +/// fontFamily or fontFamilyFallback. +/// +/// Flutter tests disable the automatic system font loading by Skia. Skia will +/// not search for system fonts. (https://github.com/flutter/engine/blob/a842207f6d90de4fc006ea8f0b649b38d6e104a0/lib/ui/text/font_collection.cc#L148) +/// +/// To show emojis in tests, load the system emoji font manually with [loadFont]. +/// E.g. "/System/Library/Fonts/Apple Color Emoji.ttc" on macOS. +/// Do not forget to set "Apple Color Emoji" as fontFamilyFallback. Skia will +/// *not* automatically fallback to "Apple Color Emoji" unless it is defined in +/// the TextStyle. +/// +/// Because showing emojis in test requires changes to you app code (set fallback) +/// [loadAppFonts] does not automatically load system emoji fonts for you. +Future loadAppFonts() async { + TestWidgetsFlutterBinding.ensureInitialized(); + + await TestAsyncUtils.guard(() async { + // First we load the Roboto font from the Flutter SDK, which most Android apps use. + // In case the app defines a custom Roboto fontFamily it will be overwritten when + // loading the fonts from the manifest + await _loadMaterialFontsFromSdk(); + + // Load all fonts defined in the FontManifest.json file + await _loadFontsFromFontManifest(); + }); +} + +/// Loads a fontFamily consisting of multiple font files. +/// +/// ```dart +/// debugDefaultTargetPlatformOverride = TargetPlatform.windows; +/// await loadFont('Comic Sans', [ +/// r'C:\Windows\Fonts\comic.ttf', // Regular +/// r'C:\Windows\Fonts\comicbd.ttf', // Bold +/// r'C:\Windows\Fonts\comici.ttf', // Italic +/// ]); +/// +/// tester.pumpWidget( +/// MaterialApp( +/// home: Center( +/// child: Text( +/// 'Loaded custom Font', +/// style: TextStyle( +/// fontFamily: 'Comic Sans', +/// ), +/// ), +/// ), +/// ), +/// ); +/// ``` +/// +/// Flutter support the following formats: .ttf, .otf, .ttc +/// +/// Calling [loadFont] multiple times with the same family will overwrites the +/// previous +/// +/// The [family] is optional: '' will extract the family name from the font file. +Future loadFont(String family, List fontPaths) async { + if (fontPaths.isEmpty) { + return; + } + + await TestAsyncUtils.guard(() async { + final fontLoader = FontLoader(family); + for (final path in fontPaths) { + try { + final file = File(path); + if (file.existsSync()) { + final Uint8List bytes = file.readAsBytesSync(); + fontLoader.addFont(Future.value(bytes.buffer.asByteData())); + } else { + final data = rootBundle.load(path); + fontLoader.addFont(Future.value(data)); + } + } catch (e, stack) { + debugPrint("Could not load font $path\n$e\n$stack"); + } + } + // the fontLoader is unusable after calling load(). + // No need to cache or return it. + await fontLoader.load(); + }); +} + +/// Loads the Roboto/RobotoCondensed/MaterialIcons fonts from the executing Flutter SDK +Future _loadMaterialFontsFromSdk() async { + final root = flutterSdkRoot().absolute.path; + + final materialFontsDir = + Directory('$root/bin/cache/artifacts/material_fonts/'); + + final fontFormats = ['.ttf', '.otf', '.ttc']; + final existingFonts = materialFontsDir + .listSync() + // dartfmt come on,... + .whereType() + .where( + (font) => fontFormats.any((element) => font.path.endsWith(element)), + ) + .toList(); + + final robotoFonts = existingFonts + .where((font) { + final name = font.name.toLowerCase(); + return name.startsWith('Roboto-'.toLowerCase()); + }) + .map((file) => file.path) + .toList(); + if (robotoFonts.isEmpty) { + debugPrint("Warning: No Roboto font found in SDK"); + } + await loadFont('Roboto', robotoFonts); + + final robotoCondensedFonts = existingFonts + .where((font) { + final name = font.name.toLowerCase(); + return name.startsWith('RobotoCondensed-'.toLowerCase()); + }) + .map((file) => file.path) + .toList(); + await loadFont('RobotoCondensed', robotoCondensedFonts); + + final materialIcons = existingFonts + .where((font) { + final name = font.name.toLowerCase(); + return name.startsWith('MaterialIcons-'.toLowerCase()); + }) + .map((file) => file.path) + .toList(); + await loadFont('MaterialIcons', materialIcons); +} + +/// Loads the fonts from the FontManifest.json file. +/// +/// Fonts defined in an app are accessible via it family name "MyFont" +/// Fonts defined in a package are accessible via "packages/myPackage/MyFont" +/// +/// Because each app can also be a package, each font is available with both +/// notations. +/// This allows packages to access their own fonts also via +/// "packages/myPackage/MyFont" like users of the package would. +Future _loadFontsFromFontManifest() async { + // The FontManifest.json file is generated by the Flutter build process + // located in /build/flutter_assets/FontManifest.json and bundled within the app + final binding = TestWidgetsFlutterBinding.instance; + final fontManifestContent = + await binding.runAsync(() => rootBundle.loadString('FontManifest.json')); + final json = jsonDecode(fontManifestContent!); + final fontManifest = _FontManifest.fromJson(json); + + for (final item in fontManifest.fontFamilies) { + final packageAsset = + item.assets.firstOrNullWhere((it) => it.startsWith('packages/')); + final packageName = packageAsset?.split('/')[1]; + + if (packageName == null) { + // font asset in pubspec.yaml references a file relative to the pubspec.yaml + // The font can not be used by other packages + await loadFont(item.family, item.assets); + } else { + // font uses the package notation, which resolves relative to the packages lib/* directory + // asset: packages///MyFont.ttf + + // Make it accessible as "MyFont" to be used by the package itself + final fontFamilyName = item.family.split('/').last; + await loadFont(fontFamilyName, item.assets); + // and "packages//MyFont" so that other packages would reference it. + await loadFont('packages/$packageName/$fontFamilyName', item.assets); + } + } +} + +/// Parsed representation of the FontManifest.json file +class _FontManifest { + final List<_FontManifestFontFamily> fontFamilies; + + /// Represents a Flutter FontManifest + _FontManifest(this.fontFamilies); + + /// Parses the FontManifest.json file + /// + /// Example: + /// ```json + /// [ + /// { + /// "family": "packages/app_font/Montserrat", + /// "fonts": [ + /// { + /// "asset": "packages/app_font/fonts/Montserrat-Regular.ttf" + /// } + /// ] + /// } + /// ] + /// ``` + factory _FontManifest.fromJson(dynamic json) { + if (json is! List) { + throw const FormatException('FontManifest must begin with a List'); + } + final List<_FontManifestFontFamily> fontFamilies = []; + for (final family in json) { + if (family is! Map) continue; + final familyName = family['family']; + if (familyName is! String) continue; + final List assets = []; + final fonts = family['fonts']; + if (fonts is! List) continue; + for (final font in fonts) { + if (font is! Map) continue; + final asset = font['asset']; + if (asset is! String) continue; + // there are other values like weight and style, but those are ignored by Flutter + // https://github.com/flutter/website/issues/3591#issuecomment-521806077 + assets.add(asset); + } + fontFamilies.add(_FontManifestFontFamily(familyName, assets)); + } + return _FontManifest(fontFamilies); + } +} + +class _FontManifestFontFamily { + final String family; + final List assets; + + _FontManifestFontFamily(this.family, this.assets); +} diff --git a/test/fonts/app_font_test.dart b/test/fonts/app_font_test.dart new file mode 100644 index 00000000..3530e8a3 --- /dev/null +++ b/test/fonts/app_font_test.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dartx/dartx_io.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/src/flutter/flutter_sdk.dart'; + +import 'font_test_project.dart'; + +void main() { + test('When defined in pubspec a third-party font is loaded', () async { + final testProject = FontTestProject('test/fonts/templates/app_font'); + await testProject.create(); + debugPrint('Run pub get'); + await Process.run( + flutterExe, + ['pub', 'get'], + workingDirectory: testProject.workingDir.path, + ); + debugPrint('Run tests'); + final test = await Process.start( + flutterExe, + ['test'], + workingDirectory: testProject.workingDir.path, + ); + test.stdout.transform(utf8.decoder).listen((event) { + debugPrint(event); + }); + test.stderr.transform(utf8.decoder).listen((event) { + debugPrint(event); + }); + final exitCode = await test.exitCode; + if (exitCode != 0) { + final failuresDir = + Directory('${testProject.workingDir.path}/test/failures'); + if (failuresDir.existsSync()) { + final testFailureDirectory = Directory('test/fonts/app_font_test/'); + if (testFailureDirectory.existsSync()) { + testFailureDirectory.deleteSync(recursive: true); + } else { + testFailureDirectory.createSync(recursive: true); + } + await failuresDir.copyRecursively(testFailureDirectory); + } + } + expect(exitCode, 0); + }); +} diff --git a/test/fonts/default_font_test.dart b/test/fonts/default_font_test.dart new file mode 100644 index 00000000..65aa7836 --- /dev/null +++ b/test/fonts/default_font_test.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dartx/dartx_io.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/src/flutter/flutter_sdk.dart'; + +import 'font_test_project.dart'; + +void main() { + test('roboto is loaded per default', () async { + final testProject = FontTestProject('test/fonts/templates/default_font'); + await testProject.create(); + debugPrint('Run pub get'); + await Process.run( + flutterExe, + ['pub', 'get'], + workingDirectory: testProject.workingDir.path, + ); + debugPrint('Run tests'); + final test = await Process.start( + flutterExe, + ['test'], + workingDirectory: testProject.workingDir.path, + ); + test.stdout.transform(utf8.decoder).listen((event) { + debugPrint(event); + }); + test.stderr.transform(utf8.decoder).listen((event) { + debugPrint(event); + }); + final exitCode = await test.exitCode; + + if (exitCode != 0) { + final failuresDir = + Directory('${testProject.workingDir.path}/test/failures'); + if (failuresDir.existsSync()) { + final testFailureDirectory = + Directory('test/fonts/default_font_test_failures/'); + if (testFailureDirectory.existsSync()) { + testFailureDirectory.deleteSync(recursive: true); + } else { + testFailureDirectory.createSync(recursive: true); + } + await failuresDir.copyRecursively(testFailureDirectory); + } + } + expect(exitCode, 0); + }); +} diff --git a/test/fonts/dependency_font_test.dart b/test/fonts/dependency_font_test.dart new file mode 100644 index 00000000..9b0a0c2d --- /dev/null +++ b/test/fonts/dependency_font_test.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dartx/dartx_io.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/src/flutter/flutter_sdk.dart'; + +import 'font_test_project.dart'; + +void main() { + test('When defined in pubspec a third-party font is loaded', () async { + final dependencyFontProject = + FontTestProject('test/fonts/templates/dependency_font'); + await dependencyFontProject.create(); + + final appFontProject = FontTestProject('test/fonts/templates/app_font'); + await appFontProject.create( + dir: Directory( + '${dependencyFontProject.workingDir.path}/packages/app_font', + ), + ); + + debugPrint('Run pub get'); + await Process.run( + flutterExe, + ['pub', 'get'], + workingDirectory: dependencyFontProject.workingDir.path, + ); + debugPrint('Run tests'); + final test = await Process.start( + flutterExe, + ['test'], + workingDirectory: dependencyFontProject.workingDir.path, + ); + test.stdout.transform(utf8.decoder).listen((event) { + debugPrint(event); + }); + test.stderr.transform(utf8.decoder).listen((event) { + debugPrint(event); + }); + final exitCode = await test.exitCode; + if (exitCode != 0) { + final failuresDir = + Directory('${dependencyFontProject.workingDir.path}/test/failures'); + if (failuresDir.existsSync()) { + final testFailureDirectory = + Directory('test/fonts/dependency_font_test/'); + if (testFailureDirectory.existsSync()) { + testFailureDirectory.deleteSync(recursive: true); + } else { + testFailureDirectory.createSync(recursive: true); + } + await failuresDir.copyRecursively(testFailureDirectory); + } + } + expect(exitCode, 0); + }); +} diff --git a/test/fonts/font_test_project.dart b/test/fonts/font_test_project.dart new file mode 100644 index 00000000..b1183cd1 --- /dev/null +++ b/test/fonts/font_test_project.dart @@ -0,0 +1,43 @@ +import 'dart:io'; + +import 'package:dartx/dartx_io.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Loads a template from the `templates` directory and mounts it in a temporary folder +class FontTestProject { + final Directory _templateDir; + + Directory get workingDir => _workingDir; + late Directory _workingDir; + + FontTestProject(String path) : _templateDir = Directory(path); + + Future create({Directory? dir}) async { + final temp = dir ?? Directory.systemTemp.createTempSync(); + _workingDir = temp; + addTearDown(() { + temp.deleteSync(recursive: true); + }); + await _templateDir.copyRecursively(temp); + for (final file in temp.listSync(recursive: true)) { + if (file is File) { + if (file.name == 'pubspec_template.yaml') { + // rename to correct pubspec.yaml. (Not using it in template because other tools might use it) + final pubspec = file.copySync( + file.path.replaceFirst('pubspec_template.yaml', 'pubspec.yaml'), + ); + + // overwrite the spot dependencies path + final repoRoot = File.fromUri(Platform.script).parent.path; + final content = + pubspec.readAsStringSync().replaceAll('../../../../', repoRoot); + pubspec.writeAsStringSync(content); + } + if (file.name == 'test.dart') { + // rename to correct _test.dart file. (Not using it in template because flutter test would execute them) + file.copySync(file.path.replaceFirst('test.dart', 'test_test.dart')); + } + } + } + } +} diff --git a/test/fonts/load_font_test.dart b/test/fonts/load_font_test.dart new file mode 100644 index 00000000..73ddfbc4 --- /dev/null +++ b/test/fonts/load_font_test.dart @@ -0,0 +1,36 @@ +import 'dart:io'; +import 'dart:ui'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; + +void main() { + testWidgets('load non-font file does not throw', (tester) async { + final tempDir = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDir.deleteSync(recursive: true)); + final notAFont = File('${tempDir.path}/someFile.txt'); + notAFont.writeAsStringSync('some data'); + + final List messages = []; + // ignore: deprecated_member_use + PlatformDispatcher.instance.onPlatformMessage = ( + String name, + ByteData? data, + PlatformMessageResponseCallback? callback, + ) { + final decoded = SystemChannels.system.codec.decodeMessage(data); + final type = (decoded! as Map)['type']; + messages.add(type); + callback?.call(null); + }; + addTearDown(() { + // ignore: deprecated_member_use + PlatformDispatcher.instance.onPlatformMessage = null; + }); + + expect(messages, isEmpty); + await loadFont('someFont', [notAFont.path]); + expect(messages, ['fontsChange']); // success regardless of file content + }); +} diff --git a/test/fonts/templates/app_font/lib/fonts/Montserrat-Regular.ttf b/test/fonts/templates/app_font/lib/fonts/Montserrat-Regular.ttf new file mode 100644 index 00000000..f4a266dd Binary files /dev/null and b/test/fonts/templates/app_font/lib/fonts/Montserrat-Regular.ttf differ diff --git a/test/fonts/templates/app_font/pubspec_template.yaml b/test/fonts/templates/app_font/pubspec_template.yaml new file mode 100644 index 00000000..8f47bdf7 --- /dev/null +++ b/test/fonts/templates/app_font/pubspec_template.yaml @@ -0,0 +1,28 @@ +name: app_font +description: "An app using a font defined in the pubspec.yaml file." +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + flutter_test: + sdk: flutter + spot: + path: ../../../../ + + +dev_dependencies: + test: ^1.24.0 + +flutter: + fonts: + - family: Montserrat + fonts: + - asset: packages/app_font/fonts/Montserrat-Regular.ttf + - family: PrivateFont + fonts: + - asset: lib/fonts/Montserrat-Regular.ttf diff --git a/test/fonts/templates/app_font/test/golden.png b/test/fonts/templates/app_font/test/golden.png new file mode 100644 index 00000000..a921f7b4 Binary files /dev/null and b/test/fonts/templates/app_font/test/golden.png differ diff --git a/test/fonts/templates/app_font/test/test.dart b/test/fonts/templates/app_font/test/test.dart new file mode 100644 index 00000000..de16df3c --- /dev/null +++ b/test/fonts/templates/app_font/test/test.dart @@ -0,0 +1,474 @@ +// ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables + +// ignore: unnecessary_import +import 'dart:io'; +// ignore: unnecessary_import +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; + +void main() { + setUp(() { + final previousGoldenFileComparator = goldenFileComparator; + goldenFileComparator = _TolerantGoldenFileComparator( + Uri.parse('test/test_test.dart'), + precisionTolerance: 0.10, + ); + addTearDown(() => goldenFileComparator = previousGoldenFileComparator); + }); + + testWidgets('App font PrivateFont is loaded from FontManifest', + (tester) async { + await loadAppFonts(); + + await tester.pumpWidget( + const MaterialApp( + debugShowCheckedModeBanner: false, + home: FontTestWidget( + fontFamily: 'PrivateFont', + ), + ), + ); + await tester.pumpAndSettle(); + await expectLater( + find.byType(MaterialApp), + matchesGoldenFile('golden.png'), + ); + }); + + testWidgets( + 'App font Montserrat (package format) is loaded from FontManifest', + (tester) async { + await loadAppFonts(); + + await tester.pumpWidget( + const MaterialApp( + debugShowCheckedModeBanner: false, + home: FontTestWidget( + fontFamily: 'Montserrat', + ), + ), + ); + await tester.pumpAndSettle(); + await expectLater( + find.byType(MaterialApp), + matchesGoldenFile('golden.png'), + ); + }); + + testWidgets('Montserrat can be loaded with via package reference', + (tester) async { + await loadFont( + 'Montserrat', + ['packages/app_font/fonts/Montserrat-Regular.ttf'], + ); + + await tester.pumpWidget( + const MaterialApp( + debugShowCheckedModeBanner: false, + home: FontTestWidget( + fontFamily: 'Montserrat', + ), + ), + ); + await tester.pumpAndSettle(); + await expectLater( + find.byType(MaterialApp), + matchesGoldenFile('golden.png'), + ); + }); + + testWidgets('Montserrat can be loaded with via file reference', + (tester) async { + final fontPath = File('lib/fonts/Montserrat-Regular.ttf').absolute.path; + await loadFont('Montserrat', [fontPath]); + + await tester.pumpWidget( + const MaterialApp( + debugShowCheckedModeBanner: false, + home: FontTestWidget( + fontFamily: 'Montserrat', + ), + ), + ); + await tester.pumpAndSettle(); + await expectLater( + find.byType(MaterialApp), + matchesGoldenFile('golden.png'), + ); + }); + + testWidgets('Montserrat can be loaded with blank family name', + (tester) async { + final fontPath = File('lib/fonts/Montserrat-Regular.ttf').absolute.path; + await loadFont('', [fontPath]); + + await tester.pumpWidget( + const MaterialApp( + debugShowCheckedModeBanner: false, + home: FontTestWidget( + fontFamily: 'Montserrat', + ), + ), + ); + await tester.pumpAndSettle(); + await expectLater( + find.byType(MaterialApp), + matchesGoldenFile('golden.png'), + ); + }); + + testWidgets('App font is also available as package fonts', (tester) async { + await loadAppFonts(); + + await tester.pumpWidget( + const MaterialApp( + debugShowCheckedModeBanner: false, + home: FontTestWidget( + fontFamily: 'packages/app_font/Montserrat', + ), + ), + ); + await tester.pumpAndSettle(); + await expectLater( + find.byType(MaterialApp), + matchesGoldenFile('golden.png'), + ); + }); +} + +class FontTestWidget extends StatelessWidget { + const FontTestWidget({ + super.key, + this.fontFamily, + this.fontFamilyFallback = const [], + }); + + final String? fontFamily; + final List fontFamilyFallback; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: DefaultTextStyle( + style: TextStyle( + inherit: false, + height: 1.2, + letterSpacing: 0.0, + fontSize: 52, + color: Colors.black, + fontFamilyFallback: fontFamilyFallback, + fontFamily: fontFamily, + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Default Font', + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'thin', + style: TextStyle( + fontWeight: FontWeight.w100, + ), + ), + SizedBox(width: 8), + Text( + 'extra-light', + style: TextStyle( + fontWeight: FontWeight.w200, + ), + ), + SizedBox(width: 8), + Text( + 'light', + style: TextStyle( + fontWeight: FontWeight.w300, + ), + ), + SizedBox(width: 8), + Text( + 'normal', + style: TextStyle( + fontWeight: FontWeight.w400, + ), + ), + SizedBox(width: 8), + ], + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'medium', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 8), + Text( + 'semi', + style: TextStyle( + fontWeight: FontWeight.w600, + ), + ), + SizedBox(width: 8), + Text( + 'bold', + style: TextStyle( + fontWeight: FontWeight.w700, + ), + ), + SizedBox(width: 8), + Text( + 'extra', + style: TextStyle( + fontWeight: FontWeight.w800, + ), + ), + SizedBox(width: 8), + Text( + 'thick', + style: TextStyle( + fontWeight: FontWeight.w900, + ), + ), + ], + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Colored ', + style: TextStyle( + color: Colors.blue, + ), + ), + Text( + 'Text ', + style: TextStyle( + color: Colors.indigo, + ), + ), + Text( + 'Rocks', + style: TextStyle( + color: Colors.purple, + ), + ), + ], + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Italic', + style: TextStyle( + fontStyle: FontStyle.italic, + ), + ), + SizedBox(width: 8), + Text( + 'Underlined', + style: TextStyle( + decoration: TextDecoration.underline, + ), + ), + SizedBox(width: 8), + Text( + 'ItalicBold', + style: TextStyle( + fontStyle: FontStyle.italic, + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + ), + ), + ], + ), + SizedBox(height: 10), + Text.rich( + TextSpan( + style: TextStyle( + fontFeatures: [ + FontFeature.liningFigures(), + ], + ), + children: [ + TextSpan( + text: '6 ', + style: TextStyle( + fontSize: 6, + ), + ), + TextSpan( + text: '8 ', + style: TextStyle( + fontSize: 8, + ), + ), + TextSpan( + text: '10 ', + style: TextStyle( + fontSize: 10, + ), + ), + TextSpan( + text: '11 ', + style: TextStyle( + fontSize: 10, + ), + ), + TextSpan( + text: '12 ', + style: TextStyle( + fontSize: 12, + ), + ), + TextSpan( + text: '13 ', + style: TextStyle( + fontSize: 13, + ), + ), + TextSpan( + text: '14 ', + style: TextStyle( + fontSize: 14, + ), + ), + TextSpan( + text: '16 ', + style: TextStyle( + fontSize: 16, + ), + ), + TextSpan( + text: '18 ', + style: TextStyle( + fontSize: 18, + ), + ), + TextSpan( + text: '20 ', + style: TextStyle( + fontSize: 20, + ), + ), + TextSpan( + text: '22 ', + style: TextStyle( + fontSize: 22, + ), + ), + TextSpan( + text: '24 ', + style: TextStyle( + fontSize: 24, + ), + ), + TextSpan( + text: '26 ', + style: TextStyle( + fontSize: 26, + ), + ), + TextSpan( + text: '32 ', + style: TextStyle( + fontSize: 32, + ), + ), + TextSpan( + text: '36 ', + style: TextStyle( + fontSize: 36, + ), + ), + TextSpan( + text: '42 ', + style: TextStyle( + fontSize: 42, + ), + ), + TextSpan( + text: '48 ', + style: TextStyle( + fontSize: 48, + ), + ), + TextSpan( + text: '52 ', + style: TextStyle( + fontSize: 52, + ), + ), + TextSpan( + text: '64', + style: TextStyle( + fontSize: 64, + ), + ), + ], + ), + ), + SizedBox(height: 10), + Text( + 'Emojis 👍 ❤️ 🎉 💩 ✌️', + style: TextStyle(fontSize: 48), + ), + ], + ), + ), + ), + ); + } +} + +class _TolerantGoldenFileComparator extends LocalFileComparator { + _TolerantGoldenFileComparator( + super.testFile, { + required double precisionTolerance, + }) : _precisionTolerance = precisionTolerance; + + /// How much the golden image can differ from the test image. + /// + /// It is expected to be between 0 and 1. Where 0 is no difference (the same image) + /// and 1 is the maximum difference (completely different images). + final double _precisionTolerance; + + @override + Future compare(Uint8List imageBytes, Uri golden) async { + final result = await GoldenFileComparator.compareLists( + imageBytes, + await getGoldenBytes(golden), + ); + + final passed = result.passed || result.diffPercent <= _precisionTolerance; + if (passed) { + if (result.diffPercent > 0.0) { + debugPrint( + 'The golden file $golden has a difference\n' + 'of ${result.diffPercent * 100}%\n' + 'which is within the tolerance of ${_precisionTolerance * 100}%.', + ); + } + return true; + } + + final error = await generateFailureOutput(result, golden, basedir); + throw FlutterError(error); + } +} diff --git a/test/fonts/templates/default_font/pubspec_template.yaml b/test/fonts/templates/default_font/pubspec_template.yaml new file mode 100644 index 00000000..822410e3 --- /dev/null +++ b/test/fonts/templates/default_font/pubspec_template.yaml @@ -0,0 +1,18 @@ +name: default_font +description: "An test app using the default font" +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + flutter_test: + sdk: flutter + spot: + path: ../../../../ + +dev_dependencies: + test: ^1.24.0 diff --git a/test/fonts/templates/default_font/test/golden.png b/test/fonts/templates/default_font/test/golden.png new file mode 100644 index 00000000..c92d9fed Binary files /dev/null and b/test/fonts/templates/default_font/test/golden.png differ diff --git a/test/fonts/templates/default_font/test/test.dart b/test/fonts/templates/default_font/test/test.dart new file mode 100644 index 00000000..a3a90830 --- /dev/null +++ b/test/fonts/templates/default_font/test/test.dart @@ -0,0 +1,365 @@ +// ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables + +import 'dart:typed_data'; +// ignore: unnecessary_import +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; + +void main() { + testWidgets('Roboto is from SDK when nothing else is defined (default)', + (WidgetTester tester) async { + final previousGoldenFileComparator = goldenFileComparator; + goldenFileComparator = _TolerantGoldenFileComparator( + Uri.parse('test/test_test.dart'), + precisionTolerance: 0.10, + ); + addTearDown(() => goldenFileComparator = previousGoldenFileComparator); + await loadAppFonts(); + await tester.pumpWidget( + const MaterialApp( + debugShowCheckedModeBanner: false, + home: FontTestWidget(fontFamily: 'Roboto'), + ), + ); + await tester.pumpAndSettle(); + await expectLater( + find.byType(MaterialApp), + matchesGoldenFile('golden.png'), + ); + }); +} + +class FontTestWidget extends StatelessWidget { + const FontTestWidget({ + super.key, + this.fontFamily, + this.fontFamilyFallback = const [], + }); + + final String? fontFamily; + final List fontFamilyFallback; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: DefaultTextStyle( + style: TextStyle( + inherit: false, + height: 1.2, + letterSpacing: 0.0, + fontSize: 52, + color: Colors.black, + fontFamilyFallback: fontFamilyFallback, + fontFamily: fontFamily, + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Default Font', + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'thin', + style: TextStyle( + fontWeight: FontWeight.w100, + ), + ), + SizedBox(width: 8), + Text( + 'extra-light', + style: TextStyle( + fontWeight: FontWeight.w200, + ), + ), + SizedBox(width: 8), + Text( + 'light', + style: TextStyle( + fontWeight: FontWeight.w300, + ), + ), + SizedBox(width: 8), + Text( + 'normal', + style: TextStyle( + fontWeight: FontWeight.w400, + ), + ), + SizedBox(width: 8), + ], + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'medium', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 8), + Text( + 'semi', + style: TextStyle( + fontWeight: FontWeight.w600, + ), + ), + SizedBox(width: 8), + Text( + 'bold', + style: TextStyle( + fontWeight: FontWeight.w700, + ), + ), + SizedBox(width: 8), + Text( + 'extra', + style: TextStyle( + fontWeight: FontWeight.w800, + ), + ), + SizedBox(width: 8), + Text( + 'thick', + style: TextStyle( + fontWeight: FontWeight.w900, + ), + ), + ], + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Colored ', + style: TextStyle( + color: Colors.blue, + ), + ), + Text( + 'Text ', + style: TextStyle( + color: Colors.indigo, + ), + ), + Text( + 'Rocks', + style: TextStyle( + color: Colors.purple, + ), + ), + ], + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Italic', + style: TextStyle( + fontStyle: FontStyle.italic, + ), + ), + SizedBox(width: 8), + Text( + 'Underlined', + style: TextStyle( + decoration: TextDecoration.underline, + ), + ), + SizedBox(width: 8), + Text( + 'ItalicBold', + style: TextStyle( + fontStyle: FontStyle.italic, + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + ), + ), + ], + ), + SizedBox(height: 10), + Text.rich( + TextSpan( + style: TextStyle( + fontFeatures: [ + FontFeature.liningFigures(), + ], + ), + children: [ + TextSpan( + text: '6 ', + style: TextStyle( + fontSize: 6, + ), + ), + TextSpan( + text: '8 ', + style: TextStyle( + fontSize: 8, + ), + ), + TextSpan( + text: '10 ', + style: TextStyle( + fontSize: 10, + ), + ), + TextSpan( + text: '11 ', + style: TextStyle( + fontSize: 10, + ), + ), + TextSpan( + text: '12 ', + style: TextStyle( + fontSize: 12, + ), + ), + TextSpan( + text: '13 ', + style: TextStyle( + fontSize: 13, + ), + ), + TextSpan( + text: '14 ', + style: TextStyle( + fontSize: 14, + ), + ), + TextSpan( + text: '16 ', + style: TextStyle( + fontSize: 16, + ), + ), + TextSpan( + text: '18 ', + style: TextStyle( + fontSize: 18, + ), + ), + TextSpan( + text: '20 ', + style: TextStyle( + fontSize: 20, + ), + ), + TextSpan( + text: '22 ', + style: TextStyle( + fontSize: 22, + ), + ), + TextSpan( + text: '24 ', + style: TextStyle( + fontSize: 24, + ), + ), + TextSpan( + text: '26 ', + style: TextStyle( + fontSize: 26, + ), + ), + TextSpan( + text: '32 ', + style: TextStyle( + fontSize: 32, + ), + ), + TextSpan( + text: '36 ', + style: TextStyle( + fontSize: 36, + ), + ), + TextSpan( + text: '42 ', + style: TextStyle( + fontSize: 42, + ), + ), + TextSpan( + text: '48 ', + style: TextStyle( + fontSize: 48, + ), + ), + TextSpan( + text: '52 ', + style: TextStyle( + fontSize: 52, + ), + ), + TextSpan( + text: '64', + style: TextStyle( + fontSize: 64, + ), + ), + ], + ), + ), + SizedBox(height: 10), + Text( + 'Emojis 👍 ❤️ 🎉 💩 ✌️', + style: TextStyle(fontSize: 48), + ), + ], + ), + ), + ), + ); + } +} + +class _TolerantGoldenFileComparator extends LocalFileComparator { + _TolerantGoldenFileComparator( + super.testFile, { + required double precisionTolerance, + }) : _precisionTolerance = precisionTolerance; + + /// How much the golden image can differ from the test image. + /// + /// It is expected to be between 0 and 1. Where 0 is no difference (the same image) + /// and 1 is the maximum difference (completely different images). + final double _precisionTolerance; + + @override + Future compare(Uint8List imageBytes, Uri golden) async { + final result = await GoldenFileComparator.compareLists( + imageBytes, + await getGoldenBytes(golden), + ); + + final passed = result.passed || result.diffPercent <= _precisionTolerance; + if (passed) { + if (result.diffPercent > 0.0) { + debugPrint( + 'The golden file $golden has a difference\n' + 'of ${result.diffPercent * 100}%\n' + 'which is within the tolerance of ${_precisionTolerance * 100}%.', + ); + } + return true; + } + + final error = await generateFailureOutput(result, golden, basedir); + throw FlutterError(error); + } +} diff --git a/test/fonts/templates/dependency_font/pubspec_template.yaml b/test/fonts/templates/dependency_font/pubspec_template.yaml new file mode 100644 index 00000000..3292692d --- /dev/null +++ b/test/fonts/templates/dependency_font/pubspec_template.yaml @@ -0,0 +1,21 @@ +name: dependency_font +description: "A test app using a font defined in a dependency package" +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + flutter_test: + sdk: flutter + spot: + path: ../../../../ + app_font: + path: packages/app_font + + +dev_dependencies: + test: ^1.24.0 diff --git a/test/fonts/templates/dependency_font/test/golden.png b/test/fonts/templates/dependency_font/test/golden.png new file mode 100644 index 00000000..a921f7b4 Binary files /dev/null and b/test/fonts/templates/dependency_font/test/golden.png differ diff --git a/test/fonts/templates/dependency_font/test/test.dart b/test/fonts/templates/dependency_font/test/test.dart new file mode 100644 index 00000000..db798bf8 --- /dev/null +++ b/test/fonts/templates/dependency_font/test/test.dart @@ -0,0 +1,367 @@ +// ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables + +import 'dart:typed_data'; +// ignore: unnecessary_import +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spot/spot.dart'; + +void main() { + testWidgets('Montserrat is loaded from a third party package', + (WidgetTester tester) async { + final previousGoldenFileComparator = goldenFileComparator; + goldenFileComparator = _TolerantGoldenFileComparator( + Uri.parse('test/test_test.dart'), + precisionTolerance: 0.10, + ); + addTearDown(() => goldenFileComparator = previousGoldenFileComparator); + await loadAppFonts(); + await tester.pumpWidget( + const MaterialApp( + debugShowCheckedModeBanner: false, + home: FontTestWidget( + fontFamily: 'Montserrat', + ), + ), + ); + await tester.pumpAndSettle(); + await expectLater( + find.byType(MaterialApp), + matchesGoldenFile('golden.png'), + ); + }); +} + +class FontTestWidget extends StatelessWidget { + const FontTestWidget({ + super.key, + this.fontFamily, + this.fontFamilyFallback = const [], + }); + + final String? fontFamily; + final List fontFamilyFallback; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: DefaultTextStyle( + style: TextStyle( + inherit: false, + height: 1.2, + letterSpacing: 0.0, + fontSize: 52, + color: Colors.black, + fontFamilyFallback: fontFamilyFallback, + fontFamily: fontFamily, + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Default Font', + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'thin', + style: TextStyle( + fontWeight: FontWeight.w100, + ), + ), + SizedBox(width: 8), + Text( + 'extra-light', + style: TextStyle( + fontWeight: FontWeight.w200, + ), + ), + SizedBox(width: 8), + Text( + 'light', + style: TextStyle( + fontWeight: FontWeight.w300, + ), + ), + SizedBox(width: 8), + Text( + 'normal', + style: TextStyle( + fontWeight: FontWeight.w400, + ), + ), + SizedBox(width: 8), + ], + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'medium', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 8), + Text( + 'semi', + style: TextStyle( + fontWeight: FontWeight.w600, + ), + ), + SizedBox(width: 8), + Text( + 'bold', + style: TextStyle( + fontWeight: FontWeight.w700, + ), + ), + SizedBox(width: 8), + Text( + 'extra', + style: TextStyle( + fontWeight: FontWeight.w800, + ), + ), + SizedBox(width: 8), + Text( + 'thick', + style: TextStyle( + fontWeight: FontWeight.w900, + ), + ), + ], + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Colored ', + style: TextStyle( + color: Colors.blue, + ), + ), + Text( + 'Text ', + style: TextStyle( + color: Colors.indigo, + ), + ), + Text( + 'Rocks', + style: TextStyle( + color: Colors.purple, + ), + ), + ], + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Italic', + style: TextStyle( + fontStyle: FontStyle.italic, + ), + ), + SizedBox(width: 8), + Text( + 'Underlined', + style: TextStyle( + decoration: TextDecoration.underline, + ), + ), + SizedBox(width: 8), + Text( + 'ItalicBold', + style: TextStyle( + fontStyle: FontStyle.italic, + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + ), + ), + ], + ), + SizedBox(height: 10), + Text.rich( + TextSpan( + style: TextStyle( + fontFeatures: [ + FontFeature.liningFigures(), + ], + ), + children: [ + TextSpan( + text: '6 ', + style: TextStyle( + fontSize: 6, + ), + ), + TextSpan( + text: '8 ', + style: TextStyle( + fontSize: 8, + ), + ), + TextSpan( + text: '10 ', + style: TextStyle( + fontSize: 10, + ), + ), + TextSpan( + text: '11 ', + style: TextStyle( + fontSize: 10, + ), + ), + TextSpan( + text: '12 ', + style: TextStyle( + fontSize: 12, + ), + ), + TextSpan( + text: '13 ', + style: TextStyle( + fontSize: 13, + ), + ), + TextSpan( + text: '14 ', + style: TextStyle( + fontSize: 14, + ), + ), + TextSpan( + text: '16 ', + style: TextStyle( + fontSize: 16, + ), + ), + TextSpan( + text: '18 ', + style: TextStyle( + fontSize: 18, + ), + ), + TextSpan( + text: '20 ', + style: TextStyle( + fontSize: 20, + ), + ), + TextSpan( + text: '22 ', + style: TextStyle( + fontSize: 22, + ), + ), + TextSpan( + text: '24 ', + style: TextStyle( + fontSize: 24, + ), + ), + TextSpan( + text: '26 ', + style: TextStyle( + fontSize: 26, + ), + ), + TextSpan( + text: '32 ', + style: TextStyle( + fontSize: 32, + ), + ), + TextSpan( + text: '36 ', + style: TextStyle( + fontSize: 36, + ), + ), + TextSpan( + text: '42 ', + style: TextStyle( + fontSize: 42, + ), + ), + TextSpan( + text: '48 ', + style: TextStyle( + fontSize: 48, + ), + ), + TextSpan( + text: '52 ', + style: TextStyle( + fontSize: 52, + ), + ), + TextSpan( + text: '64', + style: TextStyle( + fontSize: 64, + ), + ), + ], + ), + ), + SizedBox(height: 10), + Text( + 'Emojis 👍 ❤️ 🎉 💩 ✌️', + style: TextStyle(fontSize: 48), + ), + ], + ), + ), + ), + ); + } +} + +class _TolerantGoldenFileComparator extends LocalFileComparator { + _TolerantGoldenFileComparator( + super.testFile, { + required double precisionTolerance, + }) : _precisionTolerance = precisionTolerance; + + /// How much the golden image can differ from the test image. + /// + /// It is expected to be between 0 and 1. Where 0 is no difference (the same image) + /// and 1 is the maximum difference (completely different images). + final double _precisionTolerance; + + @override + Future compare(Uint8List imageBytes, Uri golden) async { + final result = await GoldenFileComparator.compareLists( + imageBytes, + await getGoldenBytes(golden), + ); + + final passed = result.passed || result.diffPercent <= _precisionTolerance; + if (passed) { + if (result.diffPercent > 0.0) { + debugPrint( + 'The golden file $golden has a difference\n' + 'of ${result.diffPercent * 100}%\n' + 'which is within the tolerance of ${_precisionTolerance * 100}%.', + ); + } + return true; + } + + final error = await generateFailureOutput(result, golden, basedir); + throw FlutterError(error); + } +} diff --git a/test/timeline/tap/act_tap_timeline_test_bodies.dart b/test/timeline/tap/act_tap_timeline_test_bodies.dart index 6c39d765..7906947d 100644 --- a/test/timeline/tap/act_tap_timeline_test_bodies.dart +++ b/test/timeline/tap/act_tap_timeline_test_bodies.dart @@ -91,20 +91,16 @@ Example: timeline.mode = $globalTimelineModeToSwitch; 'Details: Tap Icon Widget with icon: "IconData(U+0E047)"', ); expect( - timeline[start + 2].startsWith('Caller: at'), - isTrue, + timeline[start + 2], + startsWith('Caller: at'), ); expect( - timeline[start + 3].startsWith( - 'Screenshot: file:///', - ), - isTrue, + timeline[start + 3], + startsWith('Screenshot: file://'), ); expect( - timeline[start + 4].startsWith( - 'Timestamp:', - ), - isTrue, + timeline[start + 4], + startsWith('Timestamp:'), ); expect( timeline[start + 5], @@ -144,20 +140,16 @@ Example: timeline.mode = $globalTimelineModeToSwitch; 'Details: Tap Icon Widget with icon: "IconData(U+0E047)"', ); expect( - timeline[start + 2].startsWith('Caller: at'), - isTrue, + timeline[start + 2], + startsWith('Caller: at'), ); expect( - timeline[start + 3].startsWith( - 'Screenshot: file:///', - ), - isTrue, + timeline[start + 3], + startsWith('Screenshot: file://'), ); expect( - timeline[start + 4].startsWith( - 'Timestamp:', - ), - isTrue, + timeline[start + 4], + startsWith('Timestamp:'), ); expect( timeline[start + 5], @@ -199,20 +191,16 @@ Example: timeline.mode = $globalTimelineModeToSwitch; 'Details: Tap Icon Widget with icon: "IconData(U+0E516)"', ); expect( - timeline[start + 2].startsWith('Caller: at'), - isTrue, + timeline[start + 2], + startsWith('Caller: at'), ); expect( - timeline[start + 3].startsWith( - 'Screenshot: file:///', - ), - isTrue, + timeline[start + 3], + startsWith('Screenshot: file://'), ); expect( - timeline[start + 4].startsWith( - 'Timestamp:', - ), - isTrue, + timeline[start + 4], + startsWith('Timestamp:'), ); expect( timeline[start + 5], diff --git a/test/util/run_test_in_process.dart b/test/util/run_test_in_process.dart index b90c33d5..994c18a6 100644 --- a/test/util/run_test_in_process.dart +++ b/test/util/run_test_in_process.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:spot/src/flutter/flutter_sdk.dart'; import 'package:test/test.dart'; import 'package:test_process/test_process.dart'; @@ -25,13 +26,6 @@ Future runTestInProcessAndCaptureOutPut({ ...?args?.where((arg) => arg != 'test'), ]; - // Get the path to the Flutter executable the test was started with (not from PATH) - // /Users/pascalwelsch/.puro/envs/3.16.9/flutter/bin/cache/artifacts/engine/darwin-x64/flutter_tester - final flutterTesterExe = Platform.executable; - final binDir = flutterTesterExe.split('/cache/')[0]; - final flutterExe = - Platform.isWindows ? '$binDir\\flutter.exe' : '$binDir/flutter'; - printOnFailure('$flutterExe ${arguments.join(' ')}'); final testProcess = await TestProcess.start(