// 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 '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 'utils.dart'; void main() => defineTests(); void defineTests() { group('Data', () { testWidgets( 'simple 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. const Markdown markdown = Markdown(data: 'Data1'); await tester.pumpWidget(boilerplate(markdown)); expectTextStrings(tester.allWidgets, ['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, ['Data2']); }, ); }); group('Text', () { testWidgets( 'Empty string', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( const MarkdownBody(data: ''), ), ); final Iterable widgets = selfAndDescendantWidgetsOf( find.byType(MarkdownBody), tester, ); expectWidgetTypes(widgets, [ MarkdownBody, Column, ]); }, ); testWidgets( 'Simple string', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( const MarkdownBody(data: 'Hello'), ), ); final Iterable widgets = selfAndDescendantWidgetsOf( find.byType(MarkdownBody), tester, ); expectWidgetTypes(widgets, [ MarkdownBody, Column, Wrap, Text, RichText, ]); expectTextStrings(widgets, ['Hello']); }, ); }); group('Leading spaces', () { testWidgets( // Example 192 from the GitHub Flavored Markdown specification. 'leading space are ignored', (WidgetTester tester) async { const String data = ' aaa\n bbb'; await tester.pumpWidget( boilerplate( const MarkdownBody(data: data), ), ); final Iterable widgets = selfAndDescendantWidgetsOf( find.byType(MarkdownBody), tester, ); expectWidgetTypes(widgets, [ MarkdownBody, Column, Wrap, Text, RichText, ]); expectTextStrings(widgets, ['aaa bbb']); }); }); group('Line Break', () { testWidgets( // Example 654 from the GitHub Flavored Markdown specification. 'two spaces at end of line inside a block element', (WidgetTester tester) async { const String data = 'line 1 \nline 2'; await tester.pumpWidget( boilerplate( const MarkdownBody(data: data), ), ); final Iterable widgets = selfAndDescendantWidgetsOf( find.byType(MarkdownBody), tester, ); expectWidgetTypes(widgets, [MarkdownBody, Column, Wrap, Text, RichText]); expectTextStrings(widgets, ['line 1\nline 2']); }, ); testWidgets( // Example 655 from the GitHub Flavored Markdown specification. 'backslash at end of line inside a block element', (WidgetTester tester) async { const String data = 'line 1\\\nline 2'; await tester.pumpWidget( boilerplate( const MarkdownBody(data: data), ), ); final Iterable widgets = selfAndDescendantWidgetsOf( find.byType(MarkdownBody), tester, ); expectWidgetTypes(widgets, [MarkdownBody, Column, Wrap, Text, RichText]); expectTextStrings(widgets, ['line 1\nline 2']); }, ); testWidgets( 'non-applicable line break', (WidgetTester tester) async { const String data = 'line 1.\nline 2.'; await tester.pumpWidget( boilerplate( const MarkdownBody(data: data), ), ); final Iterable widgets = selfAndDescendantWidgetsOf( find.byType(MarkdownBody), tester, ); expectWidgetTypes(widgets, [ MarkdownBody, Column, Wrap, Text, RichText, ]); expectTextStrings(widgets, ['line 1. line 2.']); }, ); testWidgets( 'non-applicable line break', (WidgetTester tester) async { const String data = 'line 1.\nline 2.'; await tester.pumpWidget( boilerplate( const MarkdownBody(data: data), ), ); final Iterable widgets = selfAndDescendantWidgetsOf( find.byType(MarkdownBody), tester, ); expectWidgetTypes(widgets, [ MarkdownBody, Column, Wrap, Text, RichText, ]); expectTextStrings(widgets, ['line 1. line 2.']); }, ); testWidgets( 'soft line break', (WidgetTester tester) async { const String data = 'line 1.\nline 2.'; await tester.pumpWidget( boilerplate( const MarkdownBody( data: data, softLineBreak: true, ), ), ); final Iterable widgets = selfAndDescendantWidgetsOf( find.byType(MarkdownBody), tester, ); expectWidgetTypes(widgets, [MarkdownBody, Column, Wrap, Text, RichText]); expectTextStrings(widgets, ['line 1.\nline 2.']); }, ); }); group('Selectable', () { testWidgets( 'header with line of text', (WidgetTester tester) async { const String data = '# Title\nHello _World_!'; await tester.pumpWidget( boilerplate( const MediaQuery( data: MediaQueryData(), child: Markdown( data: data, selectable: true, ), ), ), ); expect(find.byType(SelectableText), findsNWidgets(2)); }, ); testWidgets( 'header with line of text and onTap callback', (WidgetTester tester) async { const String data = '# Title\nHello _World_!'; String? textTapResults; await tester.pumpWidget( boilerplate( MediaQuery( data: const MediaQueryData(), child: Markdown( data: data, selectable: true, onTapText: () => textTapResults = 'Text has been tapped.', ), ), ), ); final Iterable selectableWidgets = tester.widgetList(find.byType(SelectableText)); expect(selectableWidgets.length, 2); final SelectableText selectableTitle = selectableWidgets.first as SelectableText; expect(selectableTitle, isNotNull); expect(selectableTitle.onTap, isNotNull); selectableTitle.onTap!(); expect(textTapResults == 'Text has been tapped.', true); textTapResults = null; final SelectableText selectableText = selectableWidgets.last as SelectableText; expect(selectableText, isNotNull); expect(selectableText.onTap, isNotNull); selectableText.onTap!(); expect(textTapResults == 'Text has been tapped.', true); }, ); testWidgets( 'Selectable without onSelectionChanged', (WidgetTester tester) async { const String data = '# abc def ghi\njkl opq'; await tester.pumpWidget( const MaterialApp( home: Material( child: MarkdownBody( data: data, selectable: true, ), ), ), ); // Find the positions before character 'd' and 'f'. final Offset dPos = positionInRenderedText(tester, 'abc def ghi', 4); final Offset fPos = positionInRenderedText(tester, 'abc def ghi', 6); // Select from 'd' until 'f'. final TestGesture firstGesture = await tester.startGesture(dPos, kind: PointerDeviceKind.mouse); addTearDown(firstGesture.removePointer); await tester.pump(); await firstGesture.moveTo(fPos); await firstGesture.up(); await tester.pump(); // Find the positions before character 'j' and 'o'. final Offset jPos = positionInRenderedText(tester, 'jkl opq', 0); final Offset oPos = positionInRenderedText(tester, 'jkl opq', 4); // Select from 'j' until 'o'. final TestGesture secondGesture = await tester.startGesture(jPos, kind: PointerDeviceKind.mouse); addTearDown(secondGesture.removePointer); await tester.pump(); await secondGesture.moveTo(oPos); await secondGesture.up(); await tester.pump(); expect(tester.takeException(), isNull); }, ); testWidgets( 'header with line of text and onSelectionChanged callback', (WidgetTester tester) async { const String data = '# abc def ghi\njkl opq'; String? selectableText; String? selectedText; void onSelectionChanged(String? text, TextSelection selection, SelectionChangedCause? cause) { selectableText = text; selectedText = text != null ? selection.textInside(text) : null; } await tester.pumpWidget( MaterialApp( home: Material( child: MarkdownBody( data: data, selectable: true, onSelectionChanged: onSelectionChanged, ), ), ), ); // Find the positions before character 'd' and 'f'. final Offset dPos = positionInRenderedText(tester, 'abc def ghi', 4); final Offset fPos = positionInRenderedText(tester, 'abc def ghi', 6); // Select from 'd' until 'f'. final TestGesture firstGesture = await tester.startGesture(dPos, kind: PointerDeviceKind.mouse); addTearDown(firstGesture.removePointer); await tester.pump(); await firstGesture.moveTo(fPos); await firstGesture.up(); await tester.pump(); expect(selectableText, 'abc def ghi'); expect(selectedText, 'de'); // Find the positions before character 'j' and 'o'. final Offset jPos = positionInRenderedText(tester, 'jkl opq', 0); final Offset oPos = positionInRenderedText(tester, 'jkl opq', 4); // Select from 'j' until 'o'. final TestGesture secondGesture = await tester.startGesture(jPos, kind: PointerDeviceKind.mouse); addTearDown(secondGesture.removePointer); await tester.pump(); await secondGesture.moveTo(oPos); await secondGesture.up(); await tester.pump(); expect(selectableText, 'jkl opq'); expect(selectedText, 'jkl '); }, ); }); group('Strikethrough', () { testWidgets('single word', (WidgetTester tester) async { const String data = '~~strikethrough~~'; await tester.pumpWidget( boilerplate( const MarkdownBody(data: data), ), ); final Iterable widgets = selfAndDescendantWidgetsOf( find.byType(MarkdownBody), tester, ); expectWidgetTypes(widgets, [ MarkdownBody, Column, Wrap, Text, RichText, ]); expectTextStrings(widgets, ['strikethrough']); }); }); }