blob: f28ef29ba9ae34ec80d2905e18fecbf18083a730 [file] [log] [blame]
// 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:convert';
import 'dart:io' as io;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:markdown/markdown.dart' as md show version;
// TODO(Zhiguang): delete this once the min version of pkg:markdown is updated
final bool newMarkdown = md.version.compareTo('6.0.1') > 0;
final TextTheme textTheme = Typography.material2018()
.merge(const TextTheme(bodyMedium: TextStyle(fontSize: 12.0)));
Iterable<Widget> selfAndDescendantWidgetsOf(Finder start, WidgetTester tester) {
final Element startElement = tester.element(start);
final Iterable<Widget> descendants =
collectAllElementsFrom(startElement, skipOffstage: false)
.map((Element e) => e.widget);
return <Widget>[
void expectWidgetTypes(Iterable<Widget> widgets, List<Type> expected) {
final List<Type> actual = w) => w.runtimeType).toList();
expect(actual, expected);
void expectTextStrings(Iterable<Widget> widgets, List<String> strings) {
int currentString = 0;
for (final Widget widget in widgets) {
if (widget is RichText) {
final TextSpan span = widget.text as TextSpan;
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 (final TextSpan child in span.children! as Iterable<TextSpan>) {
text += _extractTextFromTextSpan(child);
return text;
// Check the font style and weight of the text span.
void expectTextSpanStyle(
TextSpan textSpan, FontStyle? style, FontWeight weight) {
// Verify a text style is set
expect(, isNotNull, reason: 'text span text style is null');
// Font style check
if (style == null) {
expect(!.fontStyle, isNull, reason: 'font style is not null');
} else {
expect(!.fontStyle, isNotNull, reason: 'font style is null');
expect(!.fontStyle == style,
reason: 'font style is not $style',
// Font weight check
expect(, isNotNull, reason: 'font style is null');
expect(!.fontWeight == weight,
reason: 'font weight is not $weight',
class MarkdownLink {
const MarkdownLink(this.text, this.destination, [this.title = '']);
final String text;
final String? destination;
final String title;
bool operator ==(Object other) =>
other is MarkdownLink &&
other.text == text &&
other.destination == destination &&
other.title == title;
int get hashCode => '$text$destination$title'.hashCode;
String toString() {
return '[$text]($destination "$title")';
/// Verify a valid link structure has been created. This routine checks for the
/// link text and the associated [TapGestureRecognizer] on the text span.
void expectValidLink(String linkText) {
final Finder richTextFinder = find.byType(RichText);
expect(richTextFinder, findsOneWidget);
final RichText richText = richTextFinder.evaluate().first.widget as RichText;
// Verify the link text.
expect(richText.text, isNotNull);
expect(richText.text, isA<TextSpan>());
// Verify the link text is a onTap gesture recognizer.
final TextSpan textSpan = richText.text as TextSpan;
expectLinkTextSpan(textSpan, linkText);
void expectLinkTextSpan(TextSpan textSpan, String linkText) {
expect(textSpan.children, isNull);
expect(textSpan.toPlainText(), linkText);
expect(textSpan.recognizer, isNotNull);
expect(textSpan.recognizer, isA<TapGestureRecognizer>());
final TapGestureRecognizer? tapRecognizer =
textSpan.recognizer as TapGestureRecognizer?;
expect(tapRecognizer?.onTap, isNotNull);
// Execute the onTap callback handler.
void expectInvalidLink(String linkText) {
final Finder richTextFinder = find.byType(RichText);
expect(richTextFinder, findsOneWidget);
final RichText richText = richTextFinder.evaluate().first.widget as RichText;
expect(richText.text, isNotNull);
expect(richText.text, isA<TextSpan>());
final String text = richText.text.toPlainText();
expect(text, linkText);
final TextSpan textSpan = richText.text as TextSpan;
expect(textSpan.recognizer, isNull);
void expectTableSize(int rows, int columns) {
final Finder tableFinder = find.byType(Table);
expect(tableFinder, findsOneWidget);
final Table table = tableFinder.evaluate().first.widget as Table;
expect(table.children.length, rows);
for (int index = 0; index < rows; index++) {
expect(table.children[index].children!.length, columns);
void expectLinkTap(MarkdownLink? actual, MarkdownLink expected) {
expect(actual, equals(expected),
'incorrect link tap results, actual: $actual expected: $expected');
String dumpRenderView() {
return WidgetsBinding.instance.renderViewElement!.toStringDeep().replaceAll(
RegExp(r'SliverChildListDelegate#\d+', multiLine: true),
/// Wraps a widget with a left-to-right [Directionality] for tests.
Widget boilerplate(Widget child) {
return Directionality(
textDirection: TextDirection.ltr,
child: child,
class TestAssetBundle extends CachingAssetBundle {
static const String manifest = r'{"assets/logo.png":["assets/logo.png"]}';
Future<ByteData> load(String key) async {
if (key == 'AssetManifest.json') {
final ByteData asset =
return Future<ByteData>.value(asset);
} else if (key == 'assets/logo.png') {
// The root directory tests are run from is different for 'flutter test'
// verses 'flutter test test/*_test.dart'. Adjust the root directory
// to access the assets directory.
final io.Directory rootDirectory =
? io.Directory.current.parent
: io.Directory.current;
final io.File file =
final ByteData asset = ByteData.view(file.readAsBytesSync().buffer);
if (asset == null) {
throw FlutterError('Unable to load asset: $key');
return asset;
} else {
throw ArgumentError('Unknown asset key: $key');