482 lines
15 KiB
Dart
482 lines
15 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';
|
|
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
void main() {
|
|
test('TextSpan equals', () {
|
|
const a1 = TextSpan(text: 'a');
|
|
const a2 = TextSpan(text: 'a');
|
|
const b1 = TextSpan(children: <TextSpan>[a1]);
|
|
const b2 = TextSpan(children: <TextSpan>[a2]);
|
|
const c1 = TextSpan();
|
|
const c2 = TextSpan();
|
|
|
|
expect(a1 == a2, isTrue);
|
|
expect(b1 == b2, isTrue);
|
|
expect(c1 == c2, isTrue);
|
|
|
|
expect(a1 == b2, isFalse);
|
|
expect(b1 == c2, isFalse);
|
|
expect(c1 == a2, isFalse);
|
|
|
|
expect(a1 == c2, isFalse);
|
|
expect(b1 == a2, isFalse);
|
|
expect(c1 == b2, isFalse);
|
|
|
|
void callback1(PointerEnterEvent _) {}
|
|
void callback2(PointerEnterEvent _) {}
|
|
|
|
final d1 = TextSpan(text: 'a', onEnter: callback1);
|
|
final d2 = TextSpan(text: 'a', onEnter: callback1);
|
|
final d3 = TextSpan(text: 'a', onEnter: callback2);
|
|
final e1 = TextSpan(text: 'a', onEnter: callback2, mouseCursor: SystemMouseCursors.forbidden);
|
|
final e2 = TextSpan(text: 'a', onEnter: callback2, mouseCursor: SystemMouseCursors.forbidden);
|
|
|
|
expect(a1 == d1, isFalse);
|
|
expect(d1 == d2, isTrue);
|
|
expect(d2 == d3, isFalse);
|
|
expect(d3 == e1, isFalse);
|
|
expect(e1 == e2, isTrue);
|
|
});
|
|
|
|
test('TextSpan toStringDeep', () {
|
|
const test = TextSpan(
|
|
text: 'a',
|
|
style: TextStyle(fontSize: 10.0),
|
|
children: <TextSpan>[
|
|
TextSpan(text: 'b', children: <TextSpan>[TextSpan()]),
|
|
TextSpan(text: 'c'),
|
|
],
|
|
);
|
|
expect(
|
|
test.toStringDeep(),
|
|
equals(
|
|
'TextSpan:\n'
|
|
' inherit: true\n'
|
|
' size: 10.0\n'
|
|
' "a"\n'
|
|
' TextSpan:\n'
|
|
' "b"\n'
|
|
' TextSpan:\n'
|
|
' (empty)\n'
|
|
' TextSpan:\n'
|
|
' "c"\n',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('TextSpan toStringDeep for mouse', () {
|
|
const test1 = TextSpan(text: 'a');
|
|
expect(
|
|
test1.toStringDeep(),
|
|
equals(
|
|
'TextSpan:\n'
|
|
' "a"\n',
|
|
),
|
|
);
|
|
|
|
final test2 = TextSpan(
|
|
text: 'a',
|
|
onEnter: (_) {},
|
|
onExit: (_) {},
|
|
mouseCursor: SystemMouseCursors.forbidden,
|
|
);
|
|
expect(
|
|
test2.toStringDeep(),
|
|
equals(
|
|
'TextSpan:\n'
|
|
' "a"\n'
|
|
' callbacks: enter, exit\n'
|
|
' mouseCursor: SystemMouseCursor(forbidden)\n',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('TextSpan toPlainText', () {
|
|
const textSpan = TextSpan(
|
|
text: 'a',
|
|
children: <TextSpan>[
|
|
TextSpan(text: 'b'),
|
|
TextSpan(text: 'c'),
|
|
],
|
|
);
|
|
expect(textSpan.toPlainText(), 'abc');
|
|
});
|
|
|
|
test('WidgetSpan toPlainText', () {
|
|
const textSpan = TextSpan(
|
|
text: 'a',
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'b'),
|
|
WidgetSpan(child: SizedBox(width: 10, height: 10)),
|
|
TextSpan(text: 'c'),
|
|
],
|
|
);
|
|
expect(textSpan.toPlainText(), 'ab\uFFFCc');
|
|
});
|
|
|
|
test('TextSpan toPlainText with semanticsLabel', () {
|
|
const textSpan = TextSpan(
|
|
text: 'a',
|
|
children: <TextSpan>[
|
|
TextSpan(text: 'b', semanticsLabel: 'foo'),
|
|
TextSpan(text: 'c'),
|
|
],
|
|
);
|
|
expect(textSpan.toPlainText(), 'afooc');
|
|
expect(textSpan.toPlainText(includeSemanticsLabels: false), 'abc');
|
|
});
|
|
|
|
test('TextSpan widget change test', () {
|
|
const textSpan1 = TextSpan(
|
|
text: 'a',
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'b'),
|
|
WidgetSpan(child: SizedBox(width: 10, height: 10)),
|
|
TextSpan(text: 'c'),
|
|
],
|
|
);
|
|
|
|
const textSpan2 = TextSpan(
|
|
text: 'a',
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'b'),
|
|
WidgetSpan(child: SizedBox(width: 10, height: 10)),
|
|
TextSpan(text: 'c'),
|
|
],
|
|
);
|
|
|
|
const textSpan3 = TextSpan(
|
|
text: 'a',
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'b'),
|
|
WidgetSpan(child: SizedBox(width: 11, height: 10)),
|
|
TextSpan(text: 'c'),
|
|
],
|
|
);
|
|
|
|
const textSpan4 = TextSpan(
|
|
text: 'a',
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'b'),
|
|
WidgetSpan(child: Text('test')),
|
|
TextSpan(text: 'c'),
|
|
],
|
|
);
|
|
|
|
const textSpan5 = TextSpan(
|
|
text: 'a',
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'b'),
|
|
WidgetSpan(child: Text('different!')),
|
|
TextSpan(text: 'c'),
|
|
],
|
|
);
|
|
|
|
const textSpan6 = TextSpan(
|
|
text: 'a',
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'b'),
|
|
WidgetSpan(child: SizedBox(width: 10, height: 10), alignment: PlaceholderAlignment.top),
|
|
TextSpan(text: 'c'),
|
|
],
|
|
);
|
|
|
|
expect(textSpan1.compareTo(textSpan3), RenderComparison.layout);
|
|
expect(textSpan1.compareTo(textSpan4), RenderComparison.layout);
|
|
expect(textSpan1.compareTo(textSpan1), RenderComparison.identical);
|
|
expect(textSpan2.compareTo(textSpan2), RenderComparison.identical);
|
|
expect(textSpan3.compareTo(textSpan3), RenderComparison.identical);
|
|
expect(textSpan2.compareTo(textSpan3), RenderComparison.layout);
|
|
expect(textSpan4.compareTo(textSpan5), RenderComparison.layout);
|
|
expect(textSpan3.compareTo(textSpan5), RenderComparison.layout);
|
|
expect(textSpan2.compareTo(textSpan5), RenderComparison.layout);
|
|
expect(textSpan1.compareTo(textSpan5), RenderComparison.layout);
|
|
expect(textSpan1.compareTo(textSpan6), RenderComparison.layout);
|
|
});
|
|
|
|
test('TextSpan nested widget change test', () {
|
|
const textSpan1 = TextSpan(
|
|
text: 'a',
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'b'),
|
|
WidgetSpan(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
WidgetSpan(child: SizedBox(width: 10, height: 10)),
|
|
TextSpan(text: 'The sky is falling :)'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
TextSpan(text: 'c'),
|
|
],
|
|
);
|
|
|
|
const textSpan2 = TextSpan(
|
|
text: 'a',
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'b'),
|
|
WidgetSpan(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
WidgetSpan(child: SizedBox(width: 10, height: 11)),
|
|
TextSpan(text: 'The sky is falling :)'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
TextSpan(text: 'c'),
|
|
],
|
|
);
|
|
|
|
expect(textSpan1.compareTo(textSpan2), RenderComparison.layout);
|
|
expect(textSpan1.compareTo(textSpan1), RenderComparison.identical);
|
|
expect(textSpan2.compareTo(textSpan2), RenderComparison.identical);
|
|
});
|
|
|
|
test('GetSpanForPosition', () {
|
|
const textSpan = TextSpan(
|
|
text: '',
|
|
children: <InlineSpan>[
|
|
TextSpan(
|
|
text: '',
|
|
children: <InlineSpan>[TextSpan(text: 'a')],
|
|
),
|
|
TextSpan(text: 'b'),
|
|
TextSpan(text: 'c'),
|
|
],
|
|
);
|
|
|
|
expect((textSpan.getSpanForPosition(const TextPosition(offset: 0)) as TextSpan?)?.text, 'a');
|
|
expect((textSpan.getSpanForPosition(const TextPosition(offset: 1)) as TextSpan?)?.text, 'b');
|
|
expect((textSpan.getSpanForPosition(const TextPosition(offset: 2)) as TextSpan?)?.text, 'c');
|
|
expect((textSpan.getSpanForPosition(const TextPosition(offset: 3)) as TextSpan?)?.text, isNull);
|
|
});
|
|
|
|
test('GetSpanForPosition with WidgetSpan', () {
|
|
const textSpan = TextSpan(
|
|
text: 'a',
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'b'),
|
|
WidgetSpan(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
children: <InlineSpan>[
|
|
WidgetSpan(child: SizedBox(width: 10, height: 10)),
|
|
TextSpan(text: 'The sky is falling :)'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
TextSpan(text: 'c'),
|
|
],
|
|
);
|
|
|
|
expect(textSpan.getSpanForPosition(const TextPosition(offset: 0)).runtimeType, TextSpan);
|
|
expect(textSpan.getSpanForPosition(const TextPosition(offset: 1)).runtimeType, TextSpan);
|
|
expect(textSpan.getSpanForPosition(const TextPosition(offset: 2)).runtimeType, WidgetSpan);
|
|
expect(textSpan.getSpanForPosition(const TextPosition(offset: 3)).runtimeType, TextSpan);
|
|
});
|
|
|
|
test('TextSpan computeSemanticsInformation', () {
|
|
final collector = <InlineSpanSemanticsInformation>[];
|
|
const TextSpan(
|
|
text: 'aaa',
|
|
semanticsLabel: 'bbb',
|
|
semanticsIdentifier: 'ccc',
|
|
).computeSemanticsInformation(collector);
|
|
expect(collector[0].text, 'aaa');
|
|
expect(collector[0].semanticsLabel, 'bbb');
|
|
expect(collector[0].semanticsIdentifier, 'ccc');
|
|
});
|
|
|
|
test('TextSpan visitDirectChildren', () {
|
|
List<InlineSpan> directChildrenOf(InlineSpan root) {
|
|
final visitOrder = <InlineSpan>[];
|
|
root.visitDirectChildren((InlineSpan span) {
|
|
visitOrder.add(span);
|
|
return true;
|
|
});
|
|
return visitOrder;
|
|
}
|
|
|
|
const leaf1 = TextSpan(text: 'leaf1');
|
|
const leaf2 = TextSpan(text: 'leaf2');
|
|
|
|
const branch1 = TextSpan(children: <InlineSpan>[leaf1, leaf2]);
|
|
const branch2 = TextSpan(text: 'branch2');
|
|
|
|
const root = TextSpan(children: <InlineSpan>[branch1, branch2]);
|
|
|
|
expect(directChildrenOf(root), <TextSpan>[branch1, branch2]);
|
|
expect(directChildrenOf(branch1), <TextSpan>[leaf1, leaf2]);
|
|
expect(directChildrenOf(branch2), isEmpty);
|
|
expect(directChildrenOf(leaf1), isEmpty);
|
|
expect(directChildrenOf(leaf2), isEmpty);
|
|
|
|
int? indexInTree(InlineSpan target) {
|
|
var index = 0;
|
|
bool findInSubtree(InlineSpan subtreeRoot) {
|
|
if (identical(target, subtreeRoot)) {
|
|
// return false to stop traversal.
|
|
return false;
|
|
}
|
|
index += 1;
|
|
return subtreeRoot.visitDirectChildren(findInSubtree);
|
|
}
|
|
|
|
return findInSubtree(root) ? null : index;
|
|
}
|
|
|
|
expect(indexInTree(root), 0);
|
|
expect(indexInTree(branch1), 1);
|
|
expect(indexInTree(leaf1), 2);
|
|
expect(indexInTree(leaf2), 3);
|
|
expect(indexInTree(branch2), 4);
|
|
expect(indexInTree(const TextSpan(text: 'foobar')), null);
|
|
});
|
|
|
|
testWidgets('handles mouse cursor', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
const Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
text: 'xxxxx',
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'yyyyy', mouseCursor: SystemMouseCursors.forbidden),
|
|
TextSpan(text: 'xxxxx'),
|
|
],
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
|
|
await gesture.moveTo(tester.getCenter(find.byType(RichText)) - const Offset(40, 0));
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.basic,
|
|
);
|
|
|
|
await gesture.moveTo(tester.getCenter(find.byType(RichText)));
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.forbidden,
|
|
);
|
|
|
|
await gesture.moveTo(tester.getCenter(find.byType(RichText)) + const Offset(40, 0));
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.basic,
|
|
);
|
|
});
|
|
|
|
testWidgets('handles onEnter and onExit', (WidgetTester tester) async {
|
|
final logEvents = <PointerEvent>[];
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
text: 'xxxxx',
|
|
children: <InlineSpan>[
|
|
TextSpan(
|
|
text: 'yyyyy',
|
|
onEnter: (PointerEnterEvent event) {
|
|
logEvents.add(event);
|
|
},
|
|
onExit: (PointerExitEvent event) {
|
|
logEvents.add(event);
|
|
},
|
|
),
|
|
const TextSpan(text: 'xxxxx'),
|
|
],
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
|
|
await gesture.moveTo(tester.getCenter(find.byType(RichText)) - const Offset(40, 0));
|
|
expect(logEvents, isEmpty);
|
|
|
|
await gesture.moveTo(tester.getCenter(find.byType(RichText)));
|
|
expect(logEvents.length, 1);
|
|
expect(logEvents[0], isA<PointerEnterEvent>());
|
|
|
|
await gesture.moveTo(tester.getCenter(find.byType(RichText)) + const Offset(40, 0));
|
|
expect(logEvents.length, 2);
|
|
expect(logEvents[1], isA<PointerExitEvent>());
|
|
});
|
|
|
|
testWidgets('TextSpan can compute StringAttributes', (WidgetTester tester) async {
|
|
const span = TextSpan(
|
|
text: 'aaaaa',
|
|
spellOut: true,
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'yyyyy', locale: Locale('es', 'MX')),
|
|
TextSpan(
|
|
text: 'xxxxx',
|
|
spellOut: false,
|
|
children: <InlineSpan>[
|
|
TextSpan(text: 'zzzzz'),
|
|
TextSpan(text: 'bbbbb', spellOut: true),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
final collector = <InlineSpanSemanticsInformation>[];
|
|
span.computeSemanticsInformation(collector);
|
|
expect(collector.length, 5);
|
|
expect(collector[0].stringAttributes.length, 1);
|
|
expect(collector[0].stringAttributes[0], isA<SpellOutStringAttribute>());
|
|
expect(collector[0].stringAttributes[0].range, const TextRange(start: 0, end: 5));
|
|
expect(collector[1].stringAttributes.length, 2);
|
|
expect(collector[1].stringAttributes[0], isA<SpellOutStringAttribute>());
|
|
expect(collector[1].stringAttributes[0].range, const TextRange(start: 0, end: 5));
|
|
expect(collector[1].stringAttributes[1], isA<LocaleStringAttribute>());
|
|
expect(collector[1].stringAttributes[1].range, const TextRange(start: 0, end: 5));
|
|
final localeStringAttribute = collector[1].stringAttributes[1] as LocaleStringAttribute;
|
|
expect(localeStringAttribute.locale, const Locale('es', 'MX'));
|
|
expect(collector[2].stringAttributes.length, 0);
|
|
expect(collector[3].stringAttributes.length, 0);
|
|
expect(collector[4].stringAttributes.length, 1);
|
|
expect(collector[4].stringAttributes[0], isA<SpellOutStringAttribute>());
|
|
expect(collector[4].stringAttributes[0].range, const TextRange(start: 0, end: 5));
|
|
|
|
final List<InlineSpanSemanticsInformation> combined = combineSemanticsInfo(collector);
|
|
expect(combined.length, 1);
|
|
expect(combined[0].stringAttributes.length, 4);
|
|
expect(combined[0].stringAttributes[0], isA<SpellOutStringAttribute>());
|
|
expect(combined[0].stringAttributes[0].range, const TextRange(start: 0, end: 5));
|
|
expect(combined[0].stringAttributes[1], isA<SpellOutStringAttribute>());
|
|
expect(combined[0].stringAttributes[1].range, const TextRange(start: 5, end: 10));
|
|
expect(combined[0].stringAttributes[2], isA<LocaleStringAttribute>());
|
|
expect(combined[0].stringAttributes[2].range, const TextRange(start: 5, end: 10));
|
|
final combinedLocaleStringAttribute = combined[0].stringAttributes[2] as LocaleStringAttribute;
|
|
expect(combinedLocaleStringAttribute.locale, const Locale('es', 'MX'));
|
|
expect(combined[0].stringAttributes[3], isA<SpellOutStringAttribute>());
|
|
expect(combined[0].stringAttributes[3].range, const TextRange(start: 20, end: 25));
|
|
});
|
|
}
|