| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:io' as io; |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter_markdown/flutter_markdown.dart'; |
| import 'package:flutter_markdown/src/style_sheet.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:mockito/mockito.dart'; |
| |
| void main() { |
| TextTheme textTheme = Typography(platform: TargetPlatform.android) |
| .black |
| .merge(TextTheme(body1: TextStyle(fontSize: 12.0))); |
| |
| testWidgets('Simple string', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const MarkdownBody(data: 'Hello'))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| _expectWidgetTypes(widgets, <Type>[Directionality, MarkdownBody, Column, Wrap, RichText]); |
| _expectTextStrings(widgets, <String>['Hello']); |
| }); |
| |
| testWidgets('Header', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const MarkdownBody(data: '# Header'))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| _expectWidgetTypes(widgets, <Type>[Directionality, MarkdownBody, Column, Wrap, RichText]); |
| _expectTextStrings(widgets, <String>['Header']); |
| }); |
| |
| testWidgets('Blockquote', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const MarkdownBody(data: '> quote'))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| _expectTextStrings(widgets, <String>['quote']); |
| }); |
| |
| testWidgets('Scrollable code block', (WidgetTester tester) async { |
| const String data = '```\nvoid main() {\n print(\'Hello World!\');\n}\n```'; |
| |
| await tester.pumpWidget(_boilerplate(MediaQuery( |
| data: MediaQueryData(), |
| child: const MarkdownBody(data: data), |
| ))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| expect(widgets.where((Widget widget) => widget is SingleChildScrollView), isNotEmpty); |
| }); |
| |
| testWidgets('Strikethrough', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const MarkdownBody(data: '~~strikethrough~~'))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| _expectWidgetTypes(widgets, <Type>[Directionality, MarkdownBody, Column, Wrap, RichText]); |
| _expectTextStrings(widgets, <String>['strikethrough']); |
| }); |
| |
| testWidgets('Line break', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const MarkdownBody(data: 'line 1 \nline 2'))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| _expectWidgetTypes(widgets, <Type>[Directionality, MarkdownBody, Column, Wrap, RichText]); |
| _expectTextStrings(widgets, <String>['line 1\nline 2']); |
| }); |
| |
| testWidgets('Empty string', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const MarkdownBody(data: ''))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| _expectWidgetTypes(widgets, <Type>[Directionality, MarkdownBody, Column]); |
| }); |
| |
| testWidgets('Ordered list', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate( |
| const MarkdownBody(data: '1. Item 1\n1. Item 2\n2. Item 3'), |
| )); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| _expectTextStrings(widgets, <String>[ |
| '1.', |
| 'Item 1', |
| '2.', |
| 'Item 2', |
| '3.', |
| 'Item 3', |
| ]); |
| }); |
| |
| testWidgets('Unordered list', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| _boilerplate(const MarkdownBody(data: '- Item 1\n- Item 2\n- Item 3')), |
| ); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| _expectTextStrings(widgets, <String>[ |
| '•', |
| 'Item 1', |
| '•', |
| 'Item 2', |
| '•', |
| 'Item 3', |
| ]); |
| }); |
| |
| testWidgets('Empty List Item', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| _boilerplate(const MarkdownBody(data: '- \n- Item 2\n- Item 3')), |
| ); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| _expectTextStrings(widgets, <String>[ |
| '•', |
| '•', |
| 'Item 2', |
| '•', |
| 'Item 3', |
| ]); |
| }); |
| |
| testWidgets('Task list', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| _boilerplate(const MarkdownBody(data: '- [x] Item 1\n- [ ] Item 2')), |
| ); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| |
| _expectTextStrings(widgets, <String>[ |
| String.fromCharCode(Icons.check_box.codePoint), |
| 'Item 1', |
| String.fromCharCode(Icons.check_box_outline_blank.codePoint), |
| 'Item 2', |
| ]); |
| }); |
| |
| testWidgets('Horizontal Rule', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const MarkdownBody(data: '-----'))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| _expectWidgetTypes(widgets, <Type>[Directionality, MarkdownBody, DecoratedBox, SizedBox]); |
| }); |
| |
| testWidgets('Scrollable wrapping', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown(data: ''))); |
| |
| final List<Widget> widgets = tester.allWidgets.toList(); |
| _expectWidgetTypes(widgets.take(3), <Type>[ |
| Directionality, |
| Markdown, |
| ListView, |
| ]); |
| _expectWidgetTypes(widgets.reversed.take(2).toList().reversed, <Type>[ |
| SliverPadding, |
| SliverList, |
| ]); |
| }); |
| |
| testWidgets('Scrollable controller', (WidgetTester tester) async { |
| final ScrollController controller = ScrollController( |
| initialScrollOffset: 209.0, |
| ); |
| |
| await tester.pumpWidget(_boilerplate(Markdown(controller: controller, data: ''))); |
| |
| double realOffset() { |
| return tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels; |
| } |
| |
| expect(controller.offset, equals(209.0)); |
| expect(realOffset(), equals(controller.offset)); |
| }); |
| |
| group('Links', () { |
| testWidgets('should be tappable', (WidgetTester tester) async { |
| String tapResult; |
| await tester.pumpWidget(_boilerplate(Markdown( |
| data: '[Link Text](href)', |
| onTapLink: (value) => tapResult = value, |
| ))); |
| |
| final RichText textWidget = tester.widget(find.byType(RichText)); |
| final TextSpan span = textWidget.text; |
| |
| (span.recognizer as TapGestureRecognizer).onTap(); |
| |
| expect(span.children, null); |
| expect(span.recognizer.runtimeType, equals(TapGestureRecognizer)); |
| expect(tapResult, 'href'); |
| }); |
| |
| testWidgets('should work with nested elements', (WidgetTester tester) async { |
| final List<String> tapResults = <String>[]; |
| await tester.pumpWidget(_boilerplate(Markdown( |
| data: '[Link `with nested code` Text](href)', |
| onTapLink: (value) => tapResults.add(value), |
| ))); |
| |
| final RichText textWidget = tester.widget(find.byType(RichText)); |
| final TextSpan span = textWidget.text; |
| |
| final List<Type> gestureRecognizerTypes = <Type>[]; |
| span.visitChildren((InlineSpan inlineSpan) { |
| if (inlineSpan is TextSpan) { |
| TapGestureRecognizer recognizer = inlineSpan.recognizer; |
| gestureRecognizerTypes.add(recognizer.runtimeType); |
| recognizer.onTap(); |
| } |
| return true; |
| }); |
| |
| expect(span.children.length, 3); |
| expect(gestureRecognizerTypes.length, 3); |
| expect(gestureRecognizerTypes, everyElement(TapGestureRecognizer)); |
| expect(tapResults.length, 3); |
| expect(tapResults, everyElement('href')); |
| }); |
| |
| testWidgets('should work next to other links', (WidgetTester tester) async { |
| final List<String> tapResults = <String>[]; |
| |
| await tester.pumpWidget(_boilerplate(Markdown( |
| data: '[First Link](firstHref) and [Second Link](secondHref)', |
| onTapLink: (value) => tapResults.add(value), |
| ))); |
| |
| final RichText textWidget = tester.widgetList(find.byType(RichText)).first; |
| final TextSpan span = textWidget.text; |
| |
| final List<Type> gestureRecognizerTypes = <Type>[]; |
| span.visitChildren((InlineSpan inlineSpan) { |
| if (inlineSpan is TextSpan) { |
| TapGestureRecognizer recognizer = inlineSpan.recognizer; |
| gestureRecognizerTypes.add(recognizer.runtimeType); |
| recognizer?.onTap(); |
| } |
| return true; |
| }); |
| |
| expect(span.children.length, 3); |
| expect(gestureRecognizerTypes, |
| orderedEquals([TapGestureRecognizer, Null, TapGestureRecognizer])); |
| expect(tapResults, orderedEquals(['firstHref', 'secondHref'])); |
| }); |
| }); |
| |
| group('Images', () { |
| setUp(() { |
| // Only needs to be done once since the HttpClient generated by this |
| // override is cached as a static singleton. |
| io.HttpOverrides.global = TestHttpOverrides(); |
| }); |
| |
| testWidgets('should not interrupt styling', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown( |
| data: '_textbefore  textafter_', |
| ))); |
| |
| final Iterable<RichText> texts = tester.widgetList(find.byType(RichText)); |
| final RichText firstTextWidget = texts.first; |
| final TextSpan firstTextSpan = firstTextWidget.text; |
| final Image image = tester.widget(find.byType(Image)); |
| final NetworkImage networkImage = image.image; |
| final RichText secondTextWidget = texts.last; |
| final TextSpan secondTextSpan = secondTextWidget.text; |
| |
| expect(firstTextSpan.text, 'textbefore '); |
| expect(firstTextSpan.style.fontStyle, FontStyle.italic); |
| expect(networkImage.url, 'http://img'); |
| expect(secondTextSpan.text, ' textafter'); |
| expect(secondTextSpan.style.fontStyle, FontStyle.italic); |
| }); |
| |
| testWidgets('should work with a link', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown(data: ''))); |
| |
| final Image image = tester.widget(find.byType(Image)); |
| final NetworkImage networkImage = image.image; |
| expect(networkImage.url, 'https://img'); |
| expect(image.width, 50); |
| expect(image.height, 50); |
| }); |
| |
| testWidgets('should work with relative remote image', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown( |
| data: '', |
| imageDirectory: 'http://localhost', |
| ))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| final Image image = widgets.firstWhere((Widget widget) => widget is Image); |
| |
| expect(image.image is NetworkImage, isTrue); |
| expect((image.image as NetworkImage).url, 'http://localhost/img.png'); |
| }); |
| |
| testWidgets('should not escape ampersands in links', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown( |
| data: |
| ''))); |
| |
| final Image image = tester.allWidgets.firstWhere((Widget widget) => widget is Image); |
| final NetworkImage networkImage = image.image; |
| expect(networkImage.url, |
| 'https://preview.redd.it/sg3q5cuedod31.jpg?width=640&crop=smart&auto=webp&s=497e6295e0c0fc2ce7df5a324fe1acd7b5a5264f'); |
| }); |
| |
| testWidgets('local files should be files', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown(data: ''))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| final Image image = widgets.firstWhere((Widget widget) => widget is Image); |
| |
| expect(image.image is FileImage, isTrue); |
| }); |
| |
| testWidgets('should work with resources', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown( |
| data: '', |
| ))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| final Image image = widgets.firstWhere((Widget widget) => widget is Image); |
| |
| expect(image.image is AssetImage, isTrue); |
| expect((image.image as AssetImage).assetName, 'assets/logo.png'); |
| }); |
| |
| testWidgets('should work with local image files', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown(data: ''))); |
| |
| final Image image = tester.widget(find.byType(Image)); |
| final FileImage fileImage = image.image; |
| expect(fileImage.file.path, 'img.png'); |
| expect(image.width, 50); |
| expect(image.height, 50); |
| }); |
| |
| testWidgets('should show properly next to text', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown(data: 'Hello '))); |
| |
| final RichText richText = tester.widget(find.byType(RichText)); |
| TextSpan textSpan = richText.text; |
| expect(textSpan.text, 'Hello '); |
| expect(textSpan.style, isNotNull); |
| }); |
| |
| testWidgets('should work when nested in a link', (WidgetTester tester) async { |
| final List<String> tapResults = <String>[]; |
| await tester.pumpWidget(_boilerplate(Markdown( |
| data: '[](href)', |
| onTapLink: (value) => tapResults.add(value), |
| ))); |
| |
| final GestureDetector detector = tester.widget(find.byType(GestureDetector)); |
| |
| detector.onTap(); |
| |
| expect(tapResults.length, 1); |
| expect(tapResults, everyElement('href')); |
| }); |
| |
| testWidgets('should work when nested in a link with text', (WidgetTester tester) async { |
| final List<String> tapResults = <String>[]; |
| await tester.pumpWidget(_boilerplate(Markdown( |
| data: '[Text before  text after](href)', |
| onTapLink: (value) => tapResults.add(value), |
| ))); |
| |
| final GestureDetector detector = tester.widget(find.byType(GestureDetector)); |
| detector.onTap(); |
| |
| final Iterable<RichText> texts = tester.widgetList(find.byType(RichText)); |
| final RichText firstTextWidget = texts.first; |
| final TextSpan firstSpan = firstTextWidget.text; |
| (firstSpan.recognizer as TapGestureRecognizer).onTap(); |
| |
| final RichText lastTextWidget = texts.last; |
| final TextSpan lastSpan = lastTextWidget.text; |
| (lastSpan.recognizer as TapGestureRecognizer).onTap(); |
| |
| expect(firstSpan.children, null); |
| expect(firstSpan.text, 'Text before '); |
| expect(firstSpan.recognizer.runtimeType, equals(TapGestureRecognizer)); |
| |
| expect(lastSpan.children, null); |
| expect(lastSpan.text, ' text after'); |
| expect(lastSpan.recognizer.runtimeType, equals(TapGestureRecognizer)); |
| |
| expect(tapResults.length, 3); |
| expect(tapResults, everyElement('href')); |
| }); |
| |
| testWidgets('should work alongside different links', (WidgetTester tester) async { |
| final List<String> tapResults = <String>[]; |
| await tester.pumpWidget(_boilerplate(Markdown( |
| data: |
| '[Link before](firstHref)[](imageHref)[link after](secondHref)', |
| onTapLink: (value) => tapResults.add(value), |
| ))); |
| |
| final Iterable<RichText> texts = tester.widgetList(find.byType(RichText)); |
| final RichText firstTextWidget = texts.first; |
| final TextSpan firstSpan = firstTextWidget.text; |
| (firstSpan.recognizer as TapGestureRecognizer).onTap(); |
| |
| final GestureDetector detector = tester.widget(find.byType(GestureDetector)); |
| detector.onTap(); |
| |
| final RichText lastTextWidget = texts.last; |
| final TextSpan lastSpan = lastTextWidget.text; |
| (lastSpan.recognizer as TapGestureRecognizer).onTap(); |
| |
| expect(firstSpan.children, null); |
| expect(firstSpan.text, 'Link before'); |
| expect(firstSpan.recognizer.runtimeType, equals(TapGestureRecognizer)); |
| |
| expect(lastSpan.children, null); |
| expect(lastSpan.text, 'link after'); |
| expect(lastSpan.recognizer.runtimeType, equals(TapGestureRecognizer)); |
| |
| expect(tapResults.length, 3); |
| expect(tapResults, ['firstHref', 'imageHref', 'secondHref']); |
| }); |
| }); |
| |
| group('Tables', () { |
| testWidgets('should show properly', (WidgetTester tester) async { |
| const String data = '|Header 1|Header 2|\n|-----|-----|\n|Col 1|Col 2|'; |
| await tester.pumpWidget(_boilerplate(const MarkdownBody(data: data))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| _expectTextStrings(widgets, <String>['Header 1', 'Header 2', 'Col 1', 'Col 2']); |
| }); |
| |
| testWidgets('work without the outer pipes', (WidgetTester tester) async { |
| const String data = 'Header 1|Header 2\n-----|-----\nCol 1|Col 2'; |
| await tester.pumpWidget(_boilerplate(const MarkdownBody(data: data))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| _expectTextStrings(widgets, <String>['Header 1', 'Header 2', 'Col 1', 'Col 2']); |
| }); |
| |
| testWidgets('should work with alignments', (WidgetTester tester) async { |
| const String data = '|Header 1|Header 2|\n|:----:|----:|\n|Col 1|Col 2|'; |
| await tester.pumpWidget(_boilerplate(const MarkdownBody(data: data))); |
| |
| final Iterable<DefaultTextStyle> styles = tester.widgetList(find.byType(DefaultTextStyle)); |
| |
| expect(styles.first.textAlign, TextAlign.center); |
| expect(styles.last.textAlign, TextAlign.right); |
| }); |
| |
| testWidgets('should work with styling', (WidgetTester tester) async { |
| const String data = '|Header|\n|----|\n|*italic*|'; |
| await tester.pumpWidget(_boilerplate(MarkdownBody(data: data))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| final RichText richText = widgets.lastWhere((Widget widget) => widget is RichText); |
| |
| _expectTextStrings(widgets, <String>['Header', 'italic']); |
| expect(richText.text.style.fontStyle, FontStyle.italic); |
| }); |
| |
| testWidgets('should work next to other tables', (WidgetTester tester) async { |
| const String data = '|first header|\n|----|\n|first col|\n\n' |
| '|second header|\n|----|\n|second col|'; |
| await tester.pumpWidget(_boilerplate(const MarkdownBody(data: data))); |
| |
| final Iterable<Widget> tables = tester.widgetList(find.byType(Table)); |
| |
| expect(tables.length, 2); |
| }); |
| |
| testWidgets('column width should follow stylesheet', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme); |
| |
| const String data = '|Header|\n|----|\n|Column|'; |
| const FixedColumnWidth columnWidth = FixedColumnWidth(100); |
| final MarkdownStyleSheet style = MarkdownStyleSheet.fromTheme(theme).copyWith( |
| tableColumnWidth: columnWidth, |
| ); |
| |
| await tester.pumpWidget(_boilerplate(MarkdownBody(data: data, styleSheet: style))); |
| |
| final Table table = tester.widget(find.byType(Table)); |
| |
| expect(table.defaultColumnWidth, columnWidth); |
| }); |
| }); |
| |
| group('Uri data scheme', () { |
| testWidgets('should work with image in uri data scheme', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown( |
| data: '', |
| ))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| final Image image = widgets.firstWhere((Widget widget) => widget is Image); |
| expect(image.image.runtimeType, MemoryImage); |
| }); |
| |
| testWidgets('should work with base64 text in uri data scheme', (WidgetTester tester) async { |
| const String imageData = ''; |
| await tester.pumpWidget(_boilerplate(const Markdown(data: imageData))); |
| |
| final Text widget = tester.widget(find.byType(Text)); |
| expect(widget.runtimeType, Text); |
| expect(widget.data, 'Flutter'); |
| }); |
| |
| testWidgets('should work with text in uri data scheme', (WidgetTester tester) async { |
| const String imageData = ''; |
| await tester.pumpWidget(_boilerplate(const Markdown(data: imageData))); |
| |
| final Text widget = tester.widget(find.byType(Text)); |
| expect(widget.runtimeType, Text); |
| expect(widget.data, 'Hello, Flutter'); |
| }); |
| |
| testWidgets('should work with empty uri data scheme', (WidgetTester tester) async { |
| const String imageData = ''; |
| await tester.pumpWidget(_boilerplate(const Markdown(data: imageData))); |
| |
| final Text widget = tester.widget(find.byType(Text)); |
| expect(widget.runtimeType, Text); |
| expect(widget.data, ''); |
| }); |
| |
| testWidgets('should work with unsupported mime types of uri data scheme', |
| (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown( |
| data: '', |
| ))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| final SizedBox widget = widgets.firstWhere((Widget widget) => widget is SizedBox); |
| expect(widget.runtimeType, SizedBox); |
| }); |
| }); |
| |
| group('Parser does not convert', () { |
| testWidgets('& to & when parsing', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown(data: '&'))); |
| _expectTextStrings(tester.allWidgets, <String>['&']); |
| }); |
| |
| testWidgets('< to < when parsing', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown(data: '<'))); |
| _expectTextStrings(tester.allWidgets, <String>['<']); |
| }); |
| |
| testWidgets('existing HTML entities when parsing', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown(data: '& © < {'))); |
| _expectTextStrings(tester.allWidgets, <String>['& © < {']); |
| }); |
| }); |
| |
| group('Changing configs', () { |
| testWidgets(' - data', (WidgetTester tester) async { |
| // extract to variable; if run with --track-widget-creation using const |
| // widgets aren't necessarily identical if created on different lines. |
| final markdown = const Markdown(data: 'Data1'); |
| |
| await tester.pumpWidget(_boilerplate(markdown)); |
| _expectTextStrings(tester.allWidgets, <String>['Data1']); |
| |
| final String stateBefore = _dumpRenderView(); |
| await tester.pumpWidget(_boilerplate(markdown)); |
| final String stateAfter = _dumpRenderView(); |
| expect(stateBefore, equals(stateAfter)); |
| |
| await tester.pumpWidget(_boilerplate(const Markdown(data: 'Data2'))); |
| _expectTextStrings(tester.allWidgets, <String>['Data2']); |
| }); |
| |
| testWidgets(' - selectable', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(MediaQuery( |
| data: MediaQueryData(), |
| child: const Markdown( |
| data: '# Title\nHello _World_!', |
| selectable: true, |
| ), |
| ))); |
| |
| expect(find.byType(SelectableText), findsNWidgets(2)); |
| }); |
| |
| testWidgets(' - style', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme); |
| |
| final MarkdownStyleSheet style1 = MarkdownStyleSheet.fromTheme(theme); |
| final MarkdownStyleSheet style2 = MarkdownStyleSheet.largeFromTheme(theme); |
| expect(style1, isNot(style2)); |
| |
| await tester.pumpWidget( |
| _boilerplate(Markdown(data: '# Test', styleSheet: style1)), |
| ); |
| final RichText text1 = tester.widget(find.byType(RichText)); |
| await tester.pumpWidget( |
| _boilerplate(Markdown(data: '# Test', styleSheet: style2)), |
| ); |
| final RichText text2 = tester.widget(find.byType(RichText)); |
| |
| expect(text1.text, isNot(text2.text)); |
| }); |
| |
| testWidgets(' - imageBuilder', (WidgetTester tester) async { |
| final String data = ''; |
| final MarkdownImageBuilder builder = (_) => Image.asset('assets/logo.png'); |
| |
| await tester.pumpWidget(_boilerplate(Markdown(data: data, imageBuilder: builder))); |
| |
| final Image image = tester.widget(find.byType(Image)); |
| |
| expect(image.image.runtimeType, AssetImage); |
| expect((image.image as AssetImage).assetName, 'assets/logo.png'); |
| }); |
| |
| testWidgets(' - checkboxBuilder', (WidgetTester tester) async { |
| final String data = '- [x] Item 1\n- [ ] Item 2'; |
| final MarkdownCheckboxBuilder builder = (bool checked) => Text('$checked'); |
| |
| await tester.pumpWidget(_boilerplate(Markdown(data: data, checkboxBuilder: builder))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| |
| _expectTextStrings(widgets, <String>[ |
| 'true', |
| 'Item 1', |
| 'false', |
| 'Item 2', |
| ]); |
| }); |
| |
| testWidgets(' - should use style textScaleFactor in RichText', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate( |
| MarkdownBody( |
| styleSheet: MarkdownStyleSheet(textScaleFactor: 2.0), |
| data: 'Hello', |
| ), |
| )); |
| |
| final RichText richText = tester.widget(find.byType(RichText)); |
| expect(richText.textScaleFactor, 2.0); |
| }); |
| }); |
| |
| group('Style', () { |
| testWidgets('equality - Material', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme); |
| |
| final MarkdownStyleSheet style1 = MarkdownStyleSheet.fromTheme(theme); |
| final MarkdownStyleSheet style2 = MarkdownStyleSheet.fromTheme(theme); |
| expect(style1, equals(style2)); |
| expect(style1.hashCode, equals(style2.hashCode)); |
| }); |
| |
| testWidgets('equality - Cupertino', (WidgetTester tester) async { |
| final CupertinoThemeData theme = CupertinoThemeData(brightness: Brightness.light); |
| |
| final MarkdownStyleSheet style1 = MarkdownStyleSheet.fromCupertinoTheme(theme); |
| final MarkdownStyleSheet style2 = MarkdownStyleSheet.fromCupertinoTheme(theme); |
| expect(style1, equals(style2)); |
| expect(style1.hashCode, equals(style2.hashCode)); |
| }); |
| |
| testWidgets('merge', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme); |
| final MarkdownStyleSheet style1 = MarkdownStyleSheet.fromTheme(theme); |
| final MarkdownStyleSheet style2 = MarkdownStyleSheet(p: TextStyle(color: Colors.red)); |
| |
| final MarkdownStyleSheet merged = style1.merge(style2); |
| expect(merged.p.color, Colors.red); |
| }); |
| |
| testWidgets('create based on which theme', (WidgetTester tester) async { |
| await tester.pumpWidget(_boilerplate(const Markdown( |
| data: '[title](url)', |
| styleSheetTheme: MarkdownStyleSheetBaseTheme.cupertino, |
| ))); |
| |
| final RichText widget = tester.widget(find.byType(RichText)); |
| expect(widget.text.style.color, CupertinoColors.link); |
| }); |
| }); |
| |
| testWidgets('HTML tag ignored ', (WidgetTester tester) async { |
| final List<String> mdData = <String>[ |
| 'Line 1\n<p>HTML content</p>\nLine 2', |
| 'Line 1\n<!-- HTML\n comment\n ignored --><\nLine 2' |
| ]; |
| |
| for (String mdLine in mdData) { |
| await tester.pumpWidget(_boilerplate(MarkdownBody(data: mdLine))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| _expectTextStrings(widgets, <String>['Line 1', 'Line 2']); |
| } |
| }); |
| |
| testWidgets('should apply text alignments from stylesheet', |
| (WidgetTester tester) async { |
| final ThemeData theme = ThemeData.light().copyWith(textTheme: textTheme); |
| final MarkdownStyleSheet style1 = |
| MarkdownStyleSheet.fromTheme(theme).copyWith( |
| h1Align: WrapAlignment.center, |
| h3Align: WrapAlignment.end, |
| ); |
| |
| const String data = '# h1\n ## h2'; |
| await tester.pumpWidget(_boilerplate(MarkdownBody( |
| data: data, |
| styleSheet: style1, |
| ))); |
| |
| final Iterable<Widget> widgets = tester.allWidgets; |
| _expectWidgetTypes(widgets, <Type>[ |
| Directionality, |
| MarkdownBody, |
| Column, |
| Column, |
| Wrap, |
| RichText, |
| SizedBox, |
| Column, |
| Wrap, |
| RichText, |
| ]); |
| |
| expect((widgets.firstWhere((w) => w is RichText) as RichText).textAlign, |
| TextAlign.center); |
| expect((widgets.last as RichText).textAlign, TextAlign.start, |
| reason: "default alignment if none is set in stylesheet"); |
| }); |
| |
| } |
| |
| void _expectWidgetTypes(Iterable<Widget> widgets, List<Type> expected) { |
| final List<Type> actual = widgets.map((Widget w) => w.runtimeType).toList(); |
| expect(actual, expected); |
| } |
| |
| void _expectTextStrings(Iterable<Widget> widgets, List<String> strings) { |
| int currentString = 0; |
| for (Widget widget in widgets) { |
| if (widget is RichText) { |
| final TextSpan span = widget.text; |
| final String text = _extractTextFromTextSpan(span); |
| expect(text, equals(strings[currentString])); |
| currentString += 1; |
| } |
| } |
| } |
| |
| String _extractTextFromTextSpan(TextSpan span) { |
| String text = span.text ?? ''; |
| if (span.children != null) { |
| for (TextSpan child in span.children) { |
| text += _extractTextFromTextSpan(child); |
| } |
| } |
| return text; |
| } |
| |
| String _dumpRenderView() { |
| return WidgetsBinding.instance.renderViewElement.toStringDeep().replaceAll( |
| RegExp(r'SliverChildListDelegate#\d+', multiLine: true), 'SliverChildListDelegate'); |
| } |
| |
| /// Wraps a widget with a left-to-right [Directionality] for tests. |
| Widget _boilerplate(Widget child) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: child, |
| ); |
| } |
| |
| class MockHttpClient extends Mock implements io.HttpClient {} |
| |
| class MockHttpClientRequest extends Mock implements io.HttpClientRequest {} |
| |
| class MockHttpClientResponse extends Mock implements io.HttpClientResponse {} |
| |
| class MockHttpHeaders extends Mock implements io.HttpHeaders {} |
| |
| class TestHttpOverrides extends io.HttpOverrides { |
| io.HttpClient createHttpClient(io.SecurityContext context) { |
| return createMockImageHttpClient(context); |
| } |
| } |
| |
| // Returns a mock HTTP client that responds with an image to all requests. |
| MockHttpClient createMockImageHttpClient(io.SecurityContext _) { |
| final MockHttpClient client = MockHttpClient(); |
| final MockHttpClientRequest request = MockHttpClientRequest(); |
| final MockHttpClientResponse response = MockHttpClientResponse(); |
| final MockHttpHeaders headers = MockHttpHeaders(); |
| |
| when(client.getUrl(any)).thenAnswer((_) => Future<io.HttpClientRequest>.value(request)); |
| when(request.headers).thenReturn(headers); |
| when(request.close()).thenAnswer((_) => Future<io.HttpClientResponse>.value(response)); |
| when(response.contentLength).thenReturn(_transparentImage.length); |
| when(response.statusCode).thenReturn(io.HttpStatus.ok); |
| when(response.listen(any)).thenAnswer((Invocation invocation) { |
| final void Function(List<int>) onData = invocation.positionalArguments[0]; |
| final void Function() onDone = invocation.namedArguments[#onDone]; |
| final void Function(Object, [StackTrace]) onError = invocation.namedArguments[#onError]; |
| final bool cancelOnError = invocation.namedArguments[#cancelOnError]; |
| |
| return Stream<List<int>>.fromIterable(<List<int>>[_transparentImage]) |
| .listen(onData, onDone: onDone, onError: onError, cancelOnError: cancelOnError); |
| }); |
| |
| return client; |
| } |
| |
| const List<int> _transparentImage = const <int>[ |
| 0x89, |
| 0x50, |
| 0x4E, |
| 0x47, |
| 0x0D, |
| 0x0A, |
| 0x1A, |
| 0x0A, |
| 0x00, |
| 0x00, |
| 0x00, |
| 0x0D, |
| 0x49, |
| 0x48, |
| 0x44, |
| 0x52, |
| 0x00, |
| 0x00, |
| 0x00, |
| 0x01, |
| 0x00, |
| 0x00, |
| 0x00, |
| 0x01, |
| 0x08, |
| 0x06, |
| 0x00, |
| 0x00, |
| 0x00, |
| 0x1F, |
| 0x15, |
| 0xC4, |
| 0x89, |
| 0x00, |
| 0x00, |
| 0x00, |
| 0x0A, |
| 0x49, |
| 0x44, |
| 0x41, |
| 0x54, |
| 0x78, |
| 0x9C, |
| 0x63, |
| 0x00, |
| 0x01, |
| 0x00, |
| 0x00, |
| 0x05, |
| 0x00, |
| 0x01, |
| 0x0D, |
| 0x0A, |
| 0x2D, |
| 0xB4, |
| 0x00, |
| 0x00, |
| 0x00, |
| 0x00, |
| 0x49, |
| 0x45, |
| 0x4E, |
| 0x44, |
| 0xAE, |
| ]; |