1230 lines
42 KiB
Dart
1230 lines
42 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.
|
|
|
|
// This file is run as part of a reduced test set in CI on Mac and Windows
|
|
// machines.
|
|
@Tags(<String>['reduced-test-set'])
|
|
library;
|
|
|
|
import 'dart:math' as math;
|
|
import 'dart:ui' as ui;
|
|
import 'dart:ui';
|
|
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'semantics_tester.dart';
|
|
|
|
void main() {
|
|
group('RawImage', () {
|
|
testWidgets('properties', (WidgetTester tester) async {
|
|
final ui.Image image1 = (await tester.runAsync<ui.Image>(() => createTestImage()))!;
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: RawImage(image: image1),
|
|
),
|
|
);
|
|
final RenderImage renderObject = tester.firstRenderObject<RenderImage>(find.byType(RawImage));
|
|
|
|
// Expect default values
|
|
expect(renderObject.image!.isCloneOf(image1), true);
|
|
expect(renderObject.debugImageLabel, null);
|
|
expect(renderObject.width, null);
|
|
expect(renderObject.height, null);
|
|
expect(renderObject.scale, 1.0);
|
|
expect(renderObject.color, null);
|
|
expect(renderObject.opacity, null);
|
|
expect(renderObject.colorBlendMode, null);
|
|
expect(renderObject.fit, null);
|
|
expect(renderObject.alignment, Alignment.center);
|
|
expect(renderObject.repeat, ImageRepeat.noRepeat);
|
|
expect(renderObject.centerSlice, null);
|
|
expect(renderObject.matchTextDirection, false);
|
|
expect(renderObject.invertColors, false);
|
|
expect(renderObject.filterQuality, FilterQuality.low);
|
|
expect(renderObject.isAntiAlias, false);
|
|
|
|
final ui.Image image2 = (await tester.runAsync<ui.Image>(() => createTestImage(width: 2, height: 2)))!;
|
|
const String debugImageLabel = 'debugImageLabel';
|
|
const double width = 1;
|
|
const double height = 1;
|
|
const double scale = 2.0;
|
|
const Color color = Colors.black;
|
|
const Animation<double> opacity = AlwaysStoppedAnimation<double>(0.0);
|
|
const BlendMode colorBlendMode = BlendMode.difference;
|
|
const BoxFit fit = BoxFit.contain;
|
|
const AlignmentGeometry alignment = Alignment.topCenter;
|
|
const ImageRepeat repeat = ImageRepeat.repeat;
|
|
const Rect centerSlice = Rect.fromLTWH(0, 0, width, height);
|
|
const bool matchTextDirection = true;
|
|
const bool invertColors = true;
|
|
const FilterQuality filterQuality = FilterQuality.high;
|
|
const bool isAntiAlias = true;
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: RawImage(
|
|
image: image2,
|
|
debugImageLabel: debugImageLabel,
|
|
width: width,
|
|
height: height,
|
|
scale: scale,
|
|
color: color,
|
|
opacity: opacity,
|
|
colorBlendMode: colorBlendMode,
|
|
fit: fit,
|
|
alignment: alignment,
|
|
repeat: repeat,
|
|
centerSlice: centerSlice,
|
|
matchTextDirection: matchTextDirection,
|
|
invertColors: invertColors,
|
|
filterQuality: filterQuality,
|
|
isAntiAlias: isAntiAlias,
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(renderObject.image!.isCloneOf(image2), true);
|
|
expect(renderObject.debugImageLabel, debugImageLabel);
|
|
expect(renderObject.width, width);
|
|
expect(renderObject.height, height);
|
|
expect(renderObject.scale, scale);
|
|
expect(renderObject.color, color);
|
|
expect(renderObject.opacity, opacity);
|
|
expect(renderObject.colorBlendMode, colorBlendMode);
|
|
expect(renderObject.fit, fit);
|
|
expect(renderObject.alignment, alignment);
|
|
expect(renderObject.repeat, repeat);
|
|
expect(renderObject.centerSlice, centerSlice);
|
|
expect(renderObject.matchTextDirection, matchTextDirection);
|
|
expect(renderObject.invertColors, invertColors);
|
|
expect(renderObject.filterQuality, filterQuality);
|
|
expect(renderObject.isAntiAlias, isAntiAlias);
|
|
});
|
|
});
|
|
|
|
group('PhysicalShape', () {
|
|
testWidgets('properties', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
const PhysicalShape(
|
|
clipper: ShapeBorderClipper(shape: CircleBorder()),
|
|
elevation: 2.0,
|
|
color: Color(0xFF0000FF),
|
|
shadowColor: Color(0xFF00FF00),
|
|
),
|
|
);
|
|
final RenderPhysicalShape renderObject = tester.renderObject(find.byType(PhysicalShape));
|
|
expect(renderObject.clipper, const ShapeBorderClipper(shape: CircleBorder()));
|
|
expect(renderObject.color, const Color(0xFF0000FF));
|
|
expect(renderObject.shadowColor, const Color(0xFF00FF00));
|
|
expect(renderObject.elevation, 2.0);
|
|
});
|
|
|
|
testWidgets('hit test', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
PhysicalShape(
|
|
clipper: const ShapeBorderClipper(shape: CircleBorder()),
|
|
elevation: 2.0,
|
|
color: const Color(0xFF0000FF),
|
|
shadowColor: const Color(0xFF00FF00),
|
|
child: Container(color: const Color(0xFF0000FF)),
|
|
),
|
|
);
|
|
|
|
final RenderPhysicalShape renderPhysicalShape =
|
|
tester.renderObject(find.byType(PhysicalShape));
|
|
|
|
// The viewport is 800x600, the CircleBorder is centered and fits
|
|
// the shortest edge, so we get a circle of radius 300, centered at
|
|
// (400, 300).
|
|
//
|
|
// We test by sampling a few points around the left-most point of the
|
|
// circle (100, 300).
|
|
|
|
expect(tester.hitTestOnBinding(const Offset(99.0, 300.0)), doesNotHit(renderPhysicalShape));
|
|
expect(tester.hitTestOnBinding(const Offset(100.0, 300.0)), hits(renderPhysicalShape));
|
|
expect(tester.hitTestOnBinding(const Offset(100.0, 299.0)), doesNotHit(renderPhysicalShape));
|
|
expect(tester.hitTestOnBinding(const Offset(100.0, 301.0)), doesNotHit(renderPhysicalShape));
|
|
});
|
|
|
|
});
|
|
|
|
group('FractionalTranslation', () {
|
|
testWidgets('hit test - entirely inside the bounding box', (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey();
|
|
bool pointerDown = false;
|
|
|
|
await tester.pumpWidget(
|
|
Center(
|
|
child: FractionalTranslation(
|
|
translation: Offset.zero,
|
|
child: Listener(
|
|
onPointerDown: (PointerDownEvent event) {
|
|
pointerDown = true;
|
|
},
|
|
child: SizedBox(
|
|
key: key1,
|
|
width: 100.0,
|
|
height: 100.0,
|
|
child: Container(
|
|
color: const Color(0xFF0000FF),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(pointerDown, isFalse);
|
|
await tester.tap(find.byKey(key1));
|
|
expect(pointerDown, isTrue);
|
|
});
|
|
|
|
testWidgets('hit test - partially inside the bounding box', (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey();
|
|
bool pointerDown = false;
|
|
|
|
await tester.pumpWidget(
|
|
Center(
|
|
child: FractionalTranslation(
|
|
translation: const Offset(0.5, 0.5),
|
|
child: Listener(
|
|
onPointerDown: (PointerDownEvent event) {
|
|
pointerDown = true;
|
|
},
|
|
child: SizedBox(
|
|
key: key1,
|
|
width: 100.0,
|
|
height: 100.0,
|
|
child: Container(
|
|
color: const Color(0xFF0000FF),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(pointerDown, isFalse);
|
|
await tester.tap(find.byKey(key1));
|
|
expect(pointerDown, isTrue);
|
|
});
|
|
|
|
testWidgets('hit test - completely outside the bounding box', (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey();
|
|
bool pointerDown = false;
|
|
|
|
await tester.pumpWidget(
|
|
Center(
|
|
child: FractionalTranslation(
|
|
translation: const Offset(1.0, 1.0),
|
|
child: Listener(
|
|
onPointerDown: (PointerDownEvent event) {
|
|
pointerDown = true;
|
|
},
|
|
child: SizedBox(
|
|
key: key1,
|
|
width: 100.0,
|
|
height: 100.0,
|
|
child: Container(
|
|
color: const Color(0xFF0000FF),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(pointerDown, isFalse);
|
|
await tester.tap(find.byKey(key1));
|
|
expect(pointerDown, isTrue);
|
|
});
|
|
|
|
testWidgets('semantics bounds are updated', (WidgetTester tester) async {
|
|
final GlobalKey fractionalTranslationKey = GlobalKey();
|
|
final GlobalKey textKey = GlobalKey();
|
|
Offset offset = const Offset(0.4, 0.4);
|
|
|
|
await tester.pumpWidget(
|
|
StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: Semantics(
|
|
explicitChildNodes: true,
|
|
child: FractionalTranslation(
|
|
key: fractionalTranslationKey,
|
|
translation: offset,
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
setState(() {
|
|
offset = const Offset(0.8, 0.8);
|
|
});
|
|
},
|
|
child: SizedBox(
|
|
width: 100.0,
|
|
height: 100.0,
|
|
child: Text(
|
|
'foo',
|
|
key: textKey,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(
|
|
tester.getSemantics(find.byKey(textKey)).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,
|
|
1170.0, 870.0, 0.0, 1.0,
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byKey(fractionalTranslationKey), warnIfMissed: false); // RenderFractionalTranslation can't be hit
|
|
await tester.pump();
|
|
expect(
|
|
tester.getSemantics(find.byKey(textKey)).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,
|
|
1290.0, 990.0, 0.0, 1.0,
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
group('Semantics', () {
|
|
testWidgets('Semantics can set attributed Text', (WidgetTester tester) async {
|
|
final UniqueKey key = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Semantics(
|
|
key: key,
|
|
attributedLabel: AttributedString(
|
|
'label',
|
|
attributes: <StringAttribute>[
|
|
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
|
|
],
|
|
),
|
|
attributedValue: AttributedString(
|
|
'value',
|
|
attributes: <StringAttribute>[
|
|
LocaleStringAttribute(range: const TextRange(start: 0, end: 5), locale: const Locale('en', 'MX')),
|
|
],
|
|
),
|
|
attributedHint: AttributedString(
|
|
'hint',
|
|
attributes: <StringAttribute>[
|
|
SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)),
|
|
],
|
|
),
|
|
child: const Placeholder(),
|
|
)
|
|
),
|
|
)
|
|
);
|
|
final AttributedString attributedLabel = tester.getSemantics(find.byKey(key)).attributedLabel;
|
|
expect(attributedLabel.string, 'label');
|
|
expect(attributedLabel.attributes.length, 1);
|
|
expect(attributedLabel.attributes[0] is SpellOutStringAttribute, isTrue);
|
|
expect(attributedLabel.attributes[0].range, const TextRange(start:0, end: 5));
|
|
|
|
final AttributedString attributedValue = tester.getSemantics(find.byKey(key)).attributedValue;
|
|
expect(attributedValue.string, 'value');
|
|
expect(attributedValue.attributes.length, 1);
|
|
expect(attributedValue.attributes[0] is LocaleStringAttribute, isTrue);
|
|
final LocaleStringAttribute valueLocale = attributedValue.attributes[0] as LocaleStringAttribute;
|
|
expect(valueLocale.range, const TextRange(start:0, end: 5));
|
|
expect(valueLocale.locale, const Locale('en', 'MX'));
|
|
|
|
final AttributedString attributedHint = tester.getSemantics(find.byKey(key)).attributedHint;
|
|
expect(attributedHint.string, 'hint');
|
|
expect(attributedHint.attributes.length, 1);
|
|
expect(attributedHint.attributes[0] is SpellOutStringAttribute, isTrue);
|
|
expect(attributedHint.attributes[0].range, const TextRange(start:1, end: 2));
|
|
});
|
|
|
|
testWidgets('Semantics can merge attributed strings', (WidgetTester tester) async {
|
|
final UniqueKey key = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Semantics(
|
|
key: key,
|
|
attributedLabel: AttributedString(
|
|
'label',
|
|
attributes: <StringAttribute>[
|
|
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
|
|
],
|
|
),
|
|
attributedHint: AttributedString(
|
|
'hint',
|
|
attributes: <StringAttribute>[
|
|
SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)),
|
|
],
|
|
),
|
|
child: Semantics(
|
|
attributedLabel: AttributedString(
|
|
'label',
|
|
attributes: <StringAttribute>[
|
|
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
|
|
],
|
|
),
|
|
attributedHint: AttributedString(
|
|
'hint',
|
|
attributes: <StringAttribute>[
|
|
SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)),
|
|
],
|
|
),
|
|
child: const Placeholder(),
|
|
)
|
|
)
|
|
),
|
|
)
|
|
);
|
|
final AttributedString attributedLabel = tester.getSemantics(find.byKey(key)).attributedLabel;
|
|
expect(attributedLabel.string, 'label\nlabel');
|
|
expect(attributedLabel.attributes.length, 2);
|
|
expect(attributedLabel.attributes[0] is SpellOutStringAttribute, isTrue);
|
|
expect(attributedLabel.attributes[0].range, const TextRange(start:0, end: 5));
|
|
expect(attributedLabel.attributes[1] is SpellOutStringAttribute, isTrue);
|
|
expect(attributedLabel.attributes[1].range, const TextRange(start:6, end: 11));
|
|
|
|
final AttributedString attributedHint = tester.getSemantics(find.byKey(key)).attributedHint;
|
|
expect(attributedHint.string, 'hint\nhint');
|
|
expect(attributedHint.attributes.length, 2);
|
|
expect(attributedHint.attributes[0] is SpellOutStringAttribute, isTrue);
|
|
expect(attributedHint.attributes[0].range, const TextRange(start:1, end: 2));
|
|
expect(attributedHint.attributes[1] is SpellOutStringAttribute, isTrue);
|
|
expect(attributedHint.attributes[1].range, const TextRange(start:6, end: 7));
|
|
});
|
|
|
|
testWidgets('Semantics can merge attributed strings with non attributed string', (WidgetTester tester) async {
|
|
final UniqueKey key = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Semantics(
|
|
key: key,
|
|
attributedLabel: AttributedString(
|
|
'label1',
|
|
attributes: <StringAttribute>[
|
|
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
|
|
],
|
|
),
|
|
child: Semantics(
|
|
label: 'label2',
|
|
child: Semantics(
|
|
attributedLabel: AttributedString(
|
|
'label3',
|
|
attributes: <StringAttribute>[
|
|
SpellOutStringAttribute(range: const TextRange(start: 1, end: 3)),
|
|
],
|
|
),
|
|
child: const Placeholder(),
|
|
),
|
|
)
|
|
)
|
|
),
|
|
)
|
|
);
|
|
final AttributedString attributedLabel = tester.getSemantics(find.byKey(key)).attributedLabel;
|
|
expect(attributedLabel.string, 'label1\nlabel2\nlabel3');
|
|
expect(attributedLabel.attributes.length, 2);
|
|
expect(attributedLabel.attributes[0] is SpellOutStringAttribute, isTrue);
|
|
expect(attributedLabel.attributes[0].range, const TextRange(start:0, end: 5));
|
|
expect(attributedLabel.attributes[1] is SpellOutStringAttribute, isTrue);
|
|
expect(attributedLabel.attributes[1].range, const TextRange(start:15, end: 17));
|
|
});
|
|
});
|
|
|
|
group('Row', () {
|
|
testWidgets('multiple baseline aligned children', (WidgetTester tester) async {
|
|
final UniqueKey key1 = UniqueKey();
|
|
final UniqueKey key2 = UniqueKey();
|
|
// The point size of the font must be a multiple of 4 until
|
|
// https://github.com/flutter/flutter/issues/122066 is resolved.
|
|
const double fontSize1 = 52;
|
|
const double fontSize2 = 12;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
|
textBaseline: TextBaseline.alphabetic,
|
|
children: <Widget>[
|
|
Text('big text',
|
|
key: key1,
|
|
style: const TextStyle(fontFamily: 'FlutterTest', fontSize: fontSize1),
|
|
),
|
|
Text('one\ntwo\nthree\nfour\nfive\nsix\nseven',
|
|
key: key2,
|
|
style: const TextStyle(fontFamily: 'FlutterTest', fontSize: fontSize2),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderBox textBox1 = tester.renderObject(find.byKey(key1));
|
|
final RenderBox textBox2 = tester.renderObject(find.byKey(key2));
|
|
final RenderBox rowBox = tester.renderObject(find.byType(Row));
|
|
|
|
// The two Texts are baseline aligned, so some portion of them extends
|
|
// both above and below the baseline. The first has a huge font size, so
|
|
// it extends higher above the baseline than usual. The second has many
|
|
// lines, but being aligned by the first line's baseline, they hang far
|
|
// below the baseline. The size of the parent row is just enough to
|
|
// contain both of them.
|
|
const double ascentRatio = 0.75;
|
|
const double aboveBaseline1 = fontSize1 * ascentRatio;
|
|
const double belowBaseline1 = fontSize1 * (1 - ascentRatio);
|
|
const double aboveBaseline2 = fontSize2 * ascentRatio;
|
|
const double belowBaseline2 = fontSize2 * (1 - ascentRatio) + fontSize2 * 6;
|
|
final double aboveBaseline = math.max(aboveBaseline1, aboveBaseline2);
|
|
final double belowBaseline = math.max(belowBaseline1, belowBaseline2);
|
|
expect(rowBox.size.height, greaterThan(textBox1.size.height));
|
|
expect(rowBox.size.height, greaterThan(textBox2.size.height));
|
|
expect(rowBox.size.height, aboveBaseline + belowBaseline, );
|
|
expect(tester.getTopLeft(find.byKey(key1)).dy, 0);
|
|
expect(tester.getTopLeft(find.byKey(key2)).dy, aboveBaseline1 - aboveBaseline2);
|
|
});
|
|
|
|
testWidgets('baseline aligned children account for a larger, no-baseline child size', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/58898
|
|
final UniqueKey key1 = UniqueKey();
|
|
final UniqueKey key2 = UniqueKey();
|
|
// The point size of the font must be a multiple of 4 until
|
|
// https://github.com/flutter/flutter/issues/122066 is resolved.
|
|
const double fontSize1 = 52;
|
|
const double fontSize2 = 12;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
|
textBaseline: TextBaseline.alphabetic,
|
|
children: <Widget>[
|
|
Text('big text',
|
|
key: key1,
|
|
style: const TextStyle(fontFamily: 'FlutterTest', fontSize: fontSize1),
|
|
),
|
|
Text('one\ntwo\nthree\nfour\nfive\nsix\nseven',
|
|
key: key2,
|
|
style: const TextStyle(fontFamily: 'FlutterTest', fontSize: fontSize2),
|
|
),
|
|
const FlutterLogo(size: 250),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderBox textBox1 = tester.renderObject(find.byKey(key1));
|
|
final RenderBox textBox2 = tester.renderObject(find.byKey(key2));
|
|
final RenderBox rowBox = tester.renderObject(find.byType(Row));
|
|
|
|
// The two Texts are baseline aligned, so some portion of them extends
|
|
// both above and below the baseline. The first has a huge font size, so
|
|
// it extends higher above the baseline than usual. The second has many
|
|
// lines, but being aligned by the first line's baseline, they hang far
|
|
// below the baseline. The FlutterLogo extends further than both Texts,
|
|
// so the size of the parent row should contain the FlutterLogo as well.
|
|
const double ascentRatio = 0.75;
|
|
const double aboveBaseline1 = fontSize1 * ascentRatio;
|
|
const double aboveBaseline2 = fontSize2 * ascentRatio;
|
|
expect(rowBox.size.height, greaterThan(textBox1.size.height));
|
|
expect(rowBox.size.height, greaterThan(textBox2.size.height));
|
|
expect(rowBox.size.height, 250);
|
|
expect(tester.getTopLeft(find.byKey(key1)).dy, 0);
|
|
expect(
|
|
tester.getTopLeft(find.byKey(key2)).dy,
|
|
aboveBaseline1 - aboveBaseline2,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('UnconstrainedBox toString', () {
|
|
expect(
|
|
const UnconstrainedBox(constrainedAxis: Axis.vertical).toString(),
|
|
equals('UnconstrainedBox(alignment: Alignment.center, constrainedAxis: vertical)'),
|
|
);
|
|
|
|
expect(
|
|
const UnconstrainedBox(constrainedAxis: Axis.horizontal, textDirection: TextDirection.rtl, alignment: Alignment.topRight).toString(),
|
|
equals('UnconstrainedBox(alignment: Alignment.topRight, constrainedAxis: horizontal, textDirection: rtl)'),
|
|
);
|
|
});
|
|
|
|
testWidgets('UnconstrainedBox can set and update clipBehavior', (WidgetTester tester) async {
|
|
await tester.pumpWidget(const UnconstrainedBox());
|
|
final RenderConstraintsTransformBox renderObject = tester.allRenderObjects.whereType<RenderConstraintsTransformBox>().first;
|
|
expect(renderObject.clipBehavior, equals(Clip.none));
|
|
|
|
await tester.pumpWidget(const UnconstrainedBox(clipBehavior: Clip.antiAlias));
|
|
expect(renderObject.clipBehavior, equals(Clip.antiAlias));
|
|
});
|
|
|
|
testWidgets('UnconstrainedBox warns only when clipBehavior is Clip.none', (WidgetTester tester) async {
|
|
for (final Clip? clip in <Clip?>[null, ...Clip.values]) {
|
|
// Clear any render objects that were there before so that we can see more
|
|
// than one error. Otherwise, it just throws the first one and skips the
|
|
// rest, since the render objects haven't changed.
|
|
await tester.pumpWidget(const SizedBox());
|
|
await tester.pumpWidget(
|
|
Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxHeight: 200, maxWidth: 200),
|
|
child: clip == null
|
|
? const UnconstrainedBox(child: SizedBox(width: 400, height: 400))
|
|
: UnconstrainedBox(
|
|
clipBehavior: clip,
|
|
child: const SizedBox(width: 400, height: 400),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderConstraintsTransformBox renderObject = tester.allRenderObjects.whereType<RenderConstraintsTransformBox>().first;
|
|
|
|
// Defaults to Clip.none
|
|
expect(renderObject.clipBehavior, equals(clip ?? Clip.none), reason: 'for clip = $clip');
|
|
|
|
switch (clip) {
|
|
case null:
|
|
case Clip.none:
|
|
// the UnconstrainedBox overflows.
|
|
final dynamic exception = tester.takeException();
|
|
expect(exception, isFlutterError, reason: 'for clip = $clip');
|
|
// ignore: avoid_dynamic_calls
|
|
expect(exception.diagnostics.first.level, DiagnosticLevel.summary, reason: 'for clip = $clip');
|
|
expect(
|
|
// ignore: avoid_dynamic_calls
|
|
exception.diagnostics.first.toString(),
|
|
startsWith('A RenderConstraintsTransformBox overflowed'),
|
|
reason: 'for clip = $clip',
|
|
);
|
|
case Clip.hardEdge:
|
|
case Clip.antiAlias:
|
|
case Clip.antiAliasWithSaveLayer:
|
|
expect(tester.takeException(), isNull, reason: 'for clip = $clip');
|
|
}
|
|
}
|
|
});
|
|
|
|
group('ConstraintsTransformBox', () {
|
|
test('toString', () {
|
|
expect(
|
|
const ConstraintsTransformBox(
|
|
constraintsTransform: ConstraintsTransformBox.unconstrained,
|
|
).toString(),
|
|
equals('ConstraintsTransformBox(alignment: Alignment.center, constraints transform: unconstrained)'),
|
|
);
|
|
expect(
|
|
const ConstraintsTransformBox(
|
|
textDirection: TextDirection.rtl,
|
|
alignment: Alignment.topRight,
|
|
constraintsTransform: ConstraintsTransformBox.widthUnconstrained,
|
|
).toString(),
|
|
equals('ConstraintsTransformBox(alignment: Alignment.topRight, textDirection: rtl, constraints transform: width constraints removed)'),
|
|
);
|
|
});
|
|
});
|
|
|
|
group('ColoredBox', () {
|
|
late _MockCanvas mockCanvas;
|
|
late _MockPaintingContext mockContext;
|
|
const Color colorToPaint = Color(0xFFABCDEF);
|
|
|
|
setUp(() {
|
|
mockContext = _MockPaintingContext();
|
|
mockCanvas = mockContext.canvas;
|
|
});
|
|
|
|
testWidgets('ColoredBox - no size, no child', (WidgetTester tester) async {
|
|
await tester.pumpWidget(const Flex(
|
|
direction: Axis.horizontal,
|
|
textDirection: TextDirection.ltr,
|
|
children: <Widget>[
|
|
SizedBox.shrink(
|
|
child: ColoredBox(color: colorToPaint),
|
|
),
|
|
],
|
|
));
|
|
expect(find.byType(ColoredBox), findsOneWidget);
|
|
final RenderObject renderColoredBox = tester.renderObject(find.byType(ColoredBox));
|
|
|
|
renderColoredBox.paint(mockContext, Offset.zero);
|
|
|
|
expect(mockCanvas.rects, isEmpty);
|
|
expect(mockCanvas.paints, isEmpty);
|
|
expect(mockContext.children, isEmpty);
|
|
expect(mockContext.offsets, isEmpty);
|
|
});
|
|
|
|
testWidgets('ColoredBox - no size, child', (WidgetTester tester) async {
|
|
const ValueKey<int> key = ValueKey<int>(0);
|
|
const Widget child = SizedBox.expand(key: key);
|
|
await tester.pumpWidget(const Flex(
|
|
direction: Axis.horizontal,
|
|
textDirection: TextDirection.ltr,
|
|
children: <Widget>[
|
|
SizedBox.shrink(
|
|
child: ColoredBox(color: colorToPaint, child: child),
|
|
),
|
|
],
|
|
));
|
|
expect(find.byType(ColoredBox), findsOneWidget);
|
|
final RenderObject renderColoredBox = tester.renderObject(find.byType(ColoredBox));
|
|
final RenderObject renderSizedBox = tester.renderObject(find.byKey(key));
|
|
|
|
renderColoredBox.paint(mockContext, Offset.zero);
|
|
|
|
expect(mockCanvas.rects, isEmpty);
|
|
expect(mockCanvas.paints, isEmpty);
|
|
expect(mockContext.children.single, renderSizedBox);
|
|
expect(mockContext.offsets.single, Offset.zero);
|
|
});
|
|
|
|
testWidgets('ColoredBox - size, no child', (WidgetTester tester) async {
|
|
await tester.pumpWidget(const ColoredBox(color: colorToPaint));
|
|
expect(find.byType(ColoredBox), findsOneWidget);
|
|
final RenderObject renderColoredBox = tester.renderObject(find.byType(ColoredBox));
|
|
|
|
renderColoredBox.paint(mockContext, Offset.zero);
|
|
|
|
expect(mockCanvas.rects.single, const Rect.fromLTWH(0, 0, 800, 600));
|
|
expect(mockCanvas.paints.single.color, colorToPaint);
|
|
expect(mockContext.children, isEmpty);
|
|
expect(mockContext.offsets, isEmpty);
|
|
});
|
|
|
|
testWidgets('ColoredBox - size, child', (WidgetTester tester) async {
|
|
const ValueKey<int> key = ValueKey<int>(0);
|
|
const Widget child = SizedBox.expand(key: key);
|
|
await tester.pumpWidget(const ColoredBox(color: colorToPaint, child: child));
|
|
expect(find.byType(ColoredBox), findsOneWidget);
|
|
final RenderObject renderColoredBox = tester.renderObject(find.byType(ColoredBox));
|
|
final RenderObject renderSizedBox = tester.renderObject(find.byKey(key));
|
|
|
|
renderColoredBox.paint(mockContext, Offset.zero);
|
|
|
|
expect(mockCanvas.rects.single, const Rect.fromLTWH(0, 0, 800, 600));
|
|
expect(mockCanvas.paints.single.color, colorToPaint);
|
|
expect(mockContext.children.single, renderSizedBox);
|
|
expect(mockContext.offsets.single, Offset.zero);
|
|
});
|
|
|
|
testWidgets('ColoredBox - debugFillProperties', (WidgetTester tester) async {
|
|
const ColoredBox box = ColoredBox(color: colorToPaint);
|
|
final DiagnosticPropertiesBuilder properties = DiagnosticPropertiesBuilder();
|
|
box.debugFillProperties(properties);
|
|
|
|
expect(properties.properties.first.value, colorToPaint);
|
|
});
|
|
});
|
|
testWidgets('Inconsequential golden test', (WidgetTester tester) async {
|
|
// The test validates the Flutter Gold integration. Any changes to the
|
|
// golden file can be approved at any time.
|
|
await tester.pumpWidget(RepaintBoundary(
|
|
child: Container(
|
|
color: const Color(0xABCDABCD),
|
|
),
|
|
));
|
|
|
|
await tester.pumpAndSettle();
|
|
await expectLater(
|
|
find.byType(RepaintBoundary),
|
|
matchesGoldenFile('inconsequential_golden_file.png'),
|
|
);
|
|
});
|
|
|
|
testWidgets('IgnorePointer ignores pointers', (WidgetTester tester) async {
|
|
final List<String> logs = <String>[];
|
|
Widget target({required bool ignoring}) => Align(
|
|
alignment: Alignment.topLeft,
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: SizedBox(
|
|
width: 100,
|
|
height: 100,
|
|
child: Listener(
|
|
onPointerDown: (_) { logs.add('down1'); },
|
|
child: MouseRegion(
|
|
onEnter: (_) { logs.add('enter1'); },
|
|
onExit: (_) { logs.add('exit1'); },
|
|
cursor: SystemMouseCursors.forbidden,
|
|
child: Stack(
|
|
children: <Widget>[
|
|
Listener(
|
|
onPointerDown: (_) { logs.add('down2'); },
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
onEnter: (_) { logs.add('enter2'); },
|
|
onExit: (_) { logs.add('exit2'); },
|
|
),
|
|
),
|
|
IgnorePointer(
|
|
ignoring: ignoring,
|
|
child: Listener(
|
|
onPointerDown: (_) { logs.add('down3'); },
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.text,
|
|
onEnter: (_) { logs.add('enter3'); },
|
|
onExit: (_) { logs.add('exit3'); },
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestGesture gesture = await tester.createGesture(pointer: 1, kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer(location: const Offset(200, 200));
|
|
|
|
await tester.pumpWidget(target(ignoring: true));
|
|
expect(logs, isEmpty);
|
|
|
|
await gesture.moveTo(const Offset(50, 50));
|
|
expect(logs, <String>['enter1', 'enter2']);
|
|
logs.clear();
|
|
|
|
await gesture.down(const Offset(50, 50));
|
|
expect(logs, <String>['down2', 'down1']);
|
|
logs.clear();
|
|
|
|
await gesture.up();
|
|
expect(logs, isEmpty);
|
|
|
|
await tester.pumpWidget(target(ignoring: false));
|
|
expect(logs, <String>['exit2', 'enter3']);
|
|
logs.clear();
|
|
|
|
await gesture.down(const Offset(50, 50));
|
|
expect(logs, <String>['down3', 'down1']);
|
|
logs.clear();
|
|
|
|
await gesture.up();
|
|
expect(logs, isEmpty);
|
|
|
|
await tester.pumpWidget(target(ignoring: true));
|
|
expect(logs, <String>['exit3', 'enter2']);
|
|
logs.clear();
|
|
});
|
|
|
|
group('IgnorePointer semantics', () {
|
|
testWidgets('does not change semantics when not ignoring', (WidgetTester tester) async {
|
|
final UniqueKey key = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: IgnorePointer(
|
|
ignoring: false,
|
|
child: ElevatedButton(
|
|
key: key,
|
|
onPressed: () { },
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key)),
|
|
matchesSemantics(
|
|
label: 'button',
|
|
hasTapAction: true,
|
|
isButton: true,
|
|
isFocusable: true,
|
|
hasEnabledState: true,
|
|
isEnabled: true,
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('can toggle the ignoring.', (WidgetTester tester) async {
|
|
final UniqueKey key1 = UniqueKey();
|
|
final UniqueKey key2 = UniqueKey();
|
|
final UniqueKey key3 = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: TestIgnorePointer(
|
|
child: Semantics(
|
|
key: key1,
|
|
label: '1',
|
|
onTap: (){ },
|
|
container: true,
|
|
child: Semantics(
|
|
key: key2,
|
|
label: '2',
|
|
onTap: (){ },
|
|
container: true,
|
|
child: Semantics(
|
|
key: key3,
|
|
label: '3',
|
|
onTap: (){ },
|
|
container: true,
|
|
child: const SizedBox(width: 10, height: 10),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key1)),
|
|
matchesSemantics(
|
|
label: '1',
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key2)),
|
|
matchesSemantics(
|
|
label: '2',
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key3)),
|
|
matchesSemantics(
|
|
label: '3',
|
|
),
|
|
);
|
|
|
|
final TestIgnorePointerState state = tester.state<TestIgnorePointerState>(find.byType(TestIgnorePointer));
|
|
state.setIgnore(false);
|
|
await tester.pump();
|
|
expect(
|
|
tester.getSemantics(find.byKey(key1)),
|
|
matchesSemantics(
|
|
label: '1',
|
|
hasTapAction: true,
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key2)),
|
|
matchesSemantics(
|
|
label: '2',
|
|
hasTapAction: true,
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key3)),
|
|
matchesSemantics(
|
|
label: '3',
|
|
hasTapAction: true,
|
|
),
|
|
);
|
|
|
|
state.setIgnore(true);
|
|
await tester.pump();
|
|
expect(
|
|
tester.getSemantics(find.byKey(key1)),
|
|
matchesSemantics(
|
|
label: '1',
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key2)),
|
|
matchesSemantics(
|
|
label: '2',
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key3)),
|
|
matchesSemantics(
|
|
label: '3',
|
|
),
|
|
);
|
|
|
|
state.setIgnore(false);
|
|
await tester.pump();
|
|
expect(
|
|
tester.getSemantics(find.byKey(key1)),
|
|
matchesSemantics(
|
|
label: '1',
|
|
hasTapAction: true,
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key2)),
|
|
matchesSemantics(
|
|
label: '2',
|
|
hasTapAction: true,
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key3)),
|
|
matchesSemantics(
|
|
label: '3',
|
|
hasTapAction: true,
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('drops semantics when its ignoringSemantics is true', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = SemanticsTester(tester);
|
|
final UniqueKey key = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: IgnorePointer(
|
|
ignoringSemantics: true,
|
|
child: ElevatedButton(
|
|
key: key,
|
|
onPressed: () { },
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(semantics, isNot(includesNodeWith(label: 'button')));
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('ignores user interactions', (WidgetTester tester) async {
|
|
final UniqueKey key = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: IgnorePointer(
|
|
child: ElevatedButton(
|
|
key: key,
|
|
onPressed: () { },
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
tester.getSemantics(find.byKey(key)),
|
|
// Tap action is blocked.
|
|
matchesSemantics(
|
|
label: 'button',
|
|
isButton: true,
|
|
isFocusable: true,
|
|
hasEnabledState: true,
|
|
isEnabled: true,
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
testWidgets('AbsorbPointer absorbs pointers', (WidgetTester tester) async {
|
|
final List<String> logs = <String>[];
|
|
Widget target({required bool absorbing}) => Align(
|
|
alignment: Alignment.topLeft,
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: SizedBox(
|
|
width: 100,
|
|
height: 100,
|
|
child: Listener(
|
|
onPointerDown: (_) { logs.add('down1'); },
|
|
child: MouseRegion(
|
|
onEnter: (_) { logs.add('enter1'); },
|
|
onExit: (_) { logs.add('exit1'); },
|
|
cursor: SystemMouseCursors.forbidden,
|
|
child: Stack(
|
|
children: <Widget>[
|
|
Listener(
|
|
onPointerDown: (_) { logs.add('down2'); },
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
onEnter: (_) { logs.add('enter2'); },
|
|
onExit: (_) { logs.add('exit2'); },
|
|
),
|
|
),
|
|
AbsorbPointer(
|
|
absorbing: absorbing,
|
|
child: Listener(
|
|
onPointerDown: (_) { logs.add('down3'); },
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.text,
|
|
onEnter: (_) { logs.add('enter3'); },
|
|
onExit: (_) { logs.add('exit3'); },
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestGesture gesture = await tester.createGesture(pointer: 1, kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer(location: const Offset(200, 200));
|
|
|
|
await tester.pumpWidget(target(absorbing: true));
|
|
expect(logs, isEmpty);
|
|
|
|
await gesture.moveTo(const Offset(50, 50));
|
|
expect(logs, <String>['enter1']);
|
|
logs.clear();
|
|
|
|
await gesture.down(const Offset(50, 50));
|
|
expect(logs, <String>['down1']);
|
|
logs.clear();
|
|
|
|
await gesture.up();
|
|
expect(logs, isEmpty);
|
|
|
|
await tester.pumpWidget(target(absorbing: false));
|
|
expect(logs, <String>['enter3']);
|
|
logs.clear();
|
|
|
|
await gesture.down(const Offset(50, 50));
|
|
expect(logs, <String>['down3', 'down1']);
|
|
logs.clear();
|
|
|
|
await gesture.up();
|
|
expect(logs, isEmpty);
|
|
|
|
await tester.pumpWidget(target(absorbing: true));
|
|
expect(logs, <String>['exit3']);
|
|
logs.clear();
|
|
});
|
|
|
|
testWidgets('Wrap implements debugFillProperties', (WidgetTester tester) async {
|
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
const Wrap(
|
|
spacing: 8.0, // gap between adjacent Text widget
|
|
runSpacing: 4.0, // gap between lines
|
|
textDirection: TextDirection.ltr,
|
|
verticalDirection: VerticalDirection.up,
|
|
children: <Widget>[
|
|
Text('Hamilton'),
|
|
Text('Lafayette'),
|
|
Text('Mulligan'),
|
|
],
|
|
).debugFillProperties(builder);
|
|
|
|
final List<String> description = builder.properties
|
|
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
|
|
.map((DiagnosticsNode node) => node.toString())
|
|
.toList();
|
|
|
|
expect(description, unorderedMatches(<dynamic>[
|
|
contains('direction: horizontal'),
|
|
contains('alignment: start'),
|
|
contains('spacing: 8.0'),
|
|
contains('runAlignment: start'),
|
|
contains('runSpacing: 4.0'),
|
|
contains('crossAxisAlignment: start'),
|
|
contains('textDirection: ltr'),
|
|
contains('verticalDirection: up'),
|
|
]));
|
|
});
|
|
}
|
|
|
|
HitsRenderBox hits(RenderBox renderBox) => HitsRenderBox(renderBox);
|
|
|
|
class HitsRenderBox extends Matcher {
|
|
const HitsRenderBox(this.renderBox);
|
|
|
|
final RenderBox renderBox;
|
|
|
|
@override
|
|
Description describe(Description description) =>
|
|
description.add('hit test result contains ').addDescriptionOf(renderBox);
|
|
|
|
@override
|
|
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
|
|
final HitTestResult hitTestResult = item as HitTestResult;
|
|
return hitTestResult.path.where(
|
|
(HitTestEntry entry) => entry.target == renderBox,
|
|
).isNotEmpty;
|
|
}
|
|
}
|
|
|
|
DoesNotHitRenderBox doesNotHit(RenderBox renderBox) => DoesNotHitRenderBox(renderBox);
|
|
|
|
class DoesNotHitRenderBox extends Matcher {
|
|
const DoesNotHitRenderBox(this.renderBox);
|
|
|
|
final RenderBox renderBox;
|
|
|
|
@override
|
|
Description describe(Description description) =>
|
|
description.add("hit test result doesn't contain ").addDescriptionOf(renderBox);
|
|
|
|
@override
|
|
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
|
|
final HitTestResult hitTestResult = item as HitTestResult;
|
|
return hitTestResult.path.where(
|
|
(HitTestEntry entry) => entry.target == renderBox,
|
|
).isEmpty;
|
|
}
|
|
}
|
|
|
|
class _MockPaintingContext extends Fake implements PaintingContext {
|
|
final List<RenderObject> children = <RenderObject>[];
|
|
final List<Offset> offsets = <Offset>[];
|
|
|
|
@override
|
|
final _MockCanvas canvas = _MockCanvas();
|
|
|
|
@override
|
|
void paintChild(RenderObject child, Offset offset) {
|
|
children.add(child);
|
|
offsets.add(offset);
|
|
}
|
|
}
|
|
|
|
class _MockCanvas extends Fake implements Canvas {
|
|
final List<Rect> rects = <Rect>[];
|
|
final List<Paint> paints = <Paint>[];
|
|
bool didPaint = false;
|
|
|
|
@override
|
|
void drawRect(Rect rect, Paint paint) {
|
|
rects.add(rect);
|
|
paints.add(paint);
|
|
}
|
|
}
|
|
|
|
|
|
class TestIgnorePointer extends StatefulWidget {
|
|
const TestIgnorePointer({super.key, required this.child});
|
|
|
|
final Widget child;
|
|
@override
|
|
State<StatefulWidget> createState() => TestIgnorePointerState();
|
|
}
|
|
|
|
class TestIgnorePointerState extends State<TestIgnorePointer> {
|
|
bool ignore = true;
|
|
|
|
void setIgnore(bool newIgnore) {
|
|
setState(() {
|
|
ignore = newIgnore;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return IgnorePointer(
|
|
ignoring: ignore,
|
|
child: widget.child,
|
|
);
|
|
}
|
|
}
|