From 78a2506c886e246a95c31b0b4e7ea49d79022e3a Mon Sep 17 00:00:00 2001 From: Jonas Klock Date: Fri, 13 Jun 2025 09:24:52 +0200 Subject: [PATCH 1/3] [FTD #6] add padding to box painters Signed-off-by: Jonas Klock --- .../screens/examples/box_example_screen.dart | 21 ++++++----- lib/flutter_text_decorator.dart | 3 +- lib/src/modules/box/enums/box_style.dart | 17 ++++++--- .../box/painter/bubble_box_painter.dart | 34 ++++++++++-------- .../box/painter/rounded_box_painter.dart | 26 ++++++++++++-- .../modules/box/painter/wavy_box_painter.dart | 36 +++++++++++-------- ...raw_decorator.dart => text_decorator.dart} | 4 +-- 7 files changed, 91 insertions(+), 50 deletions(-) rename lib/src/widgets/{text_draw_decorator.dart => text_decorator.dart} (98%) diff --git a/example/lib/screens/examples/box_example_screen.dart b/example/lib/screens/examples/box_example_screen.dart index 7a31c36..2dbf4df 100644 --- a/example/lib/screens/examples/box_example_screen.dart +++ b/example/lib/screens/examples/box_example_screen.dart @@ -46,8 +46,8 @@ class BoxExampleScreen extends StatelessWidget { TextDecorator.boxed( style: BoxStyle.bubble, text: const Text( - 'Bubble Text 2', - style: TextStyle(fontSize: 32), + 'Franz jagt im komplett verwahrlosten Taxi quer durch Berlin', + style: TextStyle(fontSize: 16), ), strokeWidth: 2, borderRadius: 16, @@ -63,14 +63,17 @@ class BoxExampleScreen extends StatelessWidget { borderRadius: 16, ), const SizedBox(height: 32), - TextDecorator.boxed( - style: BoxStyle.curled, - text: const Text( - 'Franz jagt im komplett verwahrlosten Taxi quer durch Berlin', - style: TextStyle(fontSize: 16), + Padding( + padding: const EdgeInsets.all(16.0), + child: TextDecorator.boxed( + style: BoxStyle.curled, + text: const Text( + 'Franz jagt im komplett verwahrlosten Taxi quer durch Berlin', + style: TextStyle(fontSize: 16), + ), + strokeWidth: 2, + borderRadius: 16, ), - strokeWidth: 2, - borderRadius: 16, ), ], ), diff --git a/lib/flutter_text_decorator.dart b/lib/flutter_text_decorator.dart index f22af01..0eb28e8 100644 --- a/lib/flutter_text_decorator.dart +++ b/lib/flutter_text_decorator.dart @@ -1,3 +1,2 @@ - export 'src/modules/base/styles.dart'; -export 'src/widgets/text_draw_decorator.dart'; +export 'src/widgets/text_decorator.dart'; diff --git a/lib/src/modules/box/enums/box_style.dart b/lib/src/modules/box/enums/box_style.dart index 2e61dcf..4a1298d 100644 --- a/lib/src/modules/box/enums/box_style.dart +++ b/lib/src/modules/box/enums/box_style.dart @@ -21,22 +21,31 @@ enum BoxStyle { /// - `strokeWidth`: Intended for the thickness of the box outline. /// /// See individual enum value documentation for notes on parameter usage for specific styles. - CustomPainter getPainter(Text text, double borderRadius, double strokeWidth) { + CustomPainter getPainter(Text text, double borderRadius, double strokeWidth, EdgeInsets padding) { switch (this) { case BoxStyle.rounded: - return RoundedBoxPainter(text: text, borderRadius: borderRadius, strokeWidth: strokeWidth); + return RoundedBoxPainter( + text: text, + borderRadius: borderRadius, + strokeWidth: strokeWidth, + padding: padding, + ); case BoxStyle.bubble: return BubbleBoxPainter( text: text, - padding: 4, bubbleColor: Colors.orange, tip: const BubbleBoxTip( position: TipPosition.left, orientation: TipOrientation.left, ), + padding: padding, ); case BoxStyle.curled: - return WavyBoxPainter(text: text, borderColor: Colors.black); + return WavyBoxPainter( + text: text, + borderColor: Colors.black, + padding: padding, + ); } } } diff --git a/lib/src/modules/box/painter/bubble_box_painter.dart b/lib/src/modules/box/painter/bubble_box_painter.dart index ff14d8e..bbf4be5 100644 --- a/lib/src/modules/box/painter/bubble_box_painter.dart +++ b/lib/src/modules/box/painter/bubble_box_painter.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_text_decorator/src/modules/box/decorations/bubble_box_tip.dart'; @@ -22,14 +24,14 @@ import 'package:flutter_text_decorator/src/modules/box/decorations/bubble_box_ti class BubbleBoxPainter extends CustomPainter { BubbleBoxPainter({ required this.text, - required this.padding, required this.bubbleColor, + required this.padding, super.repaint, this.borderRadius = 8, this.tip = const BubbleBoxTip(), }); final Text text; - final double padding; //! TODO: fix text not being centered + final EdgeInsets padding; //! TODO: fix text not being centered final Color bubbleColor; final double borderRadius; final BubbleBoxTip tip; @@ -48,11 +50,13 @@ class BubbleBoxPainter extends CustomPainter { )..layout(); final textWidth = textPainter.width; + final availableWidth = min(textWidth, size.width); final textHeight = textPainter.height; + final availableHeight = max(textHeight, size.height); // Calculate bubble size - final bubbleWidth = textWidth + padding * 2; - final bubbleHeight = textHeight + padding * 2; + final bubbleWidth = availableWidth + padding.horizontal; + final bubbleHeight = availableHeight + padding.vertical; // Calculate tail size //! TODO: extract @@ -61,16 +65,16 @@ class BubbleBoxPainter extends CustomPainter { final path = Path() // Top left corner - ..moveTo(0, borderRadius) - ..quadraticBezierTo(0, 0, borderRadius, 0) + ..moveTo(0 - padding.left, borderRadius - padding.top) + ..quadraticBezierTo(0 - padding.left, 0 - padding.top, borderRadius - padding.left, 0 - padding.top) // Top right corner - ..lineTo(bubbleWidth - borderRadius, 0) - ..quadraticBezierTo(bubbleWidth, 0, bubbleWidth, borderRadius) + ..lineTo(bubbleWidth - borderRadius - padding.left, 0 - padding.top) + ..quadraticBezierTo(bubbleWidth - padding.left, 0 - padding.top, bubbleWidth - padding.left, borderRadius - padding.top) // Bottom right corner - ..lineTo(bubbleWidth, bubbleHeight - borderRadius) - ..quadraticBezierTo(bubbleWidth, bubbleHeight, bubbleWidth - borderRadius, bubbleHeight); + ..lineTo(bubbleWidth - padding.left, bubbleHeight - borderRadius - padding.top) + ..quadraticBezierTo(bubbleWidth - padding.left, bubbleHeight - padding.top, bubbleWidth - borderRadius - padding.left, bubbleHeight - padding.top); final tipOffset = bubbleWidth * 0.05; @@ -84,11 +88,11 @@ class BubbleBoxPainter extends CustomPainter { // Bottom left corner with tail path - ..lineTo(tipStart, bubbleHeight) - ..lineTo(tipPeak, bubbleHeight + tailHeight) - ..lineTo(tipEnd, bubbleHeight) - ..lineTo(borderRadius, bubbleHeight) - ..quadraticBezierTo(0, bubbleHeight, 0, bubbleHeight - borderRadius) + ..lineTo(tipStart, bubbleHeight - padding.top) + ..lineTo(tipPeak, bubbleHeight + tailHeight - padding.top) + ..lineTo(tipEnd, bubbleHeight - padding.top) + ..lineTo(borderRadius - padding.left, bubbleHeight - padding.top) + ..quadraticBezierTo(0 - padding.left, bubbleHeight - padding.top, 0 - padding.left, bubbleHeight - borderRadius - padding.top) ..close(); // Draw the bubble diff --git a/lib/src/modules/box/painter/rounded_box_painter.dart b/lib/src/modules/box/painter/rounded_box_painter.dart index 9e685d6..ebc2028 100644 --- a/lib/src/modules/box/painter/rounded_box_painter.dart +++ b/lib/src/modules/box/painter/rounded_box_painter.dart @@ -30,11 +30,13 @@ class RoundedBoxPainter extends CustomPainter { required this.text, required this.borderRadius, required this.strokeWidth, + required this.padding, super.repaint, }); final Text text; final double borderRadius; final double strokeWidth; + final EdgeInsets padding; @override void paint(Canvas canvas, Size size) { @@ -59,13 +61,31 @@ class RoundedBoxPainter extends CustomPainter { final nLines = heightFactor.ceil(); boxHeight = nLines * textHeight; + double getOffsetDx() { + var dx = size.width / 2; + if (padding.left != padding.right) { + final diff = (padding.left - padding.right).abs() / 2; + dx = padding.left < padding.right ? dx + diff : dx - diff; + } + return dx; + } + + double getOffsetDy() { + var dy = size.height / 2; + if (padding.top != padding.bottom) { + final diff = (padding.top - padding.bottom).abs() / 2; + dy = padding.top < padding.bottom ? dy + diff : dy - diff; + } + return dy; + } + final centerOffset = Offset( - size.width / 2, - size.height / 2, + getOffsetDx(), + getOffsetDy(), ); final rrect = RRect.fromRectAndRadius( - Rect.fromCenter(center: centerOffset, width: boxWidth, height: boxHeight), + Rect.fromCenter(center: centerOffset, width: boxWidth + padding.horizontal, height: boxHeight + padding.vertical), Radius.circular(borderRadius), ); diff --git a/lib/src/modules/box/painter/wavy_box_painter.dart b/lib/src/modules/box/painter/wavy_box_painter.dart index da891cd..d81cee2 100644 --- a/lib/src/modules/box/painter/wavy_box_painter.dart +++ b/lib/src/modules/box/painter/wavy_box_painter.dart @@ -36,12 +36,18 @@ class WavyBoxPainter extends CustomPainter { WavyBoxPainter({ required this.text, required this.borderColor, + required this.padding, + this.nHorizontalSegments = 12, + this.nVerticalSegments = 3, super.repaint, }); // TODO(everyone): add padding? // TODO(everyone): add fill color? final Text text; final Color borderColor; + final EdgeInsets padding; + final int nHorizontalSegments; + final int nVerticalSegments; @override void paint(Canvas canvas, Size size) { @@ -63,44 +69,44 @@ class WavyBoxPainter extends CustomPainter { final nLines = heightFactor.ceil(); textHeight = nLines * textHeight; - const nHorizontalSegments = 10; - final availableWidth = min(textWidth, size.width); - final lengthSegment = (availableWidth / nHorizontalSegments).ceil(); - final nVerticalSegments = (textHeight / lengthSegment).ceil(); + final availableWidth = min(textWidth, size.width) + padding.horizontal; + final availableHeight = max(textHeight, size.height) + padding.vertical; + final widthHorizontalSegment = availableWidth / nHorizontalSegments; + final heightVerticalSegment = availableHeight / nVerticalSegments; const arcHeight = -10.0; const weight = 1.0; - var lastX2 = 0.0; + var lastX2 = 0.0 - padding.left; // TODO(everyone): Fix corners, maybe with [arcTo]? - final path = Path()..moveTo(0, 0); + final path = Path()..moveTo(0 - padding.left, 0 - padding.top); // Upper left to upper right for (var i = 1; i <= nHorizontalSegments; i++) { - final x2 = i * lengthSegment / 1.0; - path.conicTo(lastX2 + lengthSegment / 2, arcHeight, x2 / 1.0, 0, weight); + final x2 = (i * widthHorizontalSegment / 1.0) - padding.left; + path.conicTo(lastX2 + widthHorizontalSegment / 2, arcHeight - padding.top, x2 / 1.0, 0 - padding.top, weight); lastX2 = x2; } // Upper right to lower right - double lastY2 = 0; + var lastY2 = 0 - padding.top; for (var i = 1; i <= nVerticalSegments; i++) { - final y2 = i * lengthSegment / 1.0; - path.conicTo(lastX2 - arcHeight, lastY2 + lengthSegment / 2, lastX2, y2, weight); + final y2 = (i * heightVerticalSegment / 1.0) - padding.top; + path.conicTo(lastX2 - arcHeight, lastY2 + heightVerticalSegment / 2, lastX2, y2, weight); lastY2 = y2; } // Lower right to lower left for (var i = nHorizontalSegments - 1; i >= 0; i--) { - final x2 = i * lengthSegment / 1.0; - path.conicTo(lastX2 - lengthSegment / 2, lastY2 - arcHeight, x2 / 1.0, lastY2, weight); + final x2 = (i * widthHorizontalSegment / 1.0) - padding.left; + path.conicTo(lastX2 - widthHorizontalSegment / 2, lastY2 - arcHeight, x2 / 1.0, lastY2, weight); lastX2 = x2; } // Lower left to upper right for (var i = nVerticalSegments - 1; i >= 0; i--) { - final y2 = i * lengthSegment / 1.0; - path.conicTo(arcHeight, y2 + lengthSegment / 2, lastX2, y2, weight); + final y2 = (i * heightVerticalSegment / 1.0) - padding.top; + path.conicTo(arcHeight - padding.left, y2 + heightVerticalSegment / 2, 0 - padding.left, y2, weight); lastY2 = y2; } diff --git a/lib/src/widgets/text_draw_decorator.dart b/lib/src/widgets/text_decorator.dart similarity index 98% rename from lib/src/widgets/text_draw_decorator.dart rename to lib/src/widgets/text_decorator.dart index 2fd74b1..01bdcb4 100644 --- a/lib/src/widgets/text_draw_decorator.dart +++ b/lib/src/widgets/text_decorator.dart @@ -41,7 +41,6 @@ import 'package:flutter_text_decorator/src/modules/underline/enums/underline_sty /// ``` /// class TextDecorator extends StatelessWidget { - const TextDecorator({ required this.text, required this.painter, @@ -131,10 +130,11 @@ class TextDecorator extends StatelessWidget { BoxStyle style = BoxStyle.bubble, double borderRadius = 1, double strokeWidth = 1, + EdgeInsets padding = const EdgeInsets.all(8), }) { return TextDecorator( text: text, - painter: style.getPainter(text, borderRadius, strokeWidth), + painter: style.getPainter(text, borderRadius, strokeWidth, padding), ); } From a9e720769c1b40605fe1c9d2fe5cdc86017a725d Mon Sep 17 00:00:00 2001 From: Jonas Klock Date: Fri, 13 Jun 2025 09:24:34 +0200 Subject: [PATCH 2/3] [FTD #6] add golden tests for box painters Signed-off-by: Jonas Klock --- .github/workflows/basic_dart_check.yml | 4 + dart_test.yaml | 2 + .../text_decorator/box/bubble.default.png | Bin 0 -> 5680 bytes .../text_decorator/box/curled.default.png | Bin 0 -> 9587 bytes .../text_decorator/box/rounded.default.png | Bin 0 -> 4185 bytes test/src/widgets/text_decorator_test.dart | 87 ++++++++++++++++++ test/test_utils/test_util.dart | 8 ++ 7 files changed, 101 insertions(+) create mode 100644 dart_test.yaml create mode 100644 test/src/widgets/text_decorator/box/bubble.default.png create mode 100644 test/src/widgets/text_decorator/box/curled.default.png create mode 100644 test/src/widgets/text_decorator/box/rounded.default.png create mode 100644 test/src/widgets/text_decorator_test.dart create mode 100644 test/test_utils/test_util.dart diff --git a/.github/workflows/basic_dart_check.yml b/.github/workflows/basic_dart_check.yml index 8c98cb1..b5cb4e5 100644 --- a/.github/workflows/basic_dart_check.yml +++ b/.github/workflows/basic_dart_check.yml @@ -15,6 +15,10 @@ jobs: - name: 🛠️ Set up Flutter uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: 3.27.1 + - run: flutter --version - name: ⚙️ Install dependencies run: flutter pub get diff --git a/dart_test.yaml b/dart_test.yaml new file mode 100644 index 0000000..f01b955 --- /dev/null +++ b/dart_test.yaml @@ -0,0 +1,2 @@ +tags: + golden: diff --git a/test/src/widgets/text_decorator/box/bubble.default.png b/test/src/widgets/text_decorator/box/bubble.default.png new file mode 100644 index 0000000000000000000000000000000000000000..cc399d6a9e645dd6638472dd23e4ff22a8016028 GIT binary patch literal 5680 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 a#em@6h?=Z?TtAQU3_#L>lmux!z2rB`&B{Fh literal 0 HcmV?d00001 diff --git a/test/src/widgets/text_decorator_test.dart b/test/src/widgets/text_decorator_test.dart new file mode 100644 index 0000000..4ff1f43 --- /dev/null +++ b/test/src/widgets/text_decorator_test.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_text_decorator/flutter_text_decorator.dart'; + +import '../../test_utils/test_util.dart'; + +const _text = Text( + testText, + style: TextStyle(fontSize: 16), +); +const _stokeWidth = 2.0; + +Widget _createCenter(Widget child) => Center(child: Padding(padding: const EdgeInsets.all(32), child: child)); + +void main() { + group('TextDecorator tests', () { + group('Testing TextDecorator.boxed styles', () { + testWidgets( + 'BoxStyle.bubble renders', + (tester) async { + // arrange + final widget = TextDecorator.boxed( + text: _text, + strokeWidth: _stokeWidth, + ); + + // act + await tester.pumpWidget(createTestApp(_createCenter(widget))); + await tester.pumpAndSettle(); + + // assert + await expectLater( + find.byType(Text), + matchesGoldenFile('text_decorator/box/bubble.default.png'), + ); + }, + tags: ['golden'], + ); + testWidgets( + 'BoxStyle.rounded renders', + (tester) async { + // arrange + final widget = TextDecorator.boxed( + style: BoxStyle.rounded, + text: _text, + strokeWidth: _stokeWidth, + borderRadius: 2, + ); + + // act + await tester.pumpWidget(createTestApp(_createCenter(widget))); + await tester.pumpAndSettle(); + + // assert + await expectLater( + find.byType(Text), + matchesGoldenFile('text_decorator/box/rounded.default.png'), + ); + }, + tags: ['golden'], + ); + + testWidgets( + 'BoxStyle.curled renders', + (tester) async { + // arrange + final widget = TextDecorator.boxed( + style: BoxStyle.curled, + text: _text, + strokeWidth: _stokeWidth, + ); + + // act + await tester.pumpWidget(createTestApp(_createCenter(widget))); + await tester.pumpAndSettle(); + + // assert + await expectLater( + find.byType(Text), + matchesGoldenFile('text_decorator/box/curled.default.png'), + ); + }, + tags: ['golden'], + ); + }); + }); +} diff --git a/test/test_utils/test_util.dart b/test/test_utils/test_util.dart new file mode 100644 index 0000000..65fcce8 --- /dev/null +++ b/test/test_utils/test_util.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +const testText = 'Franz jagt im komplett verwahrlosten Taxi quer durch Berlin'; + +Widget createTestApp(Widget child) => MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold(body: child), + ); From 5e48c80318bfbe056eaf1a8a27381e568d72d83f Mon Sep 17 00:00:00 2001 From: Tobias Rump Date: Fri, 13 Jun 2025 11:32:42 +0200 Subject: [PATCH 3/3] [FTD #6] add golden test file comparer for threshold --- .github/workflows/basic_dart_check.yml | 2 +- dart_test.yaml | 2 - .../box/painter/bubble_box_painter.dart | 4 +- .../box/painter/rounded_box_painter.dart | 46 ++++++++---------- test/flutter_test_config.dart | 25 ++++++++++ .../text_decorator/box/bubble.default.png | Bin 5680 -> 5679 bytes .../text_decorator/box/curled.default.png | Bin 9587 -> 9740 bytes .../text_decorator/box/rounded.default.png | Bin 4185 -> 4196 bytes test/src/widgets/text_decorator_test.dart | 3 -- test/utils/local_file_comparer.dart | 36 ++++++++++++++ 10 files changed, 85 insertions(+), 33 deletions(-) delete mode 100644 dart_test.yaml create mode 100644 test/flutter_test_config.dart create mode 100644 test/utils/local_file_comparer.dart diff --git a/.github/workflows/basic_dart_check.yml b/.github/workflows/basic_dart_check.yml index b5cb4e5..84f6128 100644 --- a/.github/workflows/basic_dart_check.yml +++ b/.github/workflows/basic_dart_check.yml @@ -17,7 +17,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.27.1 + flutter-version: 3.22.3 - run: flutter --version - name: ⚙️ Install dependencies diff --git a/dart_test.yaml b/dart_test.yaml deleted file mode 100644 index f01b955..0000000 --- a/dart_test.yaml +++ /dev/null @@ -1,2 +0,0 @@ -tags: - golden: diff --git a/lib/src/modules/box/painter/bubble_box_painter.dart b/lib/src/modules/box/painter/bubble_box_painter.dart index bbf4be5..377a504 100644 --- a/lib/src/modules/box/painter/bubble_box_painter.dart +++ b/lib/src/modules/box/painter/bubble_box_painter.dart @@ -31,7 +31,7 @@ class BubbleBoxPainter extends CustomPainter { this.tip = const BubbleBoxTip(), }); final Text text; - final EdgeInsets padding; //! TODO: fix text not being centered + final EdgeInsets padding; final Color bubbleColor; final double borderRadius; final BubbleBoxTip tip; @@ -59,7 +59,7 @@ class BubbleBoxPainter extends CustomPainter { final bubbleHeight = availableHeight + padding.vertical; // Calculate tail size - //! TODO: extract + // !TODO: extract final tailHeight = bubbleHeight * 0.25; final path = Path() diff --git a/lib/src/modules/box/painter/rounded_box_painter.dart b/lib/src/modules/box/painter/rounded_box_painter.dart index ebc2028..450dc73 100644 --- a/lib/src/modules/box/painter/rounded_box_painter.dart +++ b/lib/src/modules/box/painter/rounded_box_painter.dart @@ -44,12 +44,8 @@ class RoundedBoxPainter extends CustomPainter { ..strokeWidth = strokeWidth ..style = PaintingStyle.stroke; - // TODO(everyone): Extract and make generic final textSpan = TextSpan(text: text.data, style: text.style); - final textPainter = TextPainter( - text: textSpan, - textDirection: TextDirection.ltr, - )..layout(); + final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr)..layout(); final textWidth = textPainter.width; final boxWidth = min(textWidth, size.width); @@ -61,27 +57,9 @@ class RoundedBoxPainter extends CustomPainter { final nLines = heightFactor.ceil(); boxHeight = nLines * textHeight; - double getOffsetDx() { - var dx = size.width / 2; - if (padding.left != padding.right) { - final diff = (padding.left - padding.right).abs() / 2; - dx = padding.left < padding.right ? dx + diff : dx - diff; - } - return dx; - } - - double getOffsetDy() { - var dy = size.height / 2; - if (padding.top != padding.bottom) { - final diff = (padding.top - padding.bottom).abs() / 2; - dy = padding.top < padding.bottom ? dy + diff : dy - diff; - } - return dy; - } - final centerOffset = Offset( - getOffsetDx(), - getOffsetDy(), + _getOffsetDx(size), + _getOffsetDy(size), ); final rrect = RRect.fromRectAndRadius( @@ -92,6 +70,24 @@ class RoundedBoxPainter extends CustomPainter { canvas.drawRRect(rrect, paint); } + double _getOffsetDx(Size size) { + var dx = size.width / 2; + if (padding.left != padding.right) { + final diff = (padding.left - padding.right).abs() / 2; + dx = padding.left < padding.right ? dx + diff : dx - diff; + } + return dx; + } + + double _getOffsetDy(Size size) { + var dy = size.height / 2; + if (padding.top != padding.bottom) { + final diff = (padding.top - padding.bottom).abs() / 2; + dy = padding.top < padding.bottom ? dy + diff : dy - diff; + } + return dy; + } + @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; diff --git a/test/flutter_test_config.dart b/test/flutter_test_config.dart new file mode 100644 index 0000000..9c6573f --- /dev/null +++ b/test/flutter_test_config.dart @@ -0,0 +1,25 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; + +import 'utils/local_file_comparer.dart'; + +const _kGoldenTestsThreshold = 0.5 / 100; + +Future testExecutable(FutureOr Function() testMain) async { + if (goldenFileComparator is LocalFileComparator) { + final testUrl = (goldenFileComparator as LocalFileComparator).basedir; + + goldenFileComparator = LocalFileComparatorWithThreshold( + Uri.parse('$testUrl/test.dart'), + _kGoldenTestsThreshold, + ); + } else { + throw Exception( + 'Expected `goldenFileComparator` to be of type `LocalFileComparator`, ' + 'but it is of type `${goldenFileComparator.runtimeType}`', + ); + } + + await testMain(); +} diff --git a/test/src/widgets/text_decorator/box/bubble.default.png b/test/src/widgets/text_decorator/box/bubble.default.png index cc399d6a9e645dd6638472dd23e4ff22a8016028..a08eea164dfa0e7b65e299b726708648dd2c1b04 100644 GIT binary patch delta 2739 zcmXw4dpwkR7k{MKw$LtZ?whB3o5Ze#B6)9(BJcRs(*Ip^~|=X<{2U)^5H-h2;e^w<%P zpw!a&iO}0+E5wc|j=(6-)?vi3+wWG#-km-tE)0Y(KLTwJBoy58gulIXWyiY5$83)O zzU$$hK&zD}j`tqKfA#q2y>Dq>{`?63D!~3$vu9-W_OGL&8va@zf4k$&X1j7RD~J2? zRkxw-%$m|HPwDb*Hnn$6t>UE3ge1EfkQ#ZNXIS3=fcpzLf&u`*dD#|fy++gef=P`D z$g`5_8&Ba$CsqIta<3Ae#C)E?g1A6mUc+##AvbDdX7^zeO)UR`iTlCiopdV-lKuY!F@ zwbW`P-Brm!z%Eiji#S+WYYGbX3F6^I%2k?R03NM|U>`RNR{)qibBP}d?^CX@2@*`+ zWoNKTI{{6S6EA#_76n_%0I*8czM2@}1-2ss@pTYC6aHh%Ow} z?hI422rlwlepMO(p!KKfZ00XDAAq8?7DQrn#%R^@`{Uzc_I>nV5LQ+y&X%ve_z||^vT9V42P>{>KxR9Wg+PW z5yqQk$>U8CQ3>qU=OQtcN~H*`*PjgDLcr{ zS-2HAczlIMLK z{lyjl*by_UvESKwfB)K(b2pqx{VQ%?b=B$IVCZO>go9R)ajNNv7G12VLxJxxbXs-e zufh6M9WA9KWe);^p*!oDYdIe&Qt&owd ztRGAI__4UWe1V`CnX4~$b!in>Rw7i{4NBHvSZvL;-~Ub>&07_Yo{(FvN>HjZBU=CG zQcG#(kT?=j;+TX~3x)CX=@-x(E*U}Tmo@ydeH-SKV5rvktSOM0sDpA(q$?Z7>XW@V zTnZyG45j*@A9UE*tt~oZ(y`oM{i%8^rPqm=#}HkS3m#@O2L3sV=Gz{OY#a8)xQP`S zMS>ztzqm5C$~TsoVeJ(O=l8Vgnzf3qG)J&jG;1}1e(UVh8Ufeb+6@OrL5DB8M<0Zqu|?!Sci6=L3!ob9zzd(4+ zZkX$Uz#cskL&EP$2Vvepx@Kz(`9<(XHAcB@!U_) zP@X=wa|*^|v31#z{z-c6@9RX4>cv7%4ZNN8UT%JSgZX@MR$m#15bxP64c7p^ zF+@x(0MuuZM@jz7O;DIUYJl*}sJdC`NLJ3?hxd&+jIVI7_4t9ls+56;zCD52hv;%o z=G3*WFp^y$jW>RDpud;c_TwH1`5SV!0GS2(fKj1EHaT1xY|m0R&sC|@g;Lr!5OXPv zhE{qoi?-<{Dg9#Yk{E7=JniKuzfR zr-Mc%rk>hB=}_z7owMZ2ZtP8YXhyQ|*5N2)as&I+ycc6!YAy2r;e@0gl4 zEgxgU7K2@3s?M?Yfx6whdBhsmmJ*RZFlq3ed-DMr1vm4G8PA~1P4-6hRS*(rXs8;e zz^5l&_K!=jfxgb&W{~68O`^hOp=^Un!Tp0flC1Zn00|x5rI1lifnX;9?0L^{4$`%XRVTobRJJ!imO6r#B8J>inv#nd3F| z2RLSkBgYB=x^}ty{d@E+jDa{U*B46sO$n+RD;T27a$&0^Oi_ob(T<)UQfE26<{=KY z7R40!GDn=ixIV1G)*QVwXNE*lYM}|#MW$IgKGi$~OW&eE8<&ehl1`5|GrD5FEQlmt zxfFu=ZXx#4vHBSs#YgtBkLJ(o@p;)ECaz5%%5QSb77r}-E*3IyE7NB~ zn(&dTseriN>H?geE&x~+1CaE`aKa;epp?uh$8Vg_LHq+=owGqFG|ASYsw6gS{raGQ z?xXF(_lraGM9G7Y>GM@t*)FxOmjaMEq7NXR__5SBL-rdROIu1xpTT!Ct}hhb55}li z%u#hB1X< zh;N2y2sJ|gYKw)#)v5vgKW4Jt6y4uU=t11;D7ANs-v)ynCaJe~IYt~od>S}*)bGf% IpH5%>FW*?RSO5S3 delta 2720 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 TwcU;EF@Voezaw>rFa6_xOX8u~ diff --git a/test/src/widgets/text_decorator/box/curled.default.png b/test/src/widgets/text_decorator/box/curled.default.png index 5fd1c49cc75be742834c3ffaabc7cf55a14dfb6c..6de88e6554b646f8cd2ef8eb5662dfacaf146b80 100644 GIT binary patch literal 9740 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 YKNU0NjgA_=+W-In07*qoM6N<$g0n+$rvLx| delta 1124 zcmV-q1e^QhAlV?0L4N~DL_t(|obBCBi=E{iz~Or`$ADyEqKVP$GT)$u?uxa|O!Na3 zY!_XKFZ2V5g{rHzf>LOg))!oK(FXhg)figs+d|L}5Q#*JRi`vBAc1iv(?vFdn{v)P zzcWs*YyZFRXYTVH&huk3Co4B^?CuT#0D!=uWnTaQ0C4*dAhYrdTm-Y=3p)XSW;W&G zpMLh~_}R~&9&f$<*7)r6&!*f8003CP$$L+Zv+HNa3(vne?mKzkWc#e#ys^7G)#5+? z$&bdlH_lD9JOBW&#~%6NBjXRh|KnuqPZ}Wi+<*7Da`npi%lUW41K)jMs%-!OfCatt z*LTME&ptSg-*bF?{;$o+_L($)Kn~rxGR7F==8eg=0002?Nb&qW>ED3-KQ4cKd2DTM z%{_nkj>BW?%jM0_X0x%qy*>B*7-P&~p? zOD~VJ>l<^=|NWo;7*G84$+_qE!x=34IzImBW8=AJo}GLC+V$&W{o%87&ky&(o~+>v zycdSgg)yAL7zgAGK6vjF{c-oPV{^~H^z!ejy(Fwd>c%#SbseJ%9b$ zwLQHa0O0Ec2ykG$`R1R-Q%^rN_x#40^*y~F0O0HV6D*VL2^mUWtlrZK?oPG%p*wHA=r?anwgmtHut$pL z_oe>^1ONcIeUpI!6@M)Ke?q_f@#V3#wKezr;X4kGtuL21Kby_Q_V)JN^J9#$c6x2B zuFn58bDNu+O~f+gD4QUpu|F@Pp?E@4Y|n zK6Y&G`Ct6%m*dsfUz>Y=Kb*m$Yb^RRJpSlortSohY(d)l;4U5iy^UvU$ zejhCQ8jb^S1_$9j005@9a`VRS?o^8(y7Sh= 0 && threshold <= 1, 'Threshold must be between 0 and 1'); + + /// Threshold above which tests will be marked as failing. + /// Ranges from 0 to 1, both inclusive. + final double threshold; + + @override + Future compare(Uint8List imageBytes, Uri golden) async { + final result = await GoldenFileComparator.compareLists( + imageBytes, + await getGoldenBytes(golden), + ); + + if (!result.passed && result.diffPercent <= threshold) { + debugPrint( + 'A difference of ${result.diffPercent * 100}% was found, but it is ' + 'acceptable since it is not greater than the threshold of ' + '${threshold * 100}%', + ); + + return true; + } + + if (!result.passed) { + final error = await generateFailureOutput(result, golden, basedir); + throw FlutterError(error); + } + return result.passed; + } +}