mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
WIP Commits separated as follows: - Update lints in analysis_options files - Run `dart fix --apply` - Clean up leftover analysis issues - Run `dart format .` in the right places. Local analysis and testing passes. Checking CI now. Part of https://github.com/flutter/flutter/issues/178827 - Adoption of flutter_lints in examples/api coming in a separate change (cc @loic-sharma) ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1832 lines
61 KiB
Dart
1832 lines
61 KiB
Dart
// Copyright 2014 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:ui' as ui;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
|
|
|
import 'semantics_tester.dart';
|
|
|
|
void main() {
|
|
testWidgets('DefaultTextStyle.merge correctly merges arguments', (WidgetTester tester) async {
|
|
var defaultTextStyle = const DefaultTextStyle.fallback();
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: DefaultTextStyle(
|
|
style: const TextStyle(color: Colors.black, fontSize: 20),
|
|
textAlign: TextAlign.left,
|
|
softWrap: false,
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 2,
|
|
textWidthBasis: TextWidthBasis.longestLine,
|
|
textHeightBehavior: const TextHeightBehavior(applyHeightToFirstAscent: false),
|
|
child: DefaultTextStyle.merge(
|
|
style: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
|
textAlign: TextAlign.center,
|
|
softWrap: true,
|
|
overflow: TextOverflow.fade,
|
|
maxLines: 3,
|
|
textWidthBasis: TextWidthBasis.parent,
|
|
textHeightBehavior: const TextHeightBehavior(applyHeightToLastDescent: false),
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
defaultTextStyle = DefaultTextStyle.of(context);
|
|
return const Text('Text');
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
defaultTextStyle.style,
|
|
const TextStyle(color: Colors.red, fontSize: 20, fontWeight: FontWeight.bold),
|
|
);
|
|
expect(defaultTextStyle.textAlign, TextAlign.center);
|
|
expect(defaultTextStyle.softWrap, true);
|
|
expect(defaultTextStyle.overflow, TextOverflow.fade);
|
|
expect(defaultTextStyle.maxLines, 3);
|
|
expect(defaultTextStyle.textWidthBasis, TextWidthBasis.parent);
|
|
expect(
|
|
defaultTextStyle.textHeightBehavior,
|
|
const TextHeightBehavior(applyHeightToLastDescent: false),
|
|
);
|
|
});
|
|
|
|
testWidgets(
|
|
'Text respects MediaQueryData.lineHeightScaleFactorOverride, MediaQueryData.letterSpacingOverride, and MediaQueryData.wordSpacingOverride',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
const MediaQuery(
|
|
data: MediaQueryData(
|
|
lineHeightScaleFactorOverride: 2.0,
|
|
letterSpacingOverride: 2.0,
|
|
wordSpacingOverride: 2.0,
|
|
),
|
|
child: Center(child: Text('Hello', textDirection: TextDirection.ltr)),
|
|
),
|
|
);
|
|
|
|
RichText text = tester.firstWidget(find.byType(RichText));
|
|
expect(text, isNotNull);
|
|
expect(text.text.style?.height, 2.0);
|
|
expect(text.text.style?.letterSpacing, 2.0);
|
|
expect(text.text.style?.wordSpacing, 2.0);
|
|
|
|
await tester.pumpWidget(const Center(child: Text('Hello', textDirection: TextDirection.ltr)));
|
|
|
|
text = tester.firstWidget(find.byType(RichText));
|
|
expect(text, isNotNull);
|
|
expect(text.text.style?.height, isNull);
|
|
expect(text.text.style?.letterSpacing, isNull);
|
|
expect(text.text.style?.wordSpacing, isNull);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'Text.rich respects MediaQueryData.lineHeightScaleFactorOverride, MediaQueryData.letterSpacingOverride, and MediaQueryData.wordSpacingOverride',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
const MediaQuery(
|
|
data: MediaQueryData(
|
|
lineHeightScaleFactorOverride: 2.0,
|
|
letterSpacingOverride: 2.0,
|
|
wordSpacingOverride: 2.0,
|
|
),
|
|
child: Center(
|
|
child: Text.rich(
|
|
TextSpan(text: 'Hello'),
|
|
textDirection: TextDirection.ltr,
|
|
strutStyle: StrutStyle(height: 0.9),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
RichText text = tester.firstWidget(find.byType(RichText));
|
|
expect(text, isNotNull);
|
|
expect(text.text.style?.height, 2.0);
|
|
expect(text.text.style?.letterSpacing, 2.0);
|
|
expect(text.text.style?.wordSpacing, 2.0);
|
|
expect(text.strutStyle?.height, 2.0);
|
|
|
|
await tester.pumpWidget(
|
|
const Center(
|
|
child: Text.rich(
|
|
TextSpan(text: 'Hello'),
|
|
textDirection: TextDirection.ltr,
|
|
strutStyle: StrutStyle(height: 0.9),
|
|
),
|
|
),
|
|
);
|
|
|
|
text = tester.firstWidget(find.byType(RichText));
|
|
expect(text, isNotNull);
|
|
expect(text.text.style?.height, isNull);
|
|
expect(text.text.style?.letterSpacing, isNull);
|
|
expect(text.text.style?.wordSpacing, isNull);
|
|
expect(text.strutStyle?.height, 0.9);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'RichText ignores MediaQueryData.lineHeightScaleFactorOverride, MediaQueryData.letterSpacingOverride, and MediaQueryData.wordSpacingOverride',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MediaQuery(
|
|
data: const MediaQueryData(
|
|
lineHeightScaleFactorOverride: 2.0,
|
|
letterSpacingOverride: 2.0,
|
|
wordSpacingOverride: 2.0,
|
|
),
|
|
child: Center(
|
|
child: RichText(
|
|
text: const TextSpan(text: 'Hello'),
|
|
textDirection: TextDirection.ltr,
|
|
strutStyle: const StrutStyle(height: 0.9),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RichText text = tester.firstWidget(find.byType(RichText));
|
|
expect(text, isNotNull);
|
|
expect(text.text.style?.height, isNull);
|
|
expect(text.text.style?.letterSpacing, isNull);
|
|
expect(text.text.style?.wordSpacing, isNull);
|
|
expect(text.strutStyle?.height, 0.9);
|
|
},
|
|
);
|
|
|
|
testWidgets('Text respects media query', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MediaQuery.withClampedTextScaling(
|
|
minScaleFactor: 1.3,
|
|
maxScaleFactor: 1.3,
|
|
child: const Center(child: Text('Hello', textDirection: TextDirection.ltr)),
|
|
),
|
|
);
|
|
|
|
RichText text = tester.firstWidget(find.byType(RichText));
|
|
expect(text, isNotNull);
|
|
expect(text.textScaler, const TextScaler.linear(1.3));
|
|
|
|
await tester.pumpWidget(const Center(child: Text('Hello', textDirection: TextDirection.ltr)));
|
|
|
|
text = tester.firstWidget(find.byType(RichText));
|
|
expect(text, isNotNull);
|
|
expect(text.textScaler, isSystemTextScaler(withScaleFactor: 1.0));
|
|
});
|
|
|
|
testWidgets('Text respects textScaleFactor with default font size', (WidgetTester tester) async {
|
|
await tester.pumpWidget(const Center(child: Text('Hello', textDirection: TextDirection.ltr)));
|
|
|
|
RichText text = tester.firstWidget(find.byType(RichText));
|
|
expect(text, isNotNull);
|
|
expect(text.textScaler, isSystemTextScaler(withScaleFactor: 1.0));
|
|
final Size baseSize = tester.getSize(find.byType(RichText));
|
|
expect(baseSize.width, equals(70.0));
|
|
expect(baseSize.height, equals(14.0));
|
|
|
|
await tester.pumpWidget(
|
|
const Center(child: Text('Hello', textScaleFactor: 1.5, textDirection: TextDirection.ltr)),
|
|
);
|
|
|
|
text = tester.firstWidget(find.byType(RichText));
|
|
expect(text, isNotNull);
|
|
expect(text.textScaler, const TextScaler.linear(1.5));
|
|
final Size largeSize = tester.getSize(find.byType(RichText));
|
|
expect(largeSize.width, 105.0);
|
|
expect(largeSize.height, equals(21.0));
|
|
});
|
|
|
|
testWidgets('Text respects textScaleFactor with explicit font size', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
const Center(
|
|
child: Text('Hello', style: TextStyle(fontSize: 20.0), textDirection: TextDirection.ltr),
|
|
),
|
|
);
|
|
|
|
RichText text = tester.firstWidget(find.byType(RichText));
|
|
expect(text, isNotNull);
|
|
expect(text.textScaler, isSystemTextScaler(withScaleFactor: 1.0));
|
|
final Size baseSize = tester.getSize(find.byType(RichText));
|
|
expect(baseSize.width, equals(100.0));
|
|
expect(baseSize.height, equals(20.0));
|
|
|
|
await tester.pumpWidget(
|
|
const Center(
|
|
child: Text(
|
|
'Hello',
|
|
style: TextStyle(fontSize: 20.0),
|
|
textScaleFactor: 1.3,
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
),
|
|
);
|
|
|
|
text = tester.firstWidget(find.byType(RichText));
|
|
expect(text, isNotNull);
|
|
expect(text.textScaler, const TextScaler.linear(1.3));
|
|
final Size largeSize = tester.getSize(find.byType(RichText));
|
|
expect(largeSize.width, 130.0);
|
|
expect(largeSize.height, equals(26.0));
|
|
});
|
|
|
|
testWidgets(
|
|
"Text throws a nice error message if there's no Directionality",
|
|
experimentalLeakTesting: LeakTesting.settings
|
|
.withIgnoredAll(), // leaking by design because of exception
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(const Text('Hello'));
|
|
final message = tester.takeException().toString();
|
|
expect(message, contains('Directionality'));
|
|
expect(message, contains(' Text '));
|
|
},
|
|
);
|
|
|
|
testWidgets('Text can be created from TextSpans and uses defaultTextStyle', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
const DefaultTextStyle(
|
|
style: TextStyle(fontSize: 20.0),
|
|
child: Text.rich(
|
|
TextSpan(
|
|
text: 'Hello',
|
|
children: <TextSpan>[
|
|
TextSpan(
|
|
text: ' beautiful ',
|
|
style: TextStyle(fontStyle: FontStyle.italic),
|
|
),
|
|
TextSpan(
|
|
text: 'world',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
),
|
|
);
|
|
|
|
final RichText text = tester.firstWidget(find.byType(RichText));
|
|
expect(text, isNotNull);
|
|
expect(text.text.style!.fontSize, 20.0);
|
|
});
|
|
|
|
testWidgets('inline widgets works with ellipsis', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/35869
|
|
const textStyle = TextStyle();
|
|
await tester.pumpWidget(
|
|
Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
const TextSpan(text: 'a very very very very very very very very very very long line'),
|
|
WidgetSpan(
|
|
child: SizedBox(
|
|
width: 20,
|
|
height: 40,
|
|
child: Card(
|
|
child: RichText(
|
|
text: const TextSpan(text: 'widget should be truncated'),
|
|
textDirection: TextDirection.rtl,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
style: textStyle,
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
);
|
|
expect(tester.takeException(), null);
|
|
});
|
|
|
|
testWidgets('inline widgets hitTest works with ellipsis', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/68559
|
|
const textStyle = TextStyle();
|
|
await tester.pumpWidget(
|
|
Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
const TextSpan(text: 'a very very very very very very very very very very long line'),
|
|
WidgetSpan(
|
|
child: SizedBox(
|
|
width: 20,
|
|
height: 40,
|
|
child: Card(
|
|
child: RichText(
|
|
text: const TextSpan(text: 'widget should be truncated'),
|
|
textDirection: TextDirection.rtl,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
style: textStyle,
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(Text));
|
|
|
|
expect(tester.takeException(), null);
|
|
});
|
|
|
|
testWidgets('inline widgets works with textScaleFactor', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/59316
|
|
final key = UniqueKey();
|
|
var textScaleFactor = 1.0;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(useMaterial3: false),
|
|
home: Scaffold(
|
|
appBar: AppBar(title: const Text('title')),
|
|
body: Center(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
WidgetSpan(
|
|
child: RichText(
|
|
text: const TextSpan(text: 'widget should be truncated'),
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
key: key,
|
|
textDirection: TextDirection.ltr,
|
|
textScaleFactor: textScaleFactor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
RenderBox renderText = tester.renderObject(find.byKey(key));
|
|
final double singleLineHeight = renderText.size.height;
|
|
// Now, increases the text scale factor by 5 times.
|
|
textScaleFactor = textScaleFactor * 5;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(useMaterial3: false),
|
|
home: Scaffold(
|
|
appBar: AppBar(title: const Text('title')),
|
|
body: Center(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
WidgetSpan(
|
|
child: RichText(
|
|
text: const TextSpan(text: 'widget should be truncated'),
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
key: key,
|
|
textDirection: TextDirection.ltr,
|
|
textScaleFactor: textScaleFactor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
renderText = tester.renderObject(find.byKey(key));
|
|
// The RichText in the widget span should wrap into three lines.
|
|
expect(renderText.size.height, singleLineHeight * textScaleFactor * 3);
|
|
});
|
|
|
|
testWidgets("Inline widgets' scaled sizes are constrained", (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/130588
|
|
await tester.pumpWidget(
|
|
const Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: 502.5454545454545,
|
|
child: Text.rich(WidgetSpan(child: Row()), textScaleFactor: 0.95),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('semanticsLabel can override text label', (WidgetTester tester) async {
|
|
final semantics = SemanticsTester(tester);
|
|
await tester.pumpWidget(
|
|
const Text(r'$$', semanticsLabel: 'Double dollars', textDirection: TextDirection.ltr),
|
|
);
|
|
final expectedSemantics = TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics.rootChild(label: 'Double dollars', textDirection: TextDirection.ltr),
|
|
],
|
|
);
|
|
expect(
|
|
semantics,
|
|
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
const Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Text(r'$$', semanticsLabel: 'Double dollars'),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true),
|
|
);
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('semantics label is in order when uses widget span', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
const TextSpan(text: 'before '),
|
|
WidgetSpan(
|
|
alignment: PlaceholderAlignment.baseline,
|
|
baseline: TextBaseline.alphabetic,
|
|
child: Semantics(label: 'foo'),
|
|
),
|
|
const TextSpan(text: ' after'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(tester.getSemantics(find.byType(Text)), matchesSemantics(label: 'before \nfoo\n after'));
|
|
|
|
// If the Paragraph is not dirty it should use the cache correctly.
|
|
final RenderObject parent = tester.renderObject<RenderObject>(find.byType(Directionality));
|
|
parent.markNeedsSemanticsUpdate();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(tester.getSemantics(find.byType(Text)), matchesSemantics(label: 'before \nfoo\n after'));
|
|
});
|
|
|
|
testWidgets('semantics can handle some widget spans without semantics', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
const TextSpan(text: 'before '),
|
|
const WidgetSpan(
|
|
alignment: PlaceholderAlignment.baseline,
|
|
baseline: TextBaseline.alphabetic,
|
|
child: SizedBox(width: 10.0),
|
|
),
|
|
const TextSpan(text: ' mid'),
|
|
WidgetSpan(
|
|
alignment: PlaceholderAlignment.baseline,
|
|
baseline: TextBaseline.alphabetic,
|
|
child: Semantics(label: 'foo'),
|
|
),
|
|
const TextSpan(text: ' after'),
|
|
const WidgetSpan(
|
|
alignment: PlaceholderAlignment.baseline,
|
|
baseline: TextBaseline.alphabetic,
|
|
child: SizedBox(width: 10.0),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byType(Text)),
|
|
matchesSemantics(label: 'before \n mid\nfoo\n after'),
|
|
);
|
|
|
|
// If the Paragraph is not dirty it should use the cache correctly.
|
|
final RenderObject parent = tester.renderObject<RenderObject>(find.byType(Directionality));
|
|
parent.markNeedsSemanticsUpdate();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester.getSemantics(find.byType(Text)),
|
|
matchesSemantics(label: 'before \n mid\nfoo\n after'),
|
|
);
|
|
});
|
|
|
|
testWidgets('semantics can handle all widget spans without semantics', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
const Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'before '),
|
|
WidgetSpan(
|
|
alignment: PlaceholderAlignment.baseline,
|
|
baseline: TextBaseline.alphabetic,
|
|
child: SizedBox(width: 10.0),
|
|
),
|
|
TextSpan(text: ' mid'),
|
|
WidgetSpan(
|
|
alignment: PlaceholderAlignment.baseline,
|
|
baseline: TextBaseline.alphabetic,
|
|
child: SizedBox(width: 10.0),
|
|
),
|
|
TextSpan(text: ' after'),
|
|
WidgetSpan(
|
|
alignment: PlaceholderAlignment.baseline,
|
|
baseline: TextBaseline.alphabetic,
|
|
child: SizedBox(width: 10.0),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byType(Text)),
|
|
matchesSemantics(label: 'before \n mid\n after'),
|
|
);
|
|
|
|
// If the Paragraph is not dirty it should use the cache correctly.
|
|
final RenderObject parent = tester.renderObject<RenderObject>(find.byType(Directionality));
|
|
parent.markNeedsSemanticsUpdate();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester.getSemantics(find.byType(Text)),
|
|
matchesSemantics(label: 'before \n mid\n after'),
|
|
);
|
|
});
|
|
|
|
testWidgets('semantics can handle widget spans with explicit semantics node', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
const TextSpan(text: 'before '),
|
|
WidgetSpan(
|
|
alignment: PlaceholderAlignment.baseline,
|
|
baseline: TextBaseline.alphabetic,
|
|
child: Semantics(label: 'inner', container: true),
|
|
),
|
|
const TextSpan(text: ' after'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byType(Text)),
|
|
matchesSemantics(
|
|
label: 'before \n after',
|
|
children: <Matcher>[matchesSemantics(label: 'inner')],
|
|
),
|
|
);
|
|
|
|
// If the Paragraph is not dirty it should use the cache correctly.
|
|
final RenderObject parent = tester.renderObject<RenderObject>(find.byType(Directionality));
|
|
parent.markNeedsSemanticsUpdate();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester.getSemantics(find.byType(Text)),
|
|
matchesSemantics(
|
|
label: 'before \n after',
|
|
children: <Matcher>[matchesSemantics(label: 'inner')],
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('semanticsLabel can be shorter than text', (WidgetTester tester) async {
|
|
final semantics = SemanticsTester(tester);
|
|
final recognizer = TapGestureRecognizer();
|
|
addTearDown(recognizer.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: RichText(
|
|
text: TextSpan(
|
|
children: <InlineSpan>[
|
|
const TextSpan(text: 'Some Text', semanticsLabel: ''),
|
|
TextSpan(text: 'Clickable', recognizer: recognizer..onTap = () {}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final expectedSemantics = TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
children: <TestSemantics>[
|
|
TestSemantics(textDirection: TextDirection.ltr),
|
|
TestSemantics(
|
|
label: 'Clickable',
|
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
|
flags: <SemanticsFlag>[SemanticsFlag.isLink],
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
expect(
|
|
semantics,
|
|
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true),
|
|
);
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('recognizers split semantic node', (WidgetTester tester) async {
|
|
final semantics = SemanticsTester(tester);
|
|
const textStyle = TextStyle();
|
|
final recognizer = TapGestureRecognizer();
|
|
addTearDown(recognizer.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Text.rich(
|
|
TextSpan(
|
|
children: <TextSpan>[
|
|
const TextSpan(text: 'hello '),
|
|
TextSpan(text: 'world', recognizer: recognizer..onTap = () {}),
|
|
const TextSpan(text: ' this is a '),
|
|
const TextSpan(text: 'cat-astrophe'),
|
|
],
|
|
style: textStyle,
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
);
|
|
final expectedSemantics = TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics.rootChild(
|
|
children: <TestSemantics>[
|
|
TestSemantics(label: 'hello ', textDirection: TextDirection.ltr),
|
|
TestSemantics(
|
|
label: 'world',
|
|
textDirection: TextDirection.ltr,
|
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
|
flags: <SemanticsFlag>[SemanticsFlag.isLink],
|
|
),
|
|
TestSemantics(label: ' this is a cat-astrophe', textDirection: TextDirection.ltr),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
expect(
|
|
semantics,
|
|
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true),
|
|
);
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('semantic nodes of offscreen recognizers are marked hidden', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/100395.
|
|
final semantics = SemanticsTester(tester);
|
|
const textStyle = TextStyle(fontSize: 200);
|
|
const onScreenText = 'onscreen\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n';
|
|
const offScreenText = 'off screen';
|
|
final controller = ScrollController();
|
|
addTearDown(controller.dispose);
|
|
final recognizer = TapGestureRecognizer();
|
|
addTearDown(recognizer.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
SingleChildScrollView(
|
|
controller: controller,
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: <TextSpan>[
|
|
const TextSpan(text: onScreenText),
|
|
TextSpan(text: offScreenText, recognizer: recognizer..onTap = () {}),
|
|
],
|
|
style: textStyle,
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
),
|
|
);
|
|
|
|
final expectedSemantics = TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
|
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollToOffset],
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
children: <TestSemantics>[
|
|
TestSemantics(label: onScreenText, textDirection: TextDirection.ltr),
|
|
TestSemantics(
|
|
label: offScreenText,
|
|
textDirection: TextDirection.ltr,
|
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
|
flags: <SemanticsFlag>[SemanticsFlag.isLink, SemanticsFlag.isHidden],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
expect(
|
|
semantics,
|
|
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true),
|
|
);
|
|
|
|
// Test show on screen.
|
|
expect(controller.offset, 0.0);
|
|
tester.binding.pipelineOwner.semanticsOwner!.performAction(4, SemanticsAction.showOnScreen);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.offset != 0.0, isTrue);
|
|
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('recognizers split semantic node when TextSpan overflows', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final semantics = SemanticsTester(tester);
|
|
const textStyle = TextStyle();
|
|
final recognizer = TapGestureRecognizer();
|
|
addTearDown(recognizer.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
SizedBox(
|
|
height: 10,
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: <TextSpan>[
|
|
const TextSpan(text: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'),
|
|
TextSpan(text: 'world', recognizer: recognizer..onTap = () {}),
|
|
],
|
|
style: textStyle,
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
),
|
|
);
|
|
final expectedSemantics = TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics.rootChild(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
label: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n',
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
TestSemantics(
|
|
label: 'world',
|
|
textDirection: TextDirection.ltr,
|
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
|
flags: <SemanticsFlag>[SemanticsFlag.isLink],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
expect(
|
|
semantics,
|
|
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true),
|
|
);
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('recognizers split semantic nodes with text span labels', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final semantics = SemanticsTester(tester);
|
|
const textStyle = TextStyle();
|
|
final recognizer = TapGestureRecognizer();
|
|
addTearDown(recognizer.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Text.rich(
|
|
TextSpan(
|
|
children: <TextSpan>[
|
|
const TextSpan(text: 'hello '),
|
|
TextSpan(text: 'world', recognizer: recognizer..onTap = () {}),
|
|
const TextSpan(text: ' this is a '),
|
|
const TextSpan(text: 'cat-astrophe', semanticsLabel: 'regrettable event'),
|
|
],
|
|
style: textStyle,
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
);
|
|
final expectedSemantics = TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics.rootChild(
|
|
children: <TestSemantics>[
|
|
TestSemantics(label: 'hello ', textDirection: TextDirection.ltr),
|
|
TestSemantics(
|
|
label: 'world',
|
|
textDirection: TextDirection.ltr,
|
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
|
flags: <SemanticsFlag>[SemanticsFlag.isLink],
|
|
),
|
|
TestSemantics(label: ' this is a regrettable event', textDirection: TextDirection.ltr),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
expect(
|
|
semantics,
|
|
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true),
|
|
);
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('recognizers split semantic node - bidi', (WidgetTester tester) async {
|
|
final semantics = SemanticsTester(tester);
|
|
const textStyle = TextStyle();
|
|
final recognizer1 = LongPressGestureRecognizer();
|
|
addTearDown(recognizer1.dispose);
|
|
final recognizer2 = TapGestureRecognizer();
|
|
addTearDown(recognizer2.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
RichText(
|
|
text: TextSpan(
|
|
style: textStyle,
|
|
children: <TextSpan>[
|
|
const TextSpan(text: 'hello world${Unicode.RLE}${Unicode.RLO} '),
|
|
TextSpan(text: 'BOY', recognizer: recognizer1..onLongPress = () {}),
|
|
const TextSpan(text: ' HOW DO${Unicode.PDF} you ${Unicode.RLO} DO '),
|
|
TextSpan(text: 'SIR', recognizer: recognizer2..onTap = () {}),
|
|
const TextSpan(text: '${Unicode.PDF}${Unicode.PDF} good bye'),
|
|
],
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
);
|
|
// The expected visual order of the text is:
|
|
// hello world RIS OD you OD WOH YOB good bye
|
|
// There are five unique text areas, they are, in visual order but
|
|
// showing the logical text:
|
|
// [hello world][SIR][HOW DO you DO][BOY][good bye]
|
|
// The direction of each varies based on the first bit of that area.
|
|
// The presence of the bidi formatting characters in the text is a
|
|
// bit dubious, but that's what we do currently, and it's not really
|
|
// clear what the perfect behavior would be...
|
|
final expectedSemantics = TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics.rootChild(
|
|
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
rect: const Rect.fromLTRB(-4.0, -4.0, 480.0, 18.0),
|
|
label: 'hello world${Unicode.RLE}${Unicode.RLO} ',
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
TestSemantics(
|
|
rect: const Rect.fromLTRB(416.0, -4.0, 466.0, 18.0),
|
|
label: 'BOY',
|
|
textDirection: TextDirection.rtl,
|
|
actions: <SemanticsAction>[SemanticsAction.longPress],
|
|
),
|
|
TestSemantics(
|
|
rect: const Rect.fromLTRB(192.0, -4.0, 424.0, 18.0),
|
|
label: ' HOW DO${Unicode.PDF} you ${Unicode.RLO} DO ',
|
|
textDirection: TextDirection.rtl,
|
|
),
|
|
TestSemantics(
|
|
rect: const Rect.fromLTRB(150.0, -4.0, 200.0, 18.0),
|
|
label: 'SIR',
|
|
textDirection: TextDirection.rtl,
|
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
|
flags: <SemanticsFlag>[SemanticsFlag.isLink],
|
|
),
|
|
TestSemantics(
|
|
rect: const Rect.fromLTRB(472.0, -4.0, 606.0, 18.0),
|
|
label: '${Unicode.PDF}${Unicode.PDF} good bye',
|
|
textDirection: TextDirection.rtl,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true));
|
|
semantics.dispose();
|
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/62945
|
|
|
|
testWidgets('TapGesture recognizers contribute link semantics', (WidgetTester tester) async {
|
|
final semantics = SemanticsTester(tester);
|
|
const textStyle = TextStyle();
|
|
final recognizer = TapGestureRecognizer();
|
|
addTearDown(recognizer.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Text.rich(
|
|
TextSpan(
|
|
children: <TextSpan>[TextSpan(text: 'click me', recognizer: recognizer..onTap = () {})],
|
|
style: textStyle,
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
);
|
|
final expectedSemantics = TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics.rootChild(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
label: 'click me',
|
|
textDirection: TextDirection.ltr,
|
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
|
flags: <SemanticsFlag>[SemanticsFlag.isLink],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
expect(
|
|
semantics,
|
|
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true),
|
|
);
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('inline widgets generate semantic nodes', (WidgetTester tester) async {
|
|
final semantics = SemanticsTester(tester);
|
|
const textStyle = TextStyle();
|
|
final recognizer = TapGestureRecognizer();
|
|
addTearDown(recognizer.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
const TextSpan(text: 'a '),
|
|
TextSpan(text: 'pebble', recognizer: recognizer..onTap = () {}),
|
|
const TextSpan(text: ' in the '),
|
|
WidgetSpan(
|
|
child: SizedBox(
|
|
width: 20,
|
|
height: 40,
|
|
child: Card(
|
|
child: RichText(
|
|
text: const TextSpan(text: 'INTERRUPTION'),
|
|
textDirection: TextDirection.rtl,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const TextSpan(text: 'sky'),
|
|
],
|
|
style: textStyle,
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
);
|
|
final expectedSemantics = TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics.rootChild(
|
|
children: <TestSemantics>[
|
|
TestSemantics(label: 'a ', textDirection: TextDirection.ltr),
|
|
TestSemantics(
|
|
label: 'pebble',
|
|
textDirection: TextDirection.ltr,
|
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
|
flags: <SemanticsFlag>[SemanticsFlag.isLink],
|
|
),
|
|
TestSemantics(label: ' in the ', textDirection: TextDirection.ltr),
|
|
TestSemantics(label: 'INTERRUPTION', textDirection: TextDirection.rtl),
|
|
TestSemantics(label: 'sky', textDirection: TextDirection.ltr),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
expect(
|
|
semantics,
|
|
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true),
|
|
);
|
|
semantics.dispose();
|
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/62945
|
|
|
|
testWidgets('inline widgets semantic nodes scale', (WidgetTester tester) async {
|
|
final semantics = SemanticsTester(tester);
|
|
const textStyle = TextStyle();
|
|
final recognizer = TapGestureRecognizer();
|
|
addTearDown(recognizer.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
const TextSpan(text: 'a '),
|
|
TextSpan(text: 'pebble', recognizer: recognizer..onTap = () {}),
|
|
const TextSpan(text: ' in the '),
|
|
WidgetSpan(
|
|
child: SizedBox(
|
|
width: 20,
|
|
height: 40,
|
|
child: Card(
|
|
child: RichText(
|
|
text: const TextSpan(text: 'INTERRUPTION'),
|
|
textDirection: TextDirection.rtl,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const TextSpan(text: 'sky'),
|
|
],
|
|
style: textStyle,
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
textScaleFactor: 2,
|
|
),
|
|
);
|
|
final expectedSemantics = TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics.rootChild(
|
|
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
label: 'a ',
|
|
textDirection: TextDirection.ltr,
|
|
rect: const Rect.fromLTRB(-4.0, 48.0, 60.0, 84.0),
|
|
),
|
|
TestSemantics(
|
|
label: 'pebble',
|
|
textDirection: TextDirection.ltr,
|
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
|
flags: <SemanticsFlag>[SemanticsFlag.isLink],
|
|
rect: const Rect.fromLTRB(52.0, 48.0, 228.0, 84.0),
|
|
),
|
|
TestSemantics(
|
|
label: ' in the ',
|
|
textDirection: TextDirection.ltr,
|
|
rect: const Rect.fromLTRB(220.0, 48.0, 452.0, 84.0),
|
|
),
|
|
TestSemantics(
|
|
label: 'INTERRUPTION',
|
|
textDirection: TextDirection.rtl,
|
|
rect: const Rect.fromLTRB(0.0, 0.0, 20.0, 40.0),
|
|
),
|
|
TestSemantics(
|
|
label: 'sky',
|
|
textDirection: TextDirection.ltr,
|
|
rect: const Rect.fromLTRB(484.0, 48.0, 576.0, 84.0),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true));
|
|
semantics.dispose();
|
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/62945
|
|
|
|
testWidgets('receives fontFamilyFallback and package from root ThemeData', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const fontFamily = 'fontFamily';
|
|
const package = 'package_name';
|
|
final fontFamilyFallback = <String>['font', 'family', 'fallback'];
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(
|
|
fontFamily: fontFamily,
|
|
fontFamilyFallback: fontFamilyFallback,
|
|
package: package,
|
|
primarySwatch: Colors.blue,
|
|
),
|
|
home: const Scaffold(body: Center(child: Text('foo'))),
|
|
),
|
|
);
|
|
|
|
expect(find.byType(RichText), findsOneWidget);
|
|
final RichText richText = tester.widget(find.byType(RichText));
|
|
final InlineSpan text = richText.text;
|
|
final TextStyle? style = text.style;
|
|
expect(style?.fontFamily, equals('packages/$package/$fontFamily'));
|
|
for (var i = 0; i < fontFamilyFallback.length; i++) {
|
|
final String fallback = fontFamilyFallback[i];
|
|
expect(style?.fontFamilyFallback?[i], equals('packages/$package/$fallback'));
|
|
}
|
|
});
|
|
|
|
testWidgets('Overflow is clipping correctly - short text with overflow: clip', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpTextWidget(tester: tester, overflow: TextOverflow.clip, text: 'Hi');
|
|
|
|
expect(find.byType(Text), isNot(paints..clipRect()));
|
|
});
|
|
|
|
testWidgets('Overflow is clipping correctly - long text with overflow: ellipsis', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpTextWidget(
|
|
tester: tester,
|
|
overflow: TextOverflow.ellipsis,
|
|
text: 'a long long long long text, should be clip',
|
|
);
|
|
|
|
expect(find.byType(Text), paints..clipRect(rect: const Rect.fromLTWH(0, 0, 50, 50)));
|
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/87878
|
|
|
|
testWidgets('Overflow is clipping correctly - short text with overflow: ellipsis', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpTextWidget(tester: tester, overflow: TextOverflow.ellipsis, text: 'Hi');
|
|
|
|
expect(find.byType(Text), isNot(paints..clipRect()));
|
|
});
|
|
|
|
testWidgets('Overflow is clipping correctly - long text with overflow: fade', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpTextWidget(
|
|
tester: tester,
|
|
overflow: TextOverflow.fade,
|
|
text: 'a long long long long text, should be clip',
|
|
);
|
|
|
|
expect(find.byType(Text), paints..clipRect(rect: const Rect.fromLTWH(0, 0, 50, 50)));
|
|
});
|
|
|
|
testWidgets('Overflow is clipping correctly - short text with overflow: fade', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpTextWidget(tester: tester, overflow: TextOverflow.fade, text: 'Hi');
|
|
|
|
expect(find.byType(Text), isNot(paints..clipRect()));
|
|
});
|
|
|
|
testWidgets('Overflow is clipping correctly - long text with overflow: visible', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpTextWidget(
|
|
tester: tester,
|
|
overflow: TextOverflow.visible,
|
|
text: 'a long long long long text, should be clip',
|
|
);
|
|
|
|
expect(find.byType(Text), isNot(paints..clipRect()));
|
|
});
|
|
|
|
testWidgets('Overflow is clipping correctly - short text with overflow: visible', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await _pumpTextWidget(tester: tester, overflow: TextOverflow.visible, text: 'Hi');
|
|
|
|
expect(find.byType(Text), isNot(paints..clipRect()));
|
|
});
|
|
|
|
testWidgets('textWidthBasis affects the width of a Text widget', (WidgetTester tester) async {
|
|
Future<void> createText(TextWidthBasis textWidthBasis) {
|
|
return tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(useMaterial3: false),
|
|
home: Scaffold(
|
|
body: Center(
|
|
// Each word takes up more than a half of a line. Together they
|
|
// wrap onto two lines, but leave a lot of extra space.
|
|
child: Text(
|
|
'twowordsthateachtakeupmorethanhalfof alineoftextsothattheywrapwithlotsofextraspace',
|
|
textDirection: TextDirection.ltr,
|
|
textWidthBasis: textWidthBasis,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
const fontHeight = 14.0;
|
|
const screenWidth = 800.0;
|
|
|
|
// When textWidthBasis is parent, takes up full screen width.
|
|
await createText(TextWidthBasis.parent);
|
|
final Size textSizeParent = tester.getSize(find.byType(Text));
|
|
expect(textSizeParent.width, equals(screenWidth));
|
|
expect(textSizeParent.height, equals(fontHeight * 2));
|
|
|
|
// When textWidthBasis is longestLine, sets the width to as small as
|
|
// possible for the two lines.
|
|
await createText(TextWidthBasis.longestLine);
|
|
final Size textSizeLongestLine = tester.getSize(find.byType(Text));
|
|
expect(textSizeLongestLine.width, equals(630.0));
|
|
expect(textSizeLongestLine.height, equals(fontHeight * 2));
|
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/44020
|
|
|
|
testWidgets('textWidthBasis with textAlign still obeys parent alignment', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
Text(
|
|
'LEFT ALIGNED, PARENT',
|
|
textAlign: TextAlign.left,
|
|
textWidthBasis: TextWidthBasis.parent,
|
|
),
|
|
Text(
|
|
'RIGHT ALIGNED, PARENT',
|
|
textAlign: TextAlign.right,
|
|
textWidthBasis: TextWidthBasis.parent,
|
|
),
|
|
Text(
|
|
'LEFT ALIGNED, LONGEST LINE',
|
|
textAlign: TextAlign.left,
|
|
textWidthBasis: TextWidthBasis.longestLine,
|
|
),
|
|
Text(
|
|
'RIGHT ALIGNED, LONGEST LINE',
|
|
textAlign: TextAlign.right,
|
|
textWidthBasis: TextWidthBasis.longestLine,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// All Texts have the same horizontal alignment.
|
|
final double offsetX = tester.getTopLeft(find.text('LEFT ALIGNED, PARENT')).dx;
|
|
expect(tester.getTopLeft(find.text('RIGHT ALIGNED, PARENT')).dx, equals(offsetX));
|
|
expect(tester.getTopLeft(find.text('LEFT ALIGNED, LONGEST LINE')).dx, equals(offsetX));
|
|
expect(tester.getTopLeft(find.text('RIGHT ALIGNED, LONGEST LINE')).dx, equals(offsetX));
|
|
|
|
// All Texts are less than or equal to the width of the Column.
|
|
final double width = tester.getSize(find.byType(Column)).width;
|
|
expect(tester.getSize(find.text('LEFT ALIGNED, PARENT')).width, lessThan(width));
|
|
expect(tester.getSize(find.text('RIGHT ALIGNED, PARENT')).width, lessThan(width));
|
|
expect(tester.getSize(find.text('LEFT ALIGNED, LONGEST LINE')).width, lessThan(width));
|
|
expect(tester.getSize(find.text('RIGHT ALIGNED, LONGEST LINE')).width, equals(width));
|
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/44020
|
|
|
|
testWidgets(
|
|
'textWidthBasis.longestLine confines the width of the paragraph '
|
|
'when given loose constraints',
|
|
(WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/62550.
|
|
await tester.pumpWidget(
|
|
Center(
|
|
child: SizedBox(
|
|
width: 400,
|
|
child: Center(
|
|
child: RichText(
|
|
// 400 is not wide enough for this string. The part after the
|
|
// whitespace is going to be broken into a 2nd line.
|
|
text: const TextSpan(
|
|
text: 'fwefwefwewfefewfwe fwfwfwefweabcdefghijklmnopqrstuvwxyz',
|
|
),
|
|
textWidthBasis: TextWidthBasis.longestLine,
|
|
textDirection: TextDirection.rtl,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
find.byType(RichText),
|
|
paints..something((Symbol method, List<dynamic> arguments) {
|
|
if (method != #drawParagraph) {
|
|
return false;
|
|
}
|
|
final paragraph = arguments[0] as ui.Paragraph;
|
|
final offset = arguments[1] as Offset;
|
|
final List<ui.LineMetrics> lines = paragraph.computeLineMetrics();
|
|
for (final line in lines) {
|
|
if (line.left + offset.dx + line.width >= 400) {
|
|
throw 'line $line is greater than the max width constraints';
|
|
}
|
|
}
|
|
return true;
|
|
}),
|
|
);
|
|
},
|
|
skip: isBrowser, // https://github.com/flutter/flutter/issues/44020
|
|
);
|
|
|
|
testWidgets('Paragraph.getBoxesForRange returns nothing when selection range is zero length', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final builder = ui.ParagraphBuilder(ui.ParagraphStyle());
|
|
builder.addText('hello');
|
|
final ui.Paragraph paragraph = builder.build();
|
|
paragraph.layout(const ui.ParagraphConstraints(width: 1000));
|
|
expect(paragraph.getBoxesForRange(2, 2), isEmpty);
|
|
paragraph.dispose();
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/65818
|
|
testWidgets('WidgetSpans with no semantic information are elided from semantics', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final semantics = SemanticsTester(tester);
|
|
final recognizer = TapGestureRecognizer();
|
|
addTearDown(recognizer.dispose);
|
|
// Without the fix for this bug the pump widget will throw a RangeError.
|
|
await tester.pumpWidget(
|
|
RichText(
|
|
textDirection: TextDirection.ltr,
|
|
text: TextSpan(
|
|
children: <InlineSpan>[
|
|
const WidgetSpan(child: SizedBox.shrink()),
|
|
TextSpan(
|
|
text: 'HELLO',
|
|
style: const TextStyle(color: Colors.black),
|
|
recognizer: recognizer..onTap = () {},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 1,
|
|
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
|
|
transform: Matrix4(
|
|
3.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
3.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
1.0,
|
|
),
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
rect: const Rect.fromLTRB(-4.0, -4.0, 74.0, 18.0),
|
|
id: 2,
|
|
label: 'HELLO',
|
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
|
flags: <SemanticsFlag>[SemanticsFlag.isLink],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
semantics.dispose();
|
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/69787
|
|
testWidgets('WidgetSpans with no semantic information are elided from semantics - case 2', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final semantics = SemanticsTester(tester);
|
|
final recognizer = TapGestureRecognizer();
|
|
addTearDown(recognizer.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: RichText(
|
|
text: TextSpan(
|
|
children: <InlineSpan>[
|
|
const WidgetSpan(child: SizedBox.shrink()),
|
|
const WidgetSpan(child: Text('included')),
|
|
TextSpan(
|
|
text: 'HELLO',
|
|
style: const TextStyle(color: Colors.black),
|
|
recognizer: recognizer..onTap = () {},
|
|
),
|
|
const WidgetSpan(child: Text('included2')),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
children: <TestSemantics>[
|
|
TestSemantics(label: 'included'),
|
|
TestSemantics(
|
|
label: 'HELLO',
|
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
|
flags: <SemanticsFlag>[SemanticsFlag.isLink],
|
|
),
|
|
TestSemantics(label: 'included2'),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
ignoreId: true,
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
),
|
|
);
|
|
semantics.dispose();
|
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/69787
|
|
testWidgets('WidgetSpans with no semantic information are elided from semantics - case 3', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final semantics = SemanticsTester(tester);
|
|
final recognizer = TapGestureRecognizer();
|
|
addTearDown(recognizer.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: RichText(
|
|
text: TextSpan(
|
|
children: <InlineSpan>[
|
|
const WidgetSpan(child: SizedBox.shrink()),
|
|
WidgetSpan(
|
|
child: Row(
|
|
children: <Widget>[
|
|
Semantics(container: true, child: const Text('foo')),
|
|
Semantics(container: true, child: const Text('bar')),
|
|
],
|
|
),
|
|
),
|
|
TextSpan(
|
|
text: 'HELLO',
|
|
style: const TextStyle(color: Colors.black),
|
|
recognizer: recognizer..onTap = () {},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
children: <TestSemantics>[
|
|
TestSemantics(label: 'foo'),
|
|
TestSemantics(label: 'bar'),
|
|
TestSemantics(
|
|
label: 'HELLO',
|
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
|
flags: <SemanticsFlag>[SemanticsFlag.isLink],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
ignoreId: true,
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
),
|
|
);
|
|
semantics.dispose();
|
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/69787
|
|
testWidgets('WidgetSpans with no semantic information are elided from semantics - case 4', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final semantics = SemanticsTester(tester);
|
|
final recognizer = TapGestureRecognizer();
|
|
addTearDown(recognizer.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: ClipRect(
|
|
child: Container(
|
|
color: Colors.green,
|
|
height: 100,
|
|
width: 100,
|
|
child: OverflowBox(
|
|
alignment: Alignment.topLeft,
|
|
maxWidth: double.infinity,
|
|
child: RichText(
|
|
text: TextSpan(
|
|
children: <InlineSpan>[
|
|
const WidgetSpan(
|
|
child: Icon(Icons.edit, size: 16, semanticLabel: 'not clipped'),
|
|
),
|
|
TextSpan(text: 'next WS is clipped', recognizer: recognizer..onTap = () {}),
|
|
const WidgetSpan(child: Icon(Icons.edit, size: 16, semanticLabel: 'clipped')),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
children: <TestSemantics>[
|
|
TestSemantics(label: 'not clipped'),
|
|
TestSemantics(
|
|
label: 'next WS is clipped',
|
|
flags: <SemanticsFlag>[SemanticsFlag.isLink],
|
|
actions: <SemanticsAction>[SemanticsAction.tap],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
ignoreId: true,
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
),
|
|
);
|
|
semantics.dispose();
|
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/87877
|
|
|
|
testWidgets('RenderParagraph intrinsic width', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 100,
|
|
child: IntrinsicWidth(
|
|
child: RichText(
|
|
text: const TextSpan(
|
|
style: TextStyle(fontSize: 16, height: 1),
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'S '),
|
|
WidgetSpan(
|
|
alignment: PlaceholderAlignment.top,
|
|
child: Wrap(
|
|
direction: Axis.vertical,
|
|
children: <Widget>[
|
|
SizedBox(width: 200, height: 100),
|
|
SizedBox(width: 200, height: 30),
|
|
],
|
|
),
|
|
),
|
|
TextSpan(text: ' E'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.getSize(find.byType(RichText)).width, 200 + 4 * 16.0);
|
|
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(find.byType(RichText));
|
|
// The inline spans are rendered on one (horizontal) line, the sum of the widths is the max intrinsic width.
|
|
expect(paragraph.getMaxIntrinsicWidth(0.0), 200 + 4 * 16.0);
|
|
// The inline spans are rendered in one vertical run, the widest one determines the min intrinsic width.
|
|
expect(paragraph.getMinIntrinsicWidth(0.0), 200);
|
|
});
|
|
|
|
testWidgets('can compute intrinsic width and height for widget span with text scaling', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/59316
|
|
const textKey = Key('RichText');
|
|
Widget textWithNestedInlineSpans({
|
|
required double textScaleFactor,
|
|
required double screenWidth,
|
|
}) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: OverflowBox(
|
|
alignment: Alignment.topLeft,
|
|
maxWidth: screenWidth,
|
|
child: RichText(
|
|
key: textKey,
|
|
textScaleFactor: textScaleFactor,
|
|
text: const TextSpan(children: <InlineSpan>[WidgetSpan(child: Text('one two'))]),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// The render object is going to be reused across widget tree rebuilds.
|
|
late final RenderParagraph outerParagraph = tester.renderObject(find.byKey(textKey));
|
|
|
|
await tester.pumpWidget(textWithNestedInlineSpans(textScaleFactor: 1.0, screenWidth: 100.0));
|
|
expect(outerParagraph.getMaxIntrinsicHeight(100.0), 14.0, reason: 'singleLineHeight = 14.0');
|
|
|
|
await tester.pumpWidget(textWithNestedInlineSpans(textScaleFactor: 2.0, screenWidth: 100.0));
|
|
expect(
|
|
outerParagraph.getMinIntrinsicHeight(100.0),
|
|
14.0 * 2.0 * 2,
|
|
reason: 'intrinsicHeight = singleLineHeight * textScaleFactor * two lines.',
|
|
);
|
|
|
|
await tester.pumpWidget(textWithNestedInlineSpans(textScaleFactor: 1.0, screenWidth: 1000.0));
|
|
expect(
|
|
outerParagraph.getMaxIntrinsicWidth(1000.0),
|
|
14.0 * 7,
|
|
reason: 'intrinsic width = 14.0 * 7',
|
|
);
|
|
|
|
await tester.pumpWidget(textWithNestedInlineSpans(textScaleFactor: 2.0, screenWidth: 1000.0));
|
|
expect(
|
|
outerParagraph.getMaxIntrinsicWidth(1000.0),
|
|
14.0 * 2.0 * 7,
|
|
reason: 'intrinsic width = glyph advance * textScaleFactor * num of glyphs',
|
|
);
|
|
});
|
|
|
|
testWidgets('Text uses TextStyle.overflow', (WidgetTester tester) async {
|
|
const TextOverflow overflow = TextOverflow.fade;
|
|
|
|
await tester.pumpWidget(
|
|
const Text(
|
|
'Hello World',
|
|
textDirection: TextDirection.ltr,
|
|
style: TextStyle(overflow: overflow),
|
|
),
|
|
);
|
|
|
|
final RichText richText = tester.firstWidget(find.byType(RichText));
|
|
|
|
expect(richText.overflow, overflow);
|
|
expect(richText.text.style!.overflow, overflow);
|
|
});
|
|
|
|
testWidgets('Text can be hit-tested without layout or paint being called in a frame', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/85108.
|
|
await tester.pumpWidget(
|
|
const Opacity(
|
|
opacity: 1.0,
|
|
child: Text(
|
|
'Hello World',
|
|
textDirection: TextDirection.ltr,
|
|
style: TextStyle(color: Color(0xFF123456)),
|
|
),
|
|
),
|
|
);
|
|
|
|
// The color changed and the opacity is set to 0:
|
|
// * 0 opacity will prevent RenderParagraph.paint from being called.
|
|
// * Only changing the color will prevent RenderParagraph.performLayout
|
|
// from being called.
|
|
// The underlying TextPainter should not evict its layout cache in this
|
|
// case, for hit-testing.
|
|
await tester.pumpWidget(
|
|
const Opacity(
|
|
opacity: 0.0,
|
|
child: Text(
|
|
'Hello World',
|
|
textDirection: TextDirection.ltr,
|
|
style: TextStyle(color: Color(0x87654321)),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('Hello World'));
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('Mouse hovering over selectable Text uses SystemMouseCursor.text', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(const MaterialApp(home: SelectionArea(child: Text('Flutter'))));
|
|
|
|
final TestGesture gesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
pointer: 1,
|
|
);
|
|
await gesture.addPointer(location: tester.getCenter(find.byType(Text)));
|
|
|
|
await tester.pump();
|
|
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.text,
|
|
);
|
|
});
|
|
|
|
testWidgets('Mouse hovering over selectable Text uses default selection style mouse cursor', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectionArea(
|
|
child: DefaultSelectionStyle.merge(
|
|
mouseCursor: SystemMouseCursors.click,
|
|
child: const Text('Flutter'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestGesture gesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
pointer: 1,
|
|
);
|
|
await gesture.addPointer(location: tester.getCenter(find.byType(Text)));
|
|
|
|
await tester.pump();
|
|
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.click,
|
|
);
|
|
});
|
|
|
|
testWidgets('can set heading level', (WidgetTester tester) async {
|
|
final semantics = SemanticsTester(tester);
|
|
|
|
for (var level = 1; level <= 6; level++) {
|
|
await tester.pumpWidget(
|
|
Semantics(
|
|
headingLevel: 1,
|
|
child: Text('Heading level $level', textDirection: TextDirection.ltr),
|
|
),
|
|
);
|
|
final expectedSemantics = TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics.rootChild(
|
|
label: 'Heading level $level',
|
|
headingLevel: 1,
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
],
|
|
);
|
|
expect(
|
|
semantics,
|
|
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true),
|
|
);
|
|
}
|
|
|
|
semantics.dispose();
|
|
});
|
|
}
|
|
|
|
Future<void> _pumpTextWidget({
|
|
required WidgetTester tester,
|
|
required String text,
|
|
required TextOverflow overflow,
|
|
}) {
|
|
return tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(width: 50.0, height: 50.0, child: Text(text, overflow: overflow)),
|
|
),
|
|
),
|
|
);
|
|
}
|