// 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/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; final TextTheme textTheme = Typography.material2018().black.merge(const TextTheme(bodyMedium: TextStyle(fontSize: 12.0))); Iterable selfAndDescendantWidgetsOf(Finder start, WidgetTester tester) { final Element startElement = tester.element(start); final Iterable descendants = collectAllElementsFrom(startElement, skipOffstage: false).map((Element e) => e.widget); return [ startElement.widget, ...descendants, ]; } // Returns the RenderEditable displaying the given text. RenderEditable findRenderEditableWithText(WidgetTester tester, String text) { final Iterable roots = tester.renderObjectList(find.byType(EditableText)); expect(roots, isNotEmpty); late RenderEditable renderEditable; void recursiveFinder(RenderObject child) { if (child is RenderEditable && child.plainText == text) { renderEditable = child; return; } child.visitChildren(recursiveFinder); } for (final RenderObject root in roots) { root.visitChildren(recursiveFinder); } expect(renderEditable, isNotNull); return renderEditable; } // Returns the [textOffset] position in rendered [text]. Offset positionInRenderedText(WidgetTester tester, String text, int textOffset) { final RenderEditable renderEditable = findRenderEditableWithText(tester, text); final Iterable textOffsetPoints = renderEditable.getEndpointsForSelection( TextSelection.collapsed(offset: textOffset), ); // Map the points to global positions. final List endpoints = textOffsetPoints.map((TextSelectionPoint point) { return TextSelectionPoint( renderEditable.localToGlobal(point.point), point.direction, ); }).toList(); expect(endpoints.length, 1); return endpoints[0].point + const Offset(kIsWeb ? 1.0 : 0.0, -2.0); } void expectWidgetTypes(Iterable widgets, List expected) { final List actual = widgets.map((Widget w) => w.runtimeType).toList(); expect(actual, expected); } void expectTextStrings(Iterable widgets, List strings) { int currentString = 0; for (final Widget widget in widgets) { TextSpan? span; if (widget is RichText) { span = widget.text as TextSpan; } else if (widget is SelectableText) { span = widget.textSpan; } if (span != null) { 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!.toList().cast()) { 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(textSpan.style, isNotNull, reason: 'text span text style is null'); // Font style check if (style == null) { expect(textSpan.style!.fontStyle, isNull, reason: 'font style is not null'); } else { expect(textSpan.style!.fontStyle, isNotNull, reason: 'font style is null'); expect( textSpan.style!.fontStyle == style, isTrue, reason: 'font style is not $style', ); } // Font weight check expect(textSpan.style, isNotNull, reason: 'font style is null'); expect( textSpan.style!.fontWeight == weight, isTrue, reason: 'font weight is not $weight', ); } @immutable class MarkdownLink { const MarkdownLink(this.text, this.destination, [this.title = '']); final String text; final String? destination; final String title; @override bool operator ==(Object other) => other is MarkdownLink && other.text == text && other.destination == destination && other.title == title; @override int get hashCode => '$text$destination$title'.hashCode; @override 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 textFinder = find.byType(Text); expect(textFinder, findsOneWidget); final Text text = textFinder.evaluate().first.widget as Text; // Verify the link text. expect(text.textSpan, isNotNull); expect(text.textSpan, isA()); // Verify the link text is a onTap gesture recognizer. final TextSpan textSpan = text.textSpan! 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()); final TapGestureRecognizer? tapRecognizer = textSpan.recognizer as TapGestureRecognizer?; expect(tapRecognizer?.onTap, isNotNull); // Execute the onTap callback handler. tapRecognizer!.onTap!(); } void expectInvalidLink(String linkText) { final Finder textFinder = find.byType(Text); expect(textFinder, findsOneWidget); final Text text = textFinder.evaluate().first.widget as Text; expect(text.textSpan, isNotNull); expect(text.textSpan, isA()); final String plainText = text.textSpan!.toPlainText(); expect(plainText, linkText); final TextSpan textSpan = text.textSpan! 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), reason: 'incorrect link tap results, actual: $actual expected: $expected'); } String dumpRenderView() { return WidgetsBinding.instance.rootElement!.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 TestAssetBundle extends CachingAssetBundle { @override Future load(String key) async { if (key == 'AssetManifest.json') { const String manifest = r'{"assets/logo.png":["assets/logo.png"]}'; final ByteData asset = ByteData.view(utf8.encoder.convert(manifest).buffer); return Future.value(asset); } else if (key == 'AssetManifest.bin') { final ByteData manifest = const StandardMessageCodec().encodeMessage(>{'assets/logo.png': []})!; return Future.value(manifest); } else if (key == 'AssetManifest.smcbin') { final ByteData manifest = const StandardMessageCodec().encodeMessage(>{'assets/logo.png': []})!; return Future.value(manifest); } 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.path.endsWith('${io.Platform.pathSeparator}test') ? io.Directory.current.parent : io.Directory.current; final io.File file = io.File('${rootDirectory.path}/test/assets/images/logo.png'); final ByteData asset = ByteData.view(file.readAsBytesSync().buffer); return asset; } else { throw ArgumentError('Unknown asset key: $key'); } } }