// Copyright 2013 The Flutter 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:io' as io; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:flutter_test/flutter_test.dart'; import 'image_test_mocks.dart'; import 'utils.dart'; void main() => defineTests(); void defineTests() { group('Image', () { 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 { const String data = '_textbefore ![alt](https://img) textafter_'; await tester.pumpWidget( boilerplate( const Markdown(data: data), ), ); final Iterable texts = tester.widgetList(find.byType(Text)); final Text firstTextWidget = texts.first; final TextSpan firstTextSpan = firstTextWidget.textSpan! as TextSpan; final Image image = tester.widget(find.byType(Image)); final NetworkImage networkImage = image.image as NetworkImage; final Text secondTextWidget = texts.last; final TextSpan secondTextSpan = secondTextWidget.textSpan! as TextSpan; expect(firstTextSpan.text, 'textbefore '); expect(firstTextSpan.style!.fontStyle, FontStyle.italic); expect(networkImage.url, 'https://img'); expect(secondTextSpan.text, ' textafter'); expect(secondTextSpan.style!.fontStyle, FontStyle.italic); }, ); testWidgets( 'should work with a link', (WidgetTester tester) async { const String data = '![alt](https://img#50x50)'; await tester.pumpWidget( boilerplate( const Markdown(data: data), ), ); final Image image = tester.widget(find.byType(Image)); final NetworkImage networkImage = image.image as NetworkImage; expect(networkImage.url, 'https://img'); expect(image.width, 50); expect(image.height, 50); }, ); testWidgets( 'should work with relative remote image', (WidgetTester tester) async { const String data = '![alt](/img.png)'; await tester.pumpWidget( boilerplate( const Markdown( data: data, imageDirectory: 'https://localhost', ), ), ); final Iterable widgets = tester.allWidgets; final Image image = widgets.firstWhere((Widget widget) => widget is Image) as Image; expect(image.image is NetworkImage, isTrue); expect((image.image as NetworkImage).url, 'https://localhost/img.png'); }, ); testWidgets( 'local files should be files on non-web', (WidgetTester tester) async { const String data = '![alt](http.png)'; await tester.pumpWidget( boilerplate( const Markdown(data: data), ), ); final Iterable widgets = tester.allWidgets; final Image image = widgets.firstWhere((Widget widget) => widget is Image) as Image; expect(image.image is FileImage, isTrue); }, skip: kIsWeb || isLinux, ); testWidgets( 'local files should be network on web', (WidgetTester tester) async { const String data = '![alt](http.png)'; await tester.pumpWidget( boilerplate( const Markdown(data: data), ), ); final Iterable widgets = tester.allWidgets; final Image image = widgets.firstWhere((Widget widget) => widget is Image) as Image; expect(image.image is NetworkImage, isTrue); }, skip: !kIsWeb || !isLinux, ); testWidgets( 'should work with resources', (WidgetTester tester) async { TestWidgetsFlutterBinding.ensureInitialized(); const String data = '![alt](resource:assets/logo.png)'; await tester.pumpWidget( boilerplate( MaterialApp( home: DefaultAssetBundle( bundle: TestAssetBundle(), child: Center( child: Container( color: Colors.white, width: 500, child: const Markdown( data: data, ), ), ), ), ), ), ); final Image image = tester.allWidgets.firstWhere((Widget widget) => widget is Image) as Image; expect(image.image is AssetImage, isTrue); expect((image.image as AssetImage).assetName, 'assets/logo.png'); // Force the asset image to be rasterized so it can be compared. await tester.runAsync(() async { final Element element = tester.element(find.byType(Markdown)); await precacheImage(image.image, element); }); await tester.pumpAndSettle(); await expectLater( find.byType(Container), matchesGoldenFile('assets/images/golden/image_test/resource_asset_logo.png')); }, skip: kIsWeb || isLinux, // Goldens are platform-specific. ); testWidgets( 'should work with local image files', (WidgetTester tester) async { const String data = '![alt](img.png#50x50)'; await tester.pumpWidget( boilerplate( const Markdown(data: data), ), ); final Image image = tester.widget(find.byType(Image)); final FileImage fileImage = image.image as FileImage; expect(fileImage.file.path, 'img.png'); expect(image.width, 50); expect(image.height, 50); }, skip: kIsWeb || isLinux, ); testWidgets( 'should show properly next to text', (WidgetTester tester) async { const String data = 'Hello ![alt](img#50x50)'; await tester.pumpWidget( boilerplate( const Markdown(data: data), ), ); final Text text = tester.widget(find.byType(Text)); final TextSpan textSpan = text.textSpan! as TextSpan; expect(textSpan.text, 'Hello '); expect(textSpan.style, isNotNull); }, ); testWidgets( 'should work when nested in a link', (WidgetTester tester) async { final List tapTexts = []; final List tapResults = []; const String data = '[![alt](https://img#50x50)](href)'; await tester.pumpWidget( boilerplate( Markdown( data: data, onTapLink: (String text, String? value, String title) { tapTexts.add(text); tapResults.add(value); }, ), ), ); final GestureDetector detector = tester.widget(find.byType(GestureDetector)); detector.onTap!(); expect(tapTexts.length, 1); expect(tapTexts, everyElement('alt')); expect(tapResults.length, 1); expect(tapResults, everyElement('href')); }, ); testWidgets( 'should work when nested in a link with text', (WidgetTester tester) async { final List tapTexts = []; final List tapResults = []; const String data = '[Text before ![alt](https://img#50x50) text after](href)'; await tester.pumpWidget( boilerplate( Markdown( data: data, onTapLink: (String text, String? value, String title) { tapTexts.add(text); tapResults.add(value); }, ), ), ); final GestureDetector detector = tester.widget(find.byType(GestureDetector)); detector.onTap!(); final Iterable texts = tester.widgetList(find.byType(Text)); final Text firstTextWidget = texts.first; final TextSpan firstSpan = firstTextWidget.textSpan! as TextSpan; (firstSpan.recognizer as TapGestureRecognizer?)!.onTap!(); final Text lastTextWidget = texts.last; final TextSpan lastSpan = lastTextWidget.textSpan! as TextSpan; (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(tapTexts.length, 3); expect(tapTexts, everyElement('Text before alt text after')); expect(tapResults.length, 3); expect(tapResults, everyElement('href')); }, ); testWidgets( 'should work alongside different links', (WidgetTester tester) async { final List tapTexts = []; final List tapResults = []; const String data = '[Link before](firstHref)[![alt](https://img#50x50)](imageHref)[link after](secondHref)'; await tester.pumpWidget( boilerplate( Markdown( data: data, onTapLink: (String text, String? value, String title) { tapTexts.add(text); tapResults.add(value); }, ), ), ); final Iterable texts = tester.widgetList(find.byType(Text)); final Text firstTextWidget = texts.first; final TextSpan firstSpan = firstTextWidget.textSpan! as TextSpan; (firstSpan.recognizer as TapGestureRecognizer?)!.onTap!(); final GestureDetector detector = tester.widget(find.byType(GestureDetector)); detector.onTap!(); final Text lastTextWidget = texts.last; final TextSpan lastSpan = lastTextWidget.textSpan! as TextSpan; (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(tapTexts.length, 3); expect(tapTexts, ['Link before', 'alt', 'link after']); expect(tapResults.length, 3); expect(tapResults, ['firstHref', 'imageHref', 'secondHref']); }, ); testWidgets( 'should gracefully handle image URLs with empty scheme', (WidgetTester tester) async { const String data = '![alt](://img#x50)'; await tester.pumpWidget( boilerplate( const Markdown(data: data), ), ); expect(find.byType(Image), findsNothing); expect(tester.takeException(), isNull); }, ); testWidgets( 'should gracefully handle image URLs with invalid scheme', (WidgetTester tester) async { const String data = '![alt](ttps://img#x50)'; await tester.pumpWidget( boilerplate( const Markdown(data: data), ), ); // On the web, any URI with an unrecognized scheme is treated as a network image. // Thus the error builder of the Image widget is called. // On non-web, any URI with an unrecognized scheme is treated as a file image. // However, constructing a file from an invalid URI will throw an exception. // Thus the Image widget is never created, nor is its error builder called. if (kIsWeb || isLinux) { expect(find.byType(Image), findsOneWidget); } else { expect(find.byType(Image), findsNothing); } expect(tester.takeException(), isNull); }, ); testWidgets( 'should gracefully handle width parsing failures', (WidgetTester tester) async { const String data = '![alt](https://img#x50)'; await tester.pumpWidget( boilerplate( const Markdown(data: data), ), ); final Image image = tester.widget(find.byType(Image)); final NetworkImage networkImage = image.image as NetworkImage; expect(networkImage.url, 'https://img'); expect(image.width, null); expect(image.height, 50); }, ); testWidgets( 'should gracefully handle height parsing failures', (WidgetTester tester) async { const String data = ' ![alt](https://img#50x)'; await tester.pumpWidget( boilerplate( const Markdown(data: data), ), ); final Image image = tester.widget(find.byType(Image)); final NetworkImage networkImage = image.image as NetworkImage; expect(networkImage.url, 'https://img'); expect(image.width, 50); expect(image.height, null); }, ); testWidgets( 'custom image builder', (WidgetTester tester) async { const String data = '![alt](https://img.png)'; Widget builder(Uri uri, String? title, String? alt) => Image.asset('assets/logo.png'); await tester.pumpWidget( boilerplate( MaterialApp( home: DefaultAssetBundle( bundle: TestAssetBundle(), child: Center( child: Container( color: Colors.white, width: 500, child: Markdown( data: data, imageBuilder: builder, ), ), ), ), ), ), ); final Iterable widgets = tester.allWidgets; final Image image = widgets.firstWhere((Widget widget) => widget is Image) as Image; expect(image.image.runtimeType, AssetImage); expect((image.image as AssetImage).assetName, 'assets/logo.png'); // Force the asset image to be rasterized so it can be compared. await tester.runAsync(() async { final Element element = tester.element(find.byType(Markdown)); await precacheImage(image.image, element); }); await tester.pumpAndSettle(); await expectLater( find.byType(Container), matchesGoldenFile('assets/images/golden/image_test/custom_builder_asset_logo.png')); imageCache.clear(); }, skip: kIsWeb || isLinux, // Goldens are platform-specific. ); }); }