blob: a5c5bf29b9afff88437ea32e6c47ecacae8468bf [file]
// 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/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'widgets_app_tester.dart';
void main() {
test('debugChildrenHaveDuplicateKeys control test', () {
const key = Key('key');
final children = <Widget>[Container(key: key), Container(key: key)];
final Widget widget = Flex(direction: Axis.vertical, children: children);
late FlutterError error;
try {
debugChildrenHaveDuplicateKeys(widget, children);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' Duplicate keys found.\n'
' If multiple keyed widgets exist as children of another widget,\n'
' they must have unique keys.\n'
' Flex(direction: vertical, mainAxisAlignment: start,\n'
' crossAxisAlignment: center) has multiple children with key\n'
" [<'key'>].\n",
),
);
}
});
test('debugItemsHaveDuplicateKeys control test', () {
const key = Key('key');
final items = <Widget>[Container(key: key), Container(key: key)];
late FlutterError error;
try {
debugItemsHaveDuplicateKeys(items);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
" Duplicate key found: [<'key'>].\n",
),
);
}
});
testWidgets('debugCheckHasTable control test', (WidgetTester tester) async {
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
late FlutterError error;
try {
debugCheckHasTable(context);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 4);
expect(error.diagnostics[2], isA<DiagnosticsProperty<Element>>());
expect(
error.toStringDeep(),
startsWith(
'FlutterError\n'
' No Table widget found.\n'
' Builder widgets require a Table widget ancestor.\n'
' The specific widget that could not find a Table ancestor was:\n'
' Builder\n'
' The ownership chain for the affected widget is: "Builder ←', // End of ownership chain omitted, not relevant for test.
),
);
}
return Container();
},
),
);
});
testWidgets('debugCheckHasMediaQuery control test', (WidgetTester tester) async {
// Cannot use tester.pumpWidget here because it wraps the widget in a View,
// which introduces a MediaQuery ancestor.
await tester.pumpWidget(
wrapWithView: false,
Builder(
builder: (BuildContext context) {
late FlutterError error;
try {
debugCheckHasMediaQuery(context);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 5);
expect(error.diagnostics[2], isA<DiagnosticsProperty<Element>>());
expect(error.diagnostics.last.level, DiagnosticLevel.hint);
expect(
error.diagnostics.last.toStringDeep(),
equalsIgnoringHashCodes(
'No MediaQuery ancestor could be found starting from the context\n'
'that was passed to MediaQuery.of(). This can happen because the\n'
'context used is not a descendant of a View widget, which\n'
'introduces a MediaQuery.\n',
),
);
expect(
error.toStringDeep(),
startsWith(
'FlutterError\n'
' No MediaQuery widget ancestor found.\n'
' Builder widgets require a MediaQuery widget ancestor.\n'
' The specific widget that could not find a MediaQuery ancestor\n'
' was:\n'
' Builder\n'
' The ownership chain for the affected widget is: "Builder ←', // Full chain omitted, not relevant for test.
),
);
expect(
error.toStringDeep(),
endsWith(
'[root]"\n' // End of ownership chain.
' No MediaQuery ancestor could be found starting from the context\n'
' that was passed to MediaQuery.of(). This can happen because the\n'
' context used is not a descendant of a View widget, which\n'
' introduces a MediaQuery.\n',
),
);
}
return View(view: tester.view, child: const SizedBox());
},
),
);
});
test('debugWidgetBuilderValue control test', () {
final Widget widget = Container();
FlutterError? error;
try {
debugWidgetBuilderValue(widget, null);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error!.diagnostics.length, 4);
expect(error.diagnostics[1], isA<DiagnosticsProperty<Widget>>());
expect(error.diagnostics[1].style, DiagnosticsTreeStyle.errorProperty);
expect(
error.diagnostics[1].toStringDeep(),
equalsIgnoringHashCodes(
'The offending widget is:\n'
' Container\n',
),
);
expect(error.diagnostics[2].level, DiagnosticLevel.info);
expect(error.diagnostics[3].level, DiagnosticLevel.hint);
expect(
error.diagnostics[3].toStringDeep(),
equalsIgnoringHashCodes(
'To return an empty space that causes the building widget to fill\n'
'available room, return "Container()". To return an empty space\n'
'that takes as little room as possible, return "Container(width:\n'
'0.0, height: 0.0)".\n',
),
);
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' A build function returned null.\n'
' The offending widget is:\n'
' Container\n'
' Build functions must never return null.\n'
' To return an empty space that causes the building widget to fill\n'
' available room, return "Container()". To return an empty space\n'
' that takes as little room as possible, return "Container(width:\n'
' 0.0, height: 0.0)".\n',
),
);
error = null;
}
try {
debugWidgetBuilderValue(widget, widget);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error!.diagnostics.length, 3);
expect(error.diagnostics[1], isA<DiagnosticsProperty<Widget>>());
expect(error.diagnostics[1].style, DiagnosticsTreeStyle.errorProperty);
expect(
error.diagnostics[1].toStringDeep(),
equalsIgnoringHashCodes(
'The offending widget is:\n'
' Container\n',
),
);
expect(
error.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' A build function returned context.widget.\n'
' The offending widget is:\n'
' Container\n'
" Build functions must never return their BuildContext parameter's\n"
' widget or a child that contains "context.widget". Doing so\n'
' introduces a loop in the widget tree that can cause the app to\n'
' crash.\n',
),
);
}
});
testWidgets('debugCheckHasWidgetsLocalizations throws', (WidgetTester tester) async {
final GlobalKey noLocalizationsAvailable = GlobalKey();
final GlobalKey localizationsAvailable = GlobalKey();
await tester.pumpWidget(
Container(
key: noLocalizationsAvailable,
child: WidgetsApp(
builder: (BuildContext context, Widget? child) {
return Container(key: localizationsAvailable);
},
color: const Color(0xFF4CAF50),
),
),
);
expect(
() => debugCheckHasWidgetsLocalizations(noLocalizationsAvailable.currentContext!),
throwsA(
isAssertionError.having(
(AssertionError e) => e.message,
'message',
contains('No WidgetsLocalizations found'),
),
),
);
expect(debugCheckHasWidgetsLocalizations(localizationsAvailable.currentContext!), isTrue);
});
test('debugAssertAllWidgetVarsUnset', () {
debugHighlightDeprecatedWidgets = true;
late FlutterError error;
try {
debugAssertAllWidgetVarsUnset(
'The value of a widget debug variable was changed by the test.',
);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 1);
expect(
error.toStringDeep(),
'FlutterError\n'
' The value of a widget debug variable was changed by the test.\n',
);
}
debugHighlightDeprecatedWidgets = false;
});
testWidgets('debugCreator of layers should not be null', (WidgetTester tester) async {
await tester.pumpWidget(
TestWidgetsApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
const ColorFiltered(
colorFilter: ColorFilter.mode(Color(0xFFFF0000), BlendMode.color),
child: Placeholder(),
),
const Opacity(opacity: 0.9, child: Placeholder()),
ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: const Placeholder(),
),
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: const Placeholder(),
),
ShaderMask(
shaderCallback: (Rect bounds) => const RadialGradient(
radius: 0.05,
colors: <Color>[Color(0xFFFF0000), Color(0xFF00FF00)],
tileMode: TileMode.mirror,
).createShader(bounds),
child: const Placeholder(),
),
CompositedTransformFollower(link: LayerLink()),
],
),
),
),
);
RenderObject renderObject;
renderObject = tester.firstRenderObject(find.byType(Opacity));
expect(renderObject.debugLayer?.debugCreator, isNotNull);
renderObject = tester.firstRenderObject(find.byType(ColorFiltered));
expect(renderObject.debugLayer?.debugCreator, isNotNull);
renderObject = tester.firstRenderObject(find.byType(ImageFiltered));
expect(renderObject.debugLayer?.debugCreator, isNotNull);
renderObject = tester.firstRenderObject(find.byType(BackdropFilter));
expect(renderObject.debugLayer?.debugCreator, isNotNull);
renderObject = tester.firstRenderObject(find.byType(ShaderMask));
expect(renderObject.debugLayer?.debugCreator, isNotNull);
renderObject = tester.firstRenderObject(find.byType(CompositedTransformFollower));
expect(renderObject.debugLayer?.debugCreator, isNotNull);
});
group('debugPaintFocusBoxes', () {
const kPrimaryFocusColor = Color(0xF000FF00);
const kAncestorOfPrimaryFocusColor = Color(0xF00000FF);
const kFocusableColor = Color(0xF000FFFF);
const kSkipTraversalColor = Color(0xF0FFFF00);
const kNotFocusableColor = Color(0xF0FF0000);
testWidgets('adds a border on each Focus widget if enabled',
(WidgetTester tester) async {
debugPaintFocusBoxes = true;
final nodePrimary = FocusNode(debugLabel: 'primary');
final nodeParent = FocusNode(debugLabel: 'parent');
final nodeFocusable = FocusNode(debugLabel: 'focusable');
final nodeSkipTraversal = FocusNode(debugLabel: 'skipTraversal', skipTraversal: true);
final nodeNotFocusable = FocusNode(debugLabel: 'notFocusable', canRequestFocus: false);
addTearDown(nodePrimary.dispose);
addTearDown(nodeParent.dispose);
addTearDown(nodeFocusable.dispose);
addTearDown(nodeSkipTraversal.dispose);
addTearDown(nodeNotFocusable.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Column(
children: <Widget>[
Focus(
focusNode: nodeParent,
child: Focus(
focusNode: nodePrimary,
child: const SizedBox(width: 10, height: 10),
),
),
Focus(focusNode: nodeFocusable, child: const SizedBox(width: 10, height: 10)),
Focus(focusNode: nodeSkipTraversal, child: const SizedBox(width: 10, height: 10)),
Focus(focusNode: nodeNotFocusable, child: const SizedBox(width: 10, height: 10)),
],
),
),
);
nodePrimary.requestFocus();
await tester.pumpAndSettle();
Color borderColorOf(FocusNode node) {
final Finder finder = find.descendant(
of: find.byWidgetPredicate((w) => w is Focus && w.focusNode == node),
matching: find.byType(DecoratedBox),
);
final DecoratedBox box = tester.widget<DecoratedBox>(finder.first);
return ((box.decoration as BoxDecoration).border! as Border).top.color;
}
expect(borderColorOf(nodePrimary), kPrimaryFocusColor);
expect(borderColorOf(nodeParent), kAncestorOfPrimaryFocusColor);
expect(borderColorOf(nodeFocusable), kFocusableColor);
expect(borderColorOf(nodeSkipTraversal), kSkipTraversalColor);
expect(borderColorOf(nodeNotFocusable), kNotFocusableColor);
debugPaintFocusBoxes = false;
});
testWidgets('does not add a border if disabled',
(WidgetTester tester) async {
final focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Focus(
focusNode: focusNode,
child: const SizedBox(width: 100, height: 100),
),
),
);
expect(find.byType(DecoratedBox), findsNothing);
});
testWidgets('border updates when focus changes', (WidgetTester tester) async {
debugPaintFocusBoxes = true;
final focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Focus(
focusNode: focusNode,
child: const SizedBox(width: 100, height: 100),
),
),
);
Color borderColor() {
final Finder finder = find.descendant(
of: find.byWidgetPredicate((w) => w is Focus && w.focusNode == focusNode),
matching: find.byType(DecoratedBox),
);
final DecoratedBox box = tester.widget<DecoratedBox>(finder.first);
return ((box.decoration as BoxDecoration).border! as Border).top.color;
}
// Start unfocused: cyan.
expect(borderColor(), kFocusableColor);
// Gain primary focus: green.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(borderColor(), kPrimaryFocusColor);
// Skip traversal but primary focus: green.
focusNode.skipTraversal = true;
await tester.pumpAndSettle();
expect(borderColor(), kPrimaryFocusColor);
// Lose primary focus and skip traversal: yellow.
focusNode.unfocus();
await tester.pumpAndSettle();
expect(borderColor(), kSkipTraversalColor);
// Not focusable: red.
focusNode.canRequestFocus = false;
await tester.pumpAndSettle();
expect(borderColor(), kNotFocusableColor);
debugPaintFocusBoxes = false;
});
testWidgets('no exceptions with multiple focus states', (WidgetTester tester) async {
debugPaintFocusBoxes = true;
final primary = FocusNode();
final unfocusable = FocusNode(canRequestFocus: false);
final skipTraversalNode = FocusNode(skipTraversal: true);
addTearDown(primary.dispose);
addTearDown(unfocusable.dispose);
addTearDown(skipTraversalNode.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Column(
children: <Widget>[
Focus(focusNode: primary, child: const SizedBox(width: 10, height: 10)),
Focus(focusNode: unfocusable, child: const SizedBox(width: 10, height: 10)),
Focus(focusNode: skipTraversalNode, child: const SizedBox(width: 10, height: 10)),
],
),
),
);
primary.requestFocus();
await tester.pump();
expect(tester.takeException(), isNull);
debugPaintFocusBoxes = false;
});
});
}