mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Fixes #172489 When an OutlinedButton.icon is initially built with a null icon and later updated to display an icon, the underlying widget implementation changes. The button switches from a standard OutlinedButton to a private _OutlinedButtonWithIcon widget. This switch causes the button's entire semantic node to be destroyed and a new one created in its place. For screen readers like VoiceOver, this behavior is disruptive. If an accessibility service is focused on the button when its icon appears, that focus is lost because the original semantic node is discarded, leading to strange behaviour as it makes an best effort to focus on an existing node. This PR resolves the issue by ensuring the same widget is used regardless of whether the icon is present. The logic for _OutlinedButtonWithIcon has been merged into the base class OutlinedButton, ensuring that no matter if you call OutlinedButton.icon or OutlinedButton you recieve a widget of type OutlinedButton. Demo: https://github.com/user-attachments/assets/e012bac9-823e-46f1-8eba-ec70e6b260a1
3051 lines
103 KiB
Dart
3051 lines
103 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 '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 '../widgets/semantics_tester.dart';
|
|
|
|
void main() {
|
|
TextStyle iconStyle(WidgetTester tester, IconData icon) {
|
|
final RichText iconRichText = tester.widget<RichText>(
|
|
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
|
|
);
|
|
return iconRichText.text.style!;
|
|
}
|
|
|
|
Color textColor(WidgetTester tester, String text) {
|
|
return tester.renderObject<RenderParagraph>(find.text(text)).text.style!.color!;
|
|
}
|
|
|
|
testWidgets('OutlinedButton, OutlinedButton.icon defaults', (WidgetTester tester) async {
|
|
const ColorScheme colorScheme = ColorScheme.light();
|
|
final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
|
|
final bool material3 = theme.useMaterial3;
|
|
|
|
// Enabled OutlinedButton
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Center(
|
|
child: OutlinedButton(onPressed: () {}, child: const Text('button')),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder buttonMaterial = find.descendant(
|
|
of: find.byType(OutlinedButton),
|
|
matching: find.byType(Material),
|
|
);
|
|
|
|
Material material = tester.widget<Material>(buttonMaterial);
|
|
expect(material.animationDuration, const Duration(milliseconds: 200));
|
|
expect(material.borderOnForeground, false);
|
|
expect(material.borderRadius, null);
|
|
expect(material.clipBehavior, Clip.none);
|
|
expect(material.color, Colors.transparent);
|
|
expect(material.elevation, 0.0);
|
|
expect(material.shadowColor, material3 ? Colors.transparent : const Color(0xff000000));
|
|
|
|
expect(
|
|
material.shape,
|
|
material3
|
|
? StadiumBorder(side: BorderSide(color: colorScheme.outline))
|
|
: RoundedRectangleBorder(
|
|
side: BorderSide(color: colorScheme.onSurface.withOpacity(0.12)),
|
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
|
),
|
|
);
|
|
|
|
expect(material.textStyle!.color, colorScheme.primary);
|
|
expect(material.textStyle!.fontFamily, 'Roboto');
|
|
expect(material.textStyle!.fontSize, 14);
|
|
expect(material.textStyle!.fontWeight, FontWeight.w500);
|
|
expect(material.type, MaterialType.button);
|
|
|
|
final Align align = tester.firstWidget<Align>(
|
|
find.ancestor(of: find.text('button'), matching: find.byType(Align)),
|
|
);
|
|
expect(align.alignment, Alignment.center);
|
|
|
|
final Offset center = tester.getCenter(find.byType(OutlinedButton));
|
|
final TestGesture gesture = await tester.startGesture(center);
|
|
await tester.pump(); // start the splash animation
|
|
await tester.pump(const Duration(milliseconds: 100)); // splash is underway
|
|
|
|
// Material 3 uses the InkSparkle which uses a shader, so we can't capture
|
|
// the effect with paint methods.
|
|
if (!material3) {
|
|
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
|
|
(RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
|
|
);
|
|
expect(inkFeatures, paints..circle(color: colorScheme.primary.withOpacity(0.12)));
|
|
}
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
// No change vs enabled and not pressed.
|
|
material = tester.widget<Material>(buttonMaterial);
|
|
expect(material.animationDuration, const Duration(milliseconds: 200));
|
|
expect(material.borderOnForeground, false);
|
|
expect(material.borderRadius, null);
|
|
expect(material.clipBehavior, Clip.none);
|
|
expect(material.color, Colors.transparent);
|
|
expect(material.elevation, 0.0);
|
|
expect(material.shadowColor, material3 ? Colors.transparent : const Color(0xff000000));
|
|
|
|
expect(
|
|
material.shape,
|
|
material3
|
|
? StadiumBorder(side: BorderSide(color: colorScheme.outline))
|
|
: RoundedRectangleBorder(
|
|
side: BorderSide(color: colorScheme.onSurface.withOpacity(0.12)),
|
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
|
),
|
|
);
|
|
|
|
expect(material.textStyle!.color, colorScheme.primary);
|
|
expect(material.textStyle!.fontFamily, 'Roboto');
|
|
expect(material.textStyle!.fontSize, 14);
|
|
expect(material.textStyle!.fontWeight, FontWeight.w500);
|
|
expect(material.type, MaterialType.button);
|
|
|
|
// Enabled OutlinedButton.icon
|
|
final Key iconButtonKey = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Center(
|
|
child: OutlinedButton.icon(
|
|
key: iconButtonKey,
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('label'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder iconButtonMaterial = find.descendant(
|
|
of: find.byKey(iconButtonKey),
|
|
matching: find.byType(Material),
|
|
);
|
|
|
|
material = tester.widget<Material>(iconButtonMaterial);
|
|
expect(material.animationDuration, const Duration(milliseconds: 200));
|
|
expect(material.borderOnForeground, false);
|
|
expect(material.borderRadius, null);
|
|
expect(material.clipBehavior, Clip.none);
|
|
expect(material.color, Colors.transparent);
|
|
expect(material.elevation, 0.0);
|
|
expect(material.shadowColor, material3 ? Colors.transparent : const Color(0xff000000));
|
|
|
|
expect(
|
|
material.shape,
|
|
material3
|
|
? StadiumBorder(side: BorderSide(color: colorScheme.outline))
|
|
: RoundedRectangleBorder(
|
|
side: BorderSide(color: colorScheme.onSurface.withOpacity(0.12)),
|
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
|
),
|
|
);
|
|
|
|
expect(material.textStyle!.color, colorScheme.primary);
|
|
expect(material.textStyle!.fontFamily, 'Roboto');
|
|
expect(material.textStyle!.fontSize, 14);
|
|
expect(material.textStyle!.fontWeight, FontWeight.w500);
|
|
expect(material.type, MaterialType.button);
|
|
|
|
// Disabled OutlinedButton
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: const Center(child: OutlinedButton(onPressed: null, child: Text('button'))),
|
|
),
|
|
);
|
|
|
|
material = tester.widget<Material>(buttonMaterial);
|
|
expect(material.animationDuration, const Duration(milliseconds: 200));
|
|
expect(material.borderOnForeground, false);
|
|
expect(material.borderRadius, null);
|
|
expect(material.clipBehavior, Clip.none);
|
|
expect(material.color, Colors.transparent);
|
|
expect(material.elevation, 0.0);
|
|
expect(material.shadowColor, material3 ? Colors.transparent : const Color(0xff000000));
|
|
|
|
expect(
|
|
material.shape,
|
|
material3
|
|
? StadiumBorder(side: BorderSide(color: colorScheme.onSurface.withOpacity(0.12)))
|
|
: RoundedRectangleBorder(
|
|
side: BorderSide(color: colorScheme.onSurface.withOpacity(0.12)),
|
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
|
),
|
|
);
|
|
|
|
expect(material.textStyle!.color, colorScheme.onSurface.withOpacity(0.38));
|
|
expect(material.textStyle!.fontFamily, 'Roboto');
|
|
expect(material.textStyle!.fontSize, 14);
|
|
expect(material.textStyle!.fontWeight, FontWeight.w500);
|
|
expect(material.type, MaterialType.button);
|
|
});
|
|
|
|
testWidgets(
|
|
'OutlinedButton.defaultStyle produces a ButtonStyle with appropriate non-null values',
|
|
(WidgetTester tester) async {
|
|
const ColorScheme colorScheme = ColorScheme.light();
|
|
final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
|
|
|
|
final OutlinedButton button = OutlinedButton(onPressed: () {}, child: const Text('button'));
|
|
BuildContext? capturedContext;
|
|
// Enabled OutlinedButton
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Center(
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
capturedContext = context;
|
|
return button;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final ButtonStyle style = button.defaultStyleOf(capturedContext!);
|
|
|
|
// Properties that must be non-null.
|
|
expect(style.textStyle, isNotNull, reason: 'textStyle style');
|
|
expect(style.backgroundColor, isNotNull, reason: 'backgroundColor style');
|
|
expect(style.foregroundColor, isNotNull, reason: 'foregroundColor style');
|
|
expect(style.overlayColor, isNotNull, reason: 'overlayColor style');
|
|
expect(style.shadowColor, isNotNull, reason: 'shadowColor style');
|
|
expect(style.surfaceTintColor, isNotNull, reason: 'surfaceTintColor style');
|
|
expect(style.elevation, isNotNull, reason: 'elevation style');
|
|
expect(style.padding, isNotNull, reason: 'padding style');
|
|
expect(style.minimumSize, isNotNull, reason: 'minimumSize style');
|
|
expect(style.maximumSize, isNotNull, reason: 'maximumSize style');
|
|
expect(style.iconColor, isNotNull, reason: 'iconColor style');
|
|
expect(style.iconSize, isNotNull, reason: 'iconSize style');
|
|
expect(style.shape, isNotNull, reason: 'shape style');
|
|
expect(style.mouseCursor, isNotNull, reason: 'mouseCursor style');
|
|
expect(style.visualDensity, isNotNull, reason: 'visualDensity style');
|
|
expect(style.tapTargetSize, isNotNull, reason: 'tapTargetSize style');
|
|
expect(style.animationDuration, isNotNull, reason: 'animationDuration style');
|
|
expect(style.enableFeedback, isNotNull, reason: 'enableFeedback style');
|
|
expect(style.alignment, isNotNull, reason: 'alignment style');
|
|
expect(style.splashFactory, isNotNull, reason: 'splashFactory style');
|
|
expect(style.side, isNotNull, reason: 'side style');
|
|
|
|
// Properties that are expected to be null.
|
|
expect(style.fixedSize, isNull, reason: 'fixedSize style');
|
|
expect(style.backgroundBuilder, isNull, reason: 'backgroundBuilder style');
|
|
expect(style.foregroundBuilder, isNull, reason: 'foregroundBuilder style');
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'OutlinedButton.defaultStyle with an icon produces a ButtonStyle with appropriate non-null values',
|
|
(WidgetTester tester) async {
|
|
const ColorScheme colorScheme = ColorScheme.light();
|
|
final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
|
|
|
|
final OutlinedButton button = OutlinedButton.icon(
|
|
onPressed: () {},
|
|
icon: const SizedBox(),
|
|
label: const Text('button'),
|
|
);
|
|
BuildContext? capturedContext;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Center(
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
capturedContext = context;
|
|
return button;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final ButtonStyle style = button.defaultStyleOf(capturedContext!);
|
|
|
|
// Properties that must be non-null.
|
|
expect(style.textStyle, isNotNull, reason: 'textStyle style');
|
|
expect(style.backgroundColor, isNotNull, reason: 'backgroundColor style');
|
|
expect(style.foregroundColor, isNotNull, reason: 'foregroundColor style');
|
|
expect(style.overlayColor, isNotNull, reason: 'overlayColor style');
|
|
expect(style.shadowColor, isNotNull, reason: 'shadowColor style');
|
|
expect(style.surfaceTintColor, isNotNull, reason: 'surfaceTintColor style');
|
|
expect(style.elevation, isNotNull, reason: 'elevation style');
|
|
expect(style.padding, isNotNull, reason: 'padding style');
|
|
expect(style.minimumSize, isNotNull, reason: 'minimumSize style');
|
|
expect(style.maximumSize, isNotNull, reason: 'maximumSize style');
|
|
expect(style.iconColor, isNotNull, reason: 'iconColor style');
|
|
expect(style.iconSize, isNotNull, reason: 'iconSize style');
|
|
expect(style.shape, isNotNull, reason: 'shape style');
|
|
expect(style.mouseCursor, isNotNull, reason: 'mouseCursor style');
|
|
expect(style.visualDensity, isNotNull, reason: 'visualDensity style');
|
|
expect(style.tapTargetSize, isNotNull, reason: 'tapTargetSize style');
|
|
expect(style.animationDuration, isNotNull, reason: 'animationDuration style');
|
|
expect(style.enableFeedback, isNotNull, reason: 'enableFeedback style');
|
|
expect(style.alignment, isNotNull, reason: 'alignment style');
|
|
expect(style.splashFactory, isNotNull, reason: 'splashFactory style');
|
|
expect(style.side, isNotNull, reason: 'side style');
|
|
|
|
// Properties that are expected to be null.
|
|
expect(style.fixedSize, isNull, reason: 'fixedSize style');
|
|
expect(style.backgroundBuilder, isNull, reason: 'backgroundBuilder style');
|
|
expect(style.foregroundBuilder, isNull, reason: 'foregroundBuilder style');
|
|
},
|
|
);
|
|
|
|
testWidgets('OutlinedButton.icon produces the correct widgets if icon is null', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const ColorScheme colorScheme = ColorScheme.light();
|
|
final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
|
|
final Key iconButtonKey = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Center(
|
|
child: OutlinedButton.icon(
|
|
key: iconButtonKey,
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('label'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byIcon(Icons.add), findsOneWidget);
|
|
expect(find.text('label'), findsOneWidget);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Center(
|
|
child: OutlinedButton.icon(
|
|
key: iconButtonKey,
|
|
onPressed: () {},
|
|
// No icon specified.
|
|
label: const Text('label'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byIcon(Icons.add), findsNothing);
|
|
expect(find.text('label'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('OutlinedButton default overlayColor resolves pressed state', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final FocusNode focusNode = FocusNode();
|
|
final ThemeData theme = ThemeData();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
return OutlinedButton(
|
|
onPressed: () {},
|
|
focusNode: focusNode,
|
|
child: const Text('OutlinedButton'),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
RenderObject overlayColor() {
|
|
return tester.allRenderObjects.firstWhere(
|
|
(RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
|
|
);
|
|
}
|
|
|
|
// Hovered.
|
|
final Offset center = tester.getCenter(find.byType(OutlinedButton));
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
await gesture.moveTo(center);
|
|
await tester.pumpAndSettle();
|
|
expect(overlayColor(), paints..rect(color: theme.colorScheme.primary.withOpacity(0.08)));
|
|
|
|
// Highlighted (pressed).
|
|
await gesture.down(center);
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
overlayColor(),
|
|
paints
|
|
..rect()
|
|
..rect(color: theme.colorScheme.primary.withOpacity(0.1)),
|
|
);
|
|
// Remove pressed and hovered states
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
await gesture.moveTo(const Offset(0, 50));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Focused.
|
|
focusNode.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
expect(overlayColor(), paints..rect(color: theme.colorScheme.primary.withOpacity(0.1)));
|
|
|
|
focusNode.dispose();
|
|
});
|
|
|
|
testWidgets('Does OutlinedButton work with hover', (WidgetTester tester) async {
|
|
const Color hoverColor = Color(0xff001122);
|
|
|
|
Color? getOverlayColor(Set<WidgetState> states) {
|
|
return states.contains(WidgetState.hovered) ? hoverColor : null;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: OutlinedButton(
|
|
style: ButtonStyle(
|
|
overlayColor: WidgetStateProperty.resolveWith<Color?>(getOverlayColor),
|
|
),
|
|
onPressed: () {},
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
await gesture.moveTo(tester.getCenter(find.byType(OutlinedButton)));
|
|
await tester.pumpAndSettle();
|
|
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
|
|
(RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
|
|
);
|
|
expect(inkFeatures, paints..rect(color: hoverColor));
|
|
});
|
|
|
|
testWidgets('Does OutlinedButton work with focus', (WidgetTester tester) async {
|
|
final ThemeData theme = ThemeData();
|
|
final ColorScheme colors = theme.colorScheme;
|
|
const Color focusColor = Color(0xff001122);
|
|
|
|
Color? getOverlayColor(Set<WidgetState> states) {
|
|
return states.contains(WidgetState.focused) ? focusColor : null;
|
|
}
|
|
|
|
final FocusNode focusNode = FocusNode(debugLabel: 'OutlinedButton Node');
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: OutlinedButton(
|
|
style: ButtonStyle(
|
|
overlayColor: WidgetStateProperty.resolveWith<Color?>(getOverlayColor),
|
|
),
|
|
focusNode: focusNode,
|
|
onPressed: () {},
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
);
|
|
|
|
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
|
focusNode.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
|
|
(RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
|
|
);
|
|
expect(inkFeatures, paints..rect(color: focusColor));
|
|
|
|
final Finder buttonMaterial = find.descendant(
|
|
of: find.byType(OutlinedButton),
|
|
matching: find.byType(Material),
|
|
);
|
|
|
|
final Material material = tester.widget<Material>(buttonMaterial);
|
|
|
|
expect(material.shape, StadiumBorder(side: BorderSide(color: colors.primary)));
|
|
|
|
focusNode.dispose();
|
|
});
|
|
|
|
testWidgets('Does OutlinedButton work with autofocus', (WidgetTester tester) async {
|
|
final ThemeData theme = ThemeData();
|
|
final ColorScheme colors = theme.colorScheme;
|
|
const Color focusColor = Color(0xff001122);
|
|
|
|
Color? getOverlayColor(Set<WidgetState> states) {
|
|
return states.contains(WidgetState.focused) ? focusColor : null;
|
|
}
|
|
|
|
final FocusNode focusNode = FocusNode(debugLabel: 'OutlinedButton Node');
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: OutlinedButton(
|
|
autofocus: true,
|
|
style: ButtonStyle(
|
|
overlayColor: WidgetStateProperty.resolveWith<Color?>(getOverlayColor),
|
|
),
|
|
focusNode: focusNode,
|
|
onPressed: () {},
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
);
|
|
|
|
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
|
await tester.pumpAndSettle();
|
|
|
|
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere(
|
|
(RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures',
|
|
);
|
|
expect(inkFeatures, paints..rect(color: focusColor));
|
|
|
|
final Finder buttonMaterial = find.descendant(
|
|
of: find.byType(OutlinedButton),
|
|
matching: find.byType(Material),
|
|
);
|
|
|
|
final Material material = tester.widget<Material>(buttonMaterial);
|
|
|
|
expect(material.shape, StadiumBorder(side: BorderSide(color: colors.primary)));
|
|
focusNode.dispose();
|
|
});
|
|
|
|
testWidgets(
|
|
'Default OutlinedButton meets a11y contrast guidelines',
|
|
(WidgetTester tester) async {
|
|
final FocusNode focusNode = FocusNode();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: OutlinedButton(
|
|
onPressed: () {},
|
|
focusNode: focusNode,
|
|
child: const Text('OutlinedButton'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Default, not disabled.
|
|
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
|
|
|
// Hovered.
|
|
final Offset center = tester.getCenter(find.byType(OutlinedButton));
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
await gesture.moveTo(center);
|
|
await tester.pumpAndSettle();
|
|
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
|
|
|
// Highlighted (pressed).
|
|
await gesture.down(center);
|
|
await tester.pump(); // Start the splash and highlight animations.
|
|
await tester.pump(
|
|
const Duration(milliseconds: 800),
|
|
); // Wait for splash and highlight to be well under way.
|
|
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// Focused.
|
|
focusNode.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
|
|
|
focusNode.dispose();
|
|
},
|
|
skip: isBrowser, // https://github.com/flutter/flutter/issues/44115
|
|
);
|
|
|
|
testWidgets(
|
|
'OutlinedButton with colored theme meets a11y contrast guidelines',
|
|
(WidgetTester tester) async {
|
|
final FocusNode focusNode = FocusNode();
|
|
|
|
Color getTextColor(Set<WidgetState> states) {
|
|
final Set<WidgetState> interactiveStates = <WidgetState>{
|
|
WidgetState.pressed,
|
|
WidgetState.hovered,
|
|
WidgetState.focused,
|
|
};
|
|
if (states.any(interactiveStates.contains)) {
|
|
return Colors.blue[900]!;
|
|
}
|
|
return Colors.blue[800]!;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData.from(colorScheme: ColorScheme.fromSwatch()),
|
|
home: Scaffold(
|
|
backgroundColor: Colors.white,
|
|
body: Center(
|
|
child: OutlinedButtonTheme(
|
|
data: OutlinedButtonThemeData(
|
|
style: ButtonStyle(
|
|
foregroundColor: WidgetStateProperty.resolveWith<Color>(getTextColor),
|
|
),
|
|
),
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
return OutlinedButton(
|
|
onPressed: () {},
|
|
focusNode: focusNode,
|
|
child: const Text('OutlinedButton'),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Default, not disabled.
|
|
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
|
|
|
// Focused.
|
|
focusNode.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
|
|
|
// Hovered.
|
|
final Offset center = tester.getCenter(find.byType(OutlinedButton));
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
await gesture.moveTo(center);
|
|
await tester.pumpAndSettle();
|
|
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
|
|
|
// Highlighted (pressed).
|
|
await gesture.down(center);
|
|
await tester.pump(); // Start the splash and highlight animations.
|
|
await tester.pump(
|
|
const Duration(milliseconds: 800),
|
|
); // Wait for splash and highlight to be well under way.
|
|
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
|
|
|
focusNode.dispose();
|
|
},
|
|
skip: isBrowser, // https://github.com/flutter/flutter/issues/44115
|
|
);
|
|
|
|
testWidgets('OutlinedButton uses stateful color for text color in different states', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const String buttonText = 'OutlinedButton';
|
|
final FocusNode focusNode = FocusNode();
|
|
const Color pressedColor = Color(0x00000001);
|
|
const Color hoverColor = Color(0x00000002);
|
|
const Color focusedColor = Color(0x00000003);
|
|
const Color defaultColor = Color(0x00000004);
|
|
|
|
Color getTextColor(Set<WidgetState> states) {
|
|
if (states.contains(WidgetState.pressed)) {
|
|
return pressedColor;
|
|
}
|
|
if (states.contains(WidgetState.hovered)) {
|
|
return hoverColor;
|
|
}
|
|
if (states.contains(WidgetState.focused)) {
|
|
return focusedColor;
|
|
}
|
|
return defaultColor;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: OutlinedButton(
|
|
style: ButtonStyle(
|
|
foregroundColor: WidgetStateProperty.resolveWith<Color>(getTextColor),
|
|
),
|
|
onPressed: () {},
|
|
focusNode: focusNode,
|
|
child: const Text(buttonText),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Default, not disabled.
|
|
expect(textColor(tester, buttonText), equals(defaultColor));
|
|
|
|
// Focused.
|
|
focusNode.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
expect(textColor(tester, buttonText), focusedColor);
|
|
|
|
// Hovered.
|
|
final Offset center = tester.getCenter(find.byType(OutlinedButton));
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
await gesture.moveTo(center);
|
|
await tester.pumpAndSettle();
|
|
expect(textColor(tester, buttonText), hoverColor);
|
|
|
|
// Highlighted (pressed).
|
|
await gesture.down(center);
|
|
await tester.pump(); // Start the splash and highlight animations.
|
|
await tester.pump(
|
|
const Duration(milliseconds: 800),
|
|
); // Wait for splash and highlight to be well under way.
|
|
expect(textColor(tester, buttonText), pressedColor);
|
|
|
|
focusNode.dispose();
|
|
});
|
|
|
|
testWidgets('OutlinedButton uses stateful color for icon color in different states', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final FocusNode focusNode = FocusNode();
|
|
final Key buttonKey = UniqueKey();
|
|
|
|
const Color pressedColor = Color(0x00000001);
|
|
const Color hoverColor = Color(0x00000002);
|
|
const Color focusedColor = Color(0x00000003);
|
|
const Color defaultColor = Color(0x00000004);
|
|
|
|
Color getIconColor(Set<WidgetState> states) {
|
|
if (states.contains(WidgetState.pressed)) {
|
|
return pressedColor;
|
|
}
|
|
if (states.contains(WidgetState.hovered)) {
|
|
return hoverColor;
|
|
}
|
|
if (states.contains(WidgetState.focused)) {
|
|
return focusedColor;
|
|
}
|
|
return defaultColor;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: OutlinedButton.icon(
|
|
key: buttonKey,
|
|
style: ButtonStyle(iconColor: WidgetStateProperty.resolveWith<Color>(getIconColor)),
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('OutlinedButton'),
|
|
onPressed: () {},
|
|
focusNode: focusNode,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Default, not disabled.
|
|
expect(iconStyle(tester, Icons.add).color, equals(defaultColor));
|
|
|
|
// Focused.
|
|
focusNode.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
expect(iconStyle(tester, Icons.add).color, focusedColor);
|
|
|
|
// Hovered.
|
|
final Offset center = tester.getCenter(find.byKey(buttonKey));
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
await gesture.moveTo(center);
|
|
await tester.pumpAndSettle();
|
|
expect(iconStyle(tester, Icons.add).color, hoverColor);
|
|
|
|
// Highlighted (pressed).
|
|
await gesture.down(center);
|
|
await tester.pump(); // Start the splash and highlight animations.
|
|
await tester.pump(
|
|
const Duration(milliseconds: 800),
|
|
); // Wait for splash and highlight to be well under way.
|
|
expect(iconStyle(tester, Icons.add).color, pressedColor);
|
|
|
|
focusNode.dispose();
|
|
});
|
|
|
|
testWidgets('OutlinedButton uses stateful color for border color in different states', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final FocusNode focusNode = FocusNode();
|
|
|
|
const Color pressedColor = Color(0x00000001);
|
|
const Color hoverColor = Color(0x00000002);
|
|
const Color focusedColor = Color(0x00000003);
|
|
const Color defaultColor = Color(0x00000004);
|
|
|
|
BorderSide getBorderSide(Set<WidgetState> states) {
|
|
if (states.contains(WidgetState.pressed)) {
|
|
return const BorderSide(color: pressedColor);
|
|
}
|
|
if (states.contains(WidgetState.hovered)) {
|
|
return const BorderSide(color: hoverColor);
|
|
}
|
|
if (states.contains(WidgetState.focused)) {
|
|
return const BorderSide(color: focusedColor);
|
|
}
|
|
return const BorderSide(color: defaultColor);
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: OutlinedButton(
|
|
style: ButtonStyle(
|
|
side: WidgetStateProperty.resolveWith<BorderSide>(getBorderSide),
|
|
// Test assumes a rounded rect for the shape
|
|
shape: ButtonStyleButton.allOrNull(
|
|
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4))),
|
|
),
|
|
),
|
|
onPressed: () {},
|
|
focusNode: focusNode,
|
|
child: const Text('OutlinedButton'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder outlinedButton = find.byType(OutlinedButton);
|
|
|
|
// Default, not disabled.
|
|
expect(outlinedButton, paints..drrect(color: defaultColor));
|
|
|
|
// Focused.
|
|
focusNode.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
expect(outlinedButton, paints..drrect(color: focusedColor));
|
|
|
|
// Hovered.
|
|
final Offset center = tester.getCenter(find.byType(OutlinedButton));
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
await gesture.moveTo(center);
|
|
await tester.pumpAndSettle();
|
|
expect(outlinedButton, paints..drrect(color: hoverColor));
|
|
|
|
// Highlighted (pressed).
|
|
await gesture.down(center);
|
|
await tester.pumpAndSettle();
|
|
expect(outlinedButton, paints..drrect(color: pressedColor));
|
|
|
|
focusNode.dispose();
|
|
});
|
|
|
|
testWidgets(
|
|
'OutlinedButton onPressed and onLongPress callbacks are correctly called when non-null',
|
|
(WidgetTester tester) async {
|
|
bool wasPressed;
|
|
Finder outlinedButton;
|
|
|
|
Widget buildFrame({VoidCallback? onPressed, VoidCallback? onLongPress}) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: OutlinedButton(
|
|
onPressed: onPressed,
|
|
onLongPress: onLongPress,
|
|
child: const Text('button'),
|
|
),
|
|
);
|
|
}
|
|
|
|
// onPressed not null, onLongPress null.
|
|
wasPressed = false;
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
onPressed: () {
|
|
wasPressed = true;
|
|
},
|
|
),
|
|
);
|
|
outlinedButton = find.byType(OutlinedButton);
|
|
expect(tester.widget<OutlinedButton>(outlinedButton).enabled, true);
|
|
await tester.tap(outlinedButton);
|
|
expect(wasPressed, true);
|
|
|
|
// onPressed null, onLongPress not null.
|
|
wasPressed = false;
|
|
await tester.pumpWidget(
|
|
buildFrame(
|
|
onLongPress: () {
|
|
wasPressed = true;
|
|
},
|
|
),
|
|
);
|
|
outlinedButton = find.byType(OutlinedButton);
|
|
expect(tester.widget<OutlinedButton>(outlinedButton).enabled, true);
|
|
await tester.longPress(outlinedButton);
|
|
expect(wasPressed, true);
|
|
|
|
// onPressed null, onLongPress null.
|
|
await tester.pumpWidget(buildFrame());
|
|
outlinedButton = find.byType(OutlinedButton);
|
|
expect(tester.widget<OutlinedButton>(outlinedButton).enabled, false);
|
|
},
|
|
);
|
|
|
|
testWidgets("OutlinedButton response doesn't hover when disabled", (WidgetTester tester) async {
|
|
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
|
|
final FocusNode focusNode = FocusNode(debugLabel: 'OutlinedButton Focus');
|
|
final GlobalKey childKey = GlobalKey();
|
|
bool hovering = false;
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: SizedBox(
|
|
width: 100,
|
|
height: 100,
|
|
child: OutlinedButton(
|
|
autofocus: true,
|
|
onPressed: () {},
|
|
onLongPress: () {},
|
|
onHover: (bool value) {
|
|
hovering = value;
|
|
},
|
|
focusNode: focusNode,
|
|
child: SizedBox(key: childKey),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
await gesture.moveTo(tester.getCenter(find.byKey(childKey)));
|
|
await tester.pumpAndSettle();
|
|
expect(hovering, isTrue);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: SizedBox(
|
|
width: 100,
|
|
height: 100,
|
|
child: OutlinedButton(
|
|
focusNode: focusNode,
|
|
onHover: (bool value) {
|
|
hovering = value;
|
|
},
|
|
onPressed: null,
|
|
child: SizedBox(key: childKey),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(focusNode.hasPrimaryFocus, isFalse);
|
|
|
|
focusNode.dispose();
|
|
});
|
|
|
|
testWidgets('disabled and hovered OutlinedButton responds to mouse-exit', (
|
|
WidgetTester tester,
|
|
) async {
|
|
int onHoverCount = 0;
|
|
late bool hover;
|
|
|
|
Widget buildFrame({required bool enabled}) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: 100,
|
|
height: 100,
|
|
child: OutlinedButton(
|
|
onPressed: enabled ? () {} : null,
|
|
onHover: (bool value) {
|
|
onHoverCount += 1;
|
|
hover = value;
|
|
},
|
|
child: const Text('OutlinedButton'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame(enabled: true));
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
|
|
await gesture.moveTo(tester.getCenter(find.byType(OutlinedButton)));
|
|
await tester.pumpAndSettle();
|
|
expect(onHoverCount, 1);
|
|
expect(hover, true);
|
|
|
|
await tester.pumpWidget(buildFrame(enabled: false));
|
|
await tester.pumpAndSettle();
|
|
await gesture.moveTo(Offset.zero);
|
|
// Even though the OutlinedButton has been disabled, the mouse-exit still
|
|
// causes onHover(false) to be called.
|
|
expect(onHoverCount, 2);
|
|
expect(hover, false);
|
|
|
|
await gesture.moveTo(tester.getCenter(find.byType(OutlinedButton)));
|
|
await tester.pumpAndSettle();
|
|
// We no longer see hover events because the OutlinedButton is disabled
|
|
// and it's no longer in the "hovering" state.
|
|
expect(onHoverCount, 2);
|
|
expect(hover, false);
|
|
|
|
await tester.pumpWidget(buildFrame(enabled: true));
|
|
await tester.pumpAndSettle();
|
|
// The OutlinedButton was enabled while it contained the mouse, however
|
|
// we do not call onHover() because it may call setState().
|
|
expect(onHoverCount, 2);
|
|
expect(hover, false);
|
|
|
|
await gesture.moveTo(tester.getCenter(find.byType(OutlinedButton)) - const Offset(1, 1));
|
|
await tester.pumpAndSettle();
|
|
// Moving the mouse a little within the OutlinedButton doesn't change anything.
|
|
expect(onHoverCount, 2);
|
|
expect(hover, false);
|
|
});
|
|
|
|
testWidgets('Can set OutlinedButton focus and Can set unFocus.', (WidgetTester tester) async {
|
|
final FocusNode node = FocusNode(debugLabel: 'OutlinedButton Focus');
|
|
bool gotFocus = false;
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: OutlinedButton(
|
|
focusNode: node,
|
|
onFocusChange: (bool focused) => gotFocus = focused,
|
|
onPressed: () {},
|
|
child: const SizedBox(),
|
|
),
|
|
),
|
|
);
|
|
|
|
node.requestFocus();
|
|
|
|
await tester.pump();
|
|
|
|
expect(gotFocus, isTrue);
|
|
expect(node.hasFocus, isTrue);
|
|
|
|
node.unfocus();
|
|
await tester.pump();
|
|
|
|
expect(gotFocus, isFalse);
|
|
expect(node.hasFocus, isFalse);
|
|
|
|
node.dispose();
|
|
});
|
|
|
|
testWidgets('When OutlinedButton disable, Can not set OutlinedButton focus.', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final FocusNode node = FocusNode(debugLabel: 'OutlinedButton Focus');
|
|
bool gotFocus = false;
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: OutlinedButton(
|
|
focusNode: node,
|
|
onFocusChange: (bool focused) => gotFocus = focused,
|
|
onPressed: null,
|
|
child: const SizedBox(),
|
|
),
|
|
),
|
|
);
|
|
|
|
node.requestFocus();
|
|
|
|
await tester.pump();
|
|
|
|
expect(gotFocus, isFalse);
|
|
expect(node.hasFocus, isFalse);
|
|
|
|
node.dispose();
|
|
});
|
|
|
|
testWidgets("Outline button doesn't crash if disabled during a gesture", (
|
|
WidgetTester tester,
|
|
) async {
|
|
Widget buildFrame(VoidCallback? onPressed) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Theme(
|
|
data: ThemeData(),
|
|
child: Center(
|
|
child: OutlinedButton(onPressed: onPressed, child: const Text('button')),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame(() {}));
|
|
await tester.press(find.byType(OutlinedButton));
|
|
await tester.pumpAndSettle();
|
|
await tester.pumpWidget(buildFrame(null));
|
|
await tester.pumpAndSettle();
|
|
});
|
|
|
|
testWidgets('OutlinedButton shape and border component overrides', (WidgetTester tester) async {
|
|
const Color fillColor = Color(0xFF00FF00);
|
|
const BorderSide disabledBorderSide = BorderSide(color: Color(0xFFFF0000), width: 3);
|
|
const BorderSide enabledBorderSide = BorderSide(color: Color(0xFFFF00FF), width: 4);
|
|
const BorderSide pressedBorderSide = BorderSide(color: Color(0xFF0000FF), width: 5);
|
|
|
|
Widget buildFrame({VoidCallback? onPressed}) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Theme(
|
|
data: ThemeData(
|
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
textTheme: Typography.englishLike2014,
|
|
),
|
|
child: Container(
|
|
alignment: Alignment.topLeft,
|
|
child: OutlinedButton(
|
|
style:
|
|
OutlinedButton.styleFrom(
|
|
shape: const RoundedRectangleBorder(),
|
|
// default border radius is 0
|
|
backgroundColor: fillColor,
|
|
minimumSize: const Size(64, 36),
|
|
).copyWith(
|
|
side: WidgetStateProperty.resolveWith<BorderSide>((Set<WidgetState> states) {
|
|
if (states.contains(WidgetState.disabled)) {
|
|
return disabledBorderSide;
|
|
}
|
|
if (states.contains(WidgetState.pressed)) {
|
|
return pressedBorderSide;
|
|
}
|
|
return enabledBorderSide;
|
|
}),
|
|
),
|
|
clipBehavior: Clip.antiAlias,
|
|
onPressed: onPressed,
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
final Finder outlinedButton = find.byType(OutlinedButton);
|
|
|
|
BorderSide getBorderSide() {
|
|
final OutlinedBorder border =
|
|
tester
|
|
.widget<Material>(
|
|
find.descendant(of: outlinedButton, matching: find.byType(Material)),
|
|
)
|
|
.shape!
|
|
as OutlinedBorder;
|
|
return border.side;
|
|
}
|
|
|
|
// Pump a button with a null onPressed callback to make it disabled.
|
|
await tester.pumpWidget(buildFrame());
|
|
|
|
// Expect that the button is disabled and painted with the disabled border color.
|
|
expect(tester.widget<OutlinedButton>(outlinedButton).enabled, false);
|
|
expect(getBorderSide(), disabledBorderSide);
|
|
|
|
// Pump a new button with a no-op onPressed callback to make it enabled.
|
|
await tester.pumpWidget(buildFrame(onPressed: () {}));
|
|
|
|
// Wait for the border color to change from disabled to enabled.
|
|
await tester.pumpAndSettle();
|
|
expect(getBorderSide(), enabledBorderSide);
|
|
|
|
final Offset center = tester.getCenter(outlinedButton);
|
|
final TestGesture gesture = await tester.startGesture(center);
|
|
await tester.pump(); // start gesture
|
|
|
|
// Wait for the border's color to change to pressed
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
expect(getBorderSide(), pressedBorderSide);
|
|
|
|
// Tap gesture completes, button returns to its initial configuration.
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(getBorderSide(), enabledBorderSide);
|
|
});
|
|
|
|
testWidgets('OutlinedButton has no clip by default', (WidgetTester tester) async {
|
|
final GlobalKey buttonKey = GlobalKey();
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: OutlinedButton(key: buttonKey, onPressed: () {}, child: const Text('ABC')),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.renderObject(find.byKey(buttonKey)), paintsExactlyCountTimes(#clipPath, 0));
|
|
});
|
|
|
|
testWidgets('OutlinedButton contributes semantics', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = SemanticsTester(tester);
|
|
await tester.pumpWidget(
|
|
Theme(
|
|
data: ThemeData(useMaterial3: false),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: OutlinedButton(
|
|
style: const ButtonStyle(
|
|
// Specifying minimumSize to mimic the original minimumSize for
|
|
// RaisedButton so that the corresponding button size matches
|
|
// the original version of this test.
|
|
minimumSize: MaterialStatePropertyAll<Size>(Size(88, 36)),
|
|
),
|
|
onPressed: () {},
|
|
child: const Text('ABC'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics.rootChild(
|
|
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
|
|
label: 'ABC',
|
|
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
|
|
transform: Matrix4.translationValues(356.0, 276.0, 0.0),
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.hasEnabledState,
|
|
SemanticsFlag.isButton,
|
|
SemanticsFlag.isEnabled,
|
|
SemanticsFlag.isFocusable,
|
|
],
|
|
),
|
|
],
|
|
),
|
|
ignoreId: true,
|
|
),
|
|
);
|
|
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('When an OutlinedButton gains an icon, preserves the same SemanticsNode id', (
|
|
WidgetTester tester,
|
|
) async {
|
|
bool toggled = false;
|
|
|
|
const Key key = Key('button');
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return Row(
|
|
children: <Widget>[
|
|
OutlinedButton.icon(
|
|
key: key,
|
|
onPressed: () {
|
|
setState(() {
|
|
toggled = true;
|
|
});
|
|
},
|
|
icon: toggled ? const Icon(Icons.favorite) : null,
|
|
label: const Text('Button'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Initially, no icons are present.
|
|
expect(find.byIcon(Icons.favorite), findsNothing);
|
|
|
|
// Find the original OutlinedButton with no icon and get it's SemanticsNode
|
|
final Finder outlinedButton = find.bySemanticsLabel('Button');
|
|
expect(outlinedButton, findsOneWidget);
|
|
|
|
final SemanticsNode origSemanticsNode = tester.getSemantics(outlinedButton);
|
|
|
|
// Tap the button. It should receive an icon now.
|
|
await tester.tap(outlinedButton);
|
|
await tester.pump();
|
|
|
|
// Now one icon should be present.
|
|
expect(find.byIcon(Icons.favorite), findsOneWidget);
|
|
|
|
// Check if the semantics has change
|
|
final SemanticsNode semanticsNodeWithIcon = tester.getSemantics(outlinedButton);
|
|
|
|
expect(semanticsNodeWithIcon, origSemanticsNode);
|
|
});
|
|
|
|
testWidgets('OutlinedButton scales textScaleFactor', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
Theme(
|
|
data: ThemeData(useMaterial3: false),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: Center(
|
|
child: OutlinedButton(
|
|
style: const ButtonStyle(
|
|
// Specifying minimumSize to mimic the original minimumSize for
|
|
// RaisedButton so that the corresponding button size matches
|
|
// the original version of this test.
|
|
minimumSize: MaterialStatePropertyAll<Size>(Size(88, 36)),
|
|
),
|
|
onPressed: () {},
|
|
child: const Text('ABC'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.getSize(find.byType(OutlinedButton)), equals(const Size(88.0, 48.0)));
|
|
expect(tester.getSize(find.byType(Text)), equals(const Size(42.0, 14.0)));
|
|
|
|
// textScaleFactor expands text, but not button.
|
|
await tester.pumpWidget(
|
|
Theme(
|
|
// Force Material 2 typography.
|
|
data: ThemeData(useMaterial3: false),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery.withClampedTextScaling(
|
|
minScaleFactor: 1.25,
|
|
maxScaleFactor: 1.25,
|
|
child: Center(
|
|
child: OutlinedButton(
|
|
style: const ButtonStyle(
|
|
// Specifying minimumSize to mimic the original minimumSize for
|
|
// RaisedButton so that the corresponding button size matches
|
|
// the original version of this test.
|
|
minimumSize: MaterialStatePropertyAll<Size>(Size(88, 36)),
|
|
),
|
|
onPressed: () {},
|
|
child: const Text('ABC'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.getSize(find.byType(OutlinedButton)), equals(const Size(88.0, 48.0)));
|
|
expect(tester.getSize(find.byType(Text)), const Size(52.5, 18.0));
|
|
|
|
// Set text scale large enough to expand text and button.
|
|
await tester.pumpWidget(
|
|
Theme(
|
|
data: ThemeData(useMaterial3: false),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery.withClampedTextScaling(
|
|
minScaleFactor: 3.0,
|
|
maxScaleFactor: 3.0,
|
|
child: Center(
|
|
child: OutlinedButton(onPressed: () {}, child: const Text('ABC')),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.getSize(find.byType(OutlinedButton)), const Size(134.0, 48.0));
|
|
expect(tester.getSize(find.byType(Text)), const Size(126.0, 42.0));
|
|
});
|
|
|
|
testWidgets('OutlinedButton onPressed and onLongPress callbacks are distinctly recognized', (
|
|
WidgetTester tester,
|
|
) async {
|
|
bool didPressButton = false;
|
|
bool didLongPressButton = false;
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: OutlinedButton(
|
|
onPressed: () {
|
|
didPressButton = true;
|
|
},
|
|
onLongPress: () {
|
|
didLongPressButton = true;
|
|
},
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder outlinedButton = find.byType(OutlinedButton);
|
|
expect(tester.widget<OutlinedButton>(outlinedButton).enabled, true);
|
|
|
|
expect(didPressButton, isFalse);
|
|
await tester.tap(outlinedButton);
|
|
expect(didPressButton, isTrue);
|
|
|
|
expect(didLongPressButton, isFalse);
|
|
await tester.longPress(outlinedButton);
|
|
expect(didLongPressButton, isTrue);
|
|
});
|
|
|
|
testWidgets('OutlinedButton responds to density changes.', (WidgetTester tester) async {
|
|
const Key key = Key('test');
|
|
const Key childKey = Key('test child');
|
|
|
|
Future<void> buildTest(VisualDensity visualDensity, {bool useText = false}) async {
|
|
return tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(useMaterial3: false),
|
|
home: Directionality(
|
|
textDirection: TextDirection.rtl,
|
|
child: Center(
|
|
child: OutlinedButton(
|
|
style: ButtonStyle(
|
|
visualDensity: visualDensity,
|
|
minimumSize: ButtonStyleButton.allOrNull(const Size(64, 36)),
|
|
),
|
|
key: key,
|
|
onPressed: () {},
|
|
child: useText
|
|
? const Text('Text', key: childKey)
|
|
: Container(
|
|
key: childKey,
|
|
width: 100,
|
|
height: 100,
|
|
color: const Color(0xffff0000),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await buildTest(VisualDensity.standard);
|
|
final RenderBox box = tester.renderObject(find.byKey(key));
|
|
Rect childRect = tester.getRect(find.byKey(childKey));
|
|
await tester.pumpAndSettle();
|
|
expect(box.size, equals(const Size(132, 100)));
|
|
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
|
|
|
|
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0));
|
|
await tester.pumpAndSettle();
|
|
childRect = tester.getRect(find.byKey(childKey));
|
|
expect(box.size, equals(const Size(156, 124)));
|
|
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
|
|
|
|
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0));
|
|
await tester.pumpAndSettle();
|
|
childRect = tester.getRect(find.byKey(childKey));
|
|
expect(box.size, equals(const Size(132, 100)));
|
|
expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
|
|
|
|
await buildTest(VisualDensity.standard, useText: true);
|
|
await tester.pumpAndSettle();
|
|
childRect = tester.getRect(find.byKey(childKey));
|
|
expect(box.size, equals(const Size(88, 48)));
|
|
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
|
|
|
|
await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0), useText: true);
|
|
await tester.pumpAndSettle();
|
|
childRect = tester.getRect(find.byKey(childKey));
|
|
expect(box.size, equals(const Size(112, 60)));
|
|
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
|
|
|
|
await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0), useText: true);
|
|
await tester.pumpAndSettle();
|
|
childRect = tester.getRect(find.byKey(childKey));
|
|
expect(box.size, equals(const Size(88, 36)));
|
|
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
|
|
});
|
|
|
|
group('Default OutlinedButton padding for textScaleFactor, textDirection', () {
|
|
const ValueKey<String> buttonKey = ValueKey<String>('button');
|
|
const ValueKey<String> labelKey = ValueKey<String>('label');
|
|
const ValueKey<String> iconKey = ValueKey<String>('icon');
|
|
|
|
const List<double> textScaleFactorOptions = <double>[0.5, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0];
|
|
const List<TextDirection> textDirectionOptions = <TextDirection>[
|
|
TextDirection.ltr,
|
|
TextDirection.rtl,
|
|
];
|
|
const List<Widget?> iconOptions = <Widget?>[null, Icon(Icons.add, size: 18, key: iconKey)];
|
|
|
|
// Expected values for each textScaleFactor.
|
|
final Map<double, double> paddingVertical = <double, double>{
|
|
0.5: 0,
|
|
1: 0,
|
|
1.25: 0,
|
|
1.5: 0,
|
|
2: 0,
|
|
2.5: 0,
|
|
3: 0,
|
|
4: 0,
|
|
};
|
|
final Map<double, double> paddingWithIconGap = <double, double>{
|
|
0.5: 8,
|
|
1: 8,
|
|
1.25: 7,
|
|
1.5: 6,
|
|
2: 4,
|
|
2.5: 4,
|
|
3: 4,
|
|
4: 4,
|
|
};
|
|
final Map<double, double> paddingHorizontal = <double, double>{
|
|
0.5: 16,
|
|
1: 16,
|
|
1.25: 14,
|
|
1.5: 12,
|
|
2: 8,
|
|
2.5: 6,
|
|
3: 4,
|
|
4: 4,
|
|
};
|
|
|
|
Rect globalBounds(RenderBox renderBox) {
|
|
final Offset topLeft = renderBox.localToGlobal(Offset.zero);
|
|
return topLeft & renderBox.size;
|
|
}
|
|
|
|
/// Computes the padding between two [Rect]s, one inside the other.
|
|
EdgeInsets paddingBetween({required Rect parent, required Rect child}) {
|
|
assert(parent.intersect(child) == child);
|
|
return EdgeInsets.fromLTRB(
|
|
child.left - parent.left,
|
|
child.top - parent.top,
|
|
parent.right - child.right,
|
|
parent.bottom - child.bottom,
|
|
);
|
|
}
|
|
|
|
for (final double textScaleFactor in textScaleFactorOptions) {
|
|
for (final TextDirection textDirection in textDirectionOptions) {
|
|
for (final Widget? icon in iconOptions) {
|
|
final String testName = <String>[
|
|
'OutlinedButton, text scale $textScaleFactor',
|
|
if (icon != null) 'with icon',
|
|
if (textDirection == TextDirection.rtl) 'RTL',
|
|
].join(', ');
|
|
testWidgets(testName, (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(
|
|
useMaterial3: false,
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: OutlinedButton.styleFrom(minimumSize: const Size(64, 36)),
|
|
),
|
|
),
|
|
home: Builder(
|
|
builder: (BuildContext context) {
|
|
return MediaQuery.withClampedTextScaling(
|
|
minScaleFactor: textScaleFactor,
|
|
maxScaleFactor: textScaleFactor,
|
|
child: Directionality(
|
|
textDirection: textDirection,
|
|
child: Scaffold(
|
|
body: Center(
|
|
child: icon == null
|
|
? OutlinedButton(
|
|
key: buttonKey,
|
|
onPressed: () {},
|
|
child: const Text('button', key: labelKey),
|
|
)
|
|
: OutlinedButton.icon(
|
|
key: buttonKey,
|
|
onPressed: () {},
|
|
icon: icon,
|
|
label: const Text('button', key: labelKey),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
final Element paddingElement = tester.element(
|
|
find.descendant(of: find.byKey(buttonKey), matching: find.byType(Padding)),
|
|
);
|
|
expect(Directionality.of(paddingElement), textDirection);
|
|
final Padding paddingWidget = paddingElement.widget as Padding;
|
|
|
|
// Compute expected padding, and check.
|
|
|
|
final double expectedPaddingTop = paddingVertical[textScaleFactor]!;
|
|
final double expectedPaddingBottom = paddingVertical[textScaleFactor]!;
|
|
final double expectedPaddingStart = paddingHorizontal[textScaleFactor]!;
|
|
final double expectedPaddingEnd = expectedPaddingStart;
|
|
|
|
final EdgeInsets expectedPadding = EdgeInsetsDirectional.fromSTEB(
|
|
expectedPaddingStart,
|
|
expectedPaddingTop,
|
|
expectedPaddingEnd,
|
|
expectedPaddingBottom,
|
|
).resolve(textDirection);
|
|
|
|
expect(paddingWidget.padding.resolve(textDirection), expectedPadding);
|
|
|
|
// Measure padding in terms of the difference between the button and its label child
|
|
// and check that.
|
|
|
|
final RenderBox labelRenderBox = tester.renderObject<RenderBox>(find.byKey(labelKey));
|
|
final Rect labelBounds = globalBounds(labelRenderBox);
|
|
final RenderBox? iconRenderBox = icon == null
|
|
? null
|
|
: tester.renderObject<RenderBox>(find.byKey(iconKey));
|
|
final Rect? iconBounds = icon == null ? null : globalBounds(iconRenderBox!);
|
|
final Rect childBounds = icon == null
|
|
? labelBounds
|
|
: labelBounds.expandToInclude(iconBounds!);
|
|
|
|
// We measure the `InkResponse` descendant of the button
|
|
// element, because the button has a larger `RenderBox`
|
|
// which accommodates the minimum tap target with a height
|
|
// of 48.
|
|
final RenderBox buttonRenderBox = tester.renderObject<RenderBox>(
|
|
find.descendant(
|
|
of: find.byKey(buttonKey),
|
|
matching: find.byWidgetPredicate((Widget widget) => widget is InkResponse),
|
|
),
|
|
);
|
|
final Rect buttonBounds = globalBounds(buttonRenderBox);
|
|
final EdgeInsets visuallyMeasuredPadding = paddingBetween(
|
|
parent: buttonBounds,
|
|
child: childBounds,
|
|
);
|
|
|
|
// Since there is a requirement of a minimum width of 64
|
|
// and a minimum height of 36 on material buttons, the visual
|
|
// padding of smaller buttons may not match their settings.
|
|
// Therefore, we only test buttons that are large enough.
|
|
if (buttonBounds.width > 64) {
|
|
expect(visuallyMeasuredPadding.left, expectedPadding.left);
|
|
expect(visuallyMeasuredPadding.right, expectedPadding.right);
|
|
}
|
|
|
|
if (buttonBounds.height > 36) {
|
|
expect(visuallyMeasuredPadding.top, expectedPadding.top);
|
|
expect(visuallyMeasuredPadding.bottom, expectedPadding.bottom);
|
|
}
|
|
|
|
// Check the gap between the icon and the label
|
|
if (icon != null) {
|
|
final double gapWidth = textDirection == TextDirection.ltr
|
|
? labelBounds.left - iconBounds!.right
|
|
: iconBounds!.left - labelBounds.right;
|
|
expect(gapWidth, paddingWithIconGap[textScaleFactor]);
|
|
}
|
|
|
|
// Check the text's height - should be consistent with the textScaleFactor.
|
|
final RenderBox textRenderObject = tester.renderObject<RenderBox>(
|
|
find.descendant(
|
|
of: find.byKey(labelKey),
|
|
matching: find.byElementPredicate((Element element) => element.widget is RichText),
|
|
),
|
|
);
|
|
final double textHeight = textRenderObject.paintBounds.size.height;
|
|
final double expectedTextHeight = 14 * textScaleFactor;
|
|
expect(textHeight, moreOrLessEquals(expectedTextHeight, epsilon: 0.5));
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
testWidgets('Override OutlinedButton default padding', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(useMaterial3: false),
|
|
home: Builder(
|
|
builder: (BuildContext context) {
|
|
return MediaQuery.withClampedTextScaling(
|
|
minScaleFactor: 2,
|
|
maxScaleFactor: 2,
|
|
child: Scaffold(
|
|
body: Center(
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(padding: const EdgeInsets.all(22)),
|
|
onPressed: () {},
|
|
child: const Text('OutlinedButton'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
final Padding paddingWidget = tester.widget<Padding>(
|
|
find.descendant(of: find.byType(OutlinedButton), matching: find.byType(Padding)),
|
|
);
|
|
expect(paddingWidget.padding, const EdgeInsets.all(22));
|
|
});
|
|
|
|
testWidgets('Override theme fontSize changes padding', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData.from(
|
|
colorScheme: const ColorScheme.light(),
|
|
textTheme: const TextTheme(labelLarge: TextStyle(fontSize: 28.0)),
|
|
),
|
|
home: Builder(
|
|
builder: (BuildContext context) {
|
|
return Scaffold(
|
|
body: Center(
|
|
child: OutlinedButton(onPressed: () {}, child: const Text('text')),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
final Padding paddingWidget = tester.widget<Padding>(
|
|
find.descendant(of: find.byType(OutlinedButton), matching: find.byType(Padding)),
|
|
);
|
|
expect(paddingWidget.padding, const EdgeInsets.symmetric(horizontal: 12));
|
|
});
|
|
|
|
testWidgets('M3 OutlinedButton has correct padding', (WidgetTester tester) async {
|
|
final Key key = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: OutlinedButton(key: key, onPressed: () {}, child: const Text('OutlinedButton')),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Padding paddingWidget = tester.widget<Padding>(
|
|
find.descendant(of: find.byKey(key), matching: find.byType(Padding)),
|
|
);
|
|
expect(paddingWidget.padding, const EdgeInsets.symmetric(horizontal: 24));
|
|
});
|
|
|
|
testWidgets('M3 OutlinedButton.icon has correct padding', (WidgetTester tester) async {
|
|
final Key key = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: OutlinedButton.icon(
|
|
key: key,
|
|
icon: const Icon(Icons.favorite),
|
|
onPressed: () {},
|
|
label: const Text('OutlinedButton'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Padding paddingWidget = tester.widget<Padding>(
|
|
find.descendant(of: find.byKey(key), matching: find.byType(Padding)),
|
|
);
|
|
expect(paddingWidget.padding, const EdgeInsetsDirectional.fromSTEB(16.0, 0.0, 24.0, 0.0));
|
|
});
|
|
|
|
testWidgets('Fixed size OutlinedButtons', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
OutlinedButton(
|
|
style: OutlinedButton.styleFrom(fixedSize: const Size(100, 100)),
|
|
onPressed: () {},
|
|
child: const Text('100x100'),
|
|
),
|
|
OutlinedButton(
|
|
style: OutlinedButton.styleFrom(fixedSize: const Size.fromWidth(200)),
|
|
onPressed: () {},
|
|
child: const Text('200xh'),
|
|
),
|
|
OutlinedButton(
|
|
style: OutlinedButton.styleFrom(fixedSize: const Size.fromHeight(200)),
|
|
onPressed: () {},
|
|
child: const Text('wx200'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.getSize(find.widgetWithText(OutlinedButton, '100x100')), const Size(100, 100));
|
|
expect(tester.getSize(find.widgetWithText(OutlinedButton, '200xh')).width, 200);
|
|
expect(tester.getSize(find.widgetWithText(OutlinedButton, 'wx200')).height, 200);
|
|
});
|
|
|
|
testWidgets('OutlinedButton with NoSplash splashFactory paints nothing', (
|
|
WidgetTester tester,
|
|
) async {
|
|
Widget buildFrame({InteractiveInkFeatureFactory? splashFactory}) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(splashFactory: splashFactory),
|
|
onPressed: () {},
|
|
child: const Text('test'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// NoSplash.splashFactory, no splash circles drawn
|
|
await tester.pumpWidget(buildFrame(splashFactory: NoSplash.splashFactory));
|
|
{
|
|
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('test')));
|
|
final MaterialInkController material = Material.of(tester.element(find.text('test')));
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
expect(material, paintsExactlyCountTimes(#drawCircle, 0));
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
// InkRipple.splashFactory, one splash circle drawn.
|
|
await tester.pumpWidget(buildFrame(splashFactory: InkRipple.splashFactory));
|
|
{
|
|
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('test')));
|
|
final MaterialInkController material = Material.of(tester.element(find.text('test')));
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
expect(material, paintsExactlyCountTimes(#drawCircle, 1));
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
}
|
|
});
|
|
|
|
testWidgets(
|
|
'OutlinedButton uses InkSparkle only for Android non-web when useMaterial3 is true',
|
|
(WidgetTester tester) async {
|
|
final ThemeData theme = ThemeData();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Center(
|
|
child: OutlinedButton(onPressed: () {}, child: const Text('button')),
|
|
),
|
|
),
|
|
);
|
|
|
|
final InkWell buttonInkWell = tester.widget<InkWell>(
|
|
find.descendant(of: find.byType(OutlinedButton), matching: find.byType(InkWell)),
|
|
);
|
|
|
|
if (debugDefaultTargetPlatformOverride! == TargetPlatform.android && !kIsWeb) {
|
|
expect(buttonInkWell.splashFactory, equals(InkSparkle.splashFactory));
|
|
} else {
|
|
expect(buttonInkWell.splashFactory, equals(InkRipple.splashFactory));
|
|
}
|
|
},
|
|
variant: TargetPlatformVariant.all(),
|
|
);
|
|
|
|
testWidgets('OutlinedButton uses InkRipple when useMaterial3 is false', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final ThemeData theme = ThemeData(useMaterial3: false);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: theme,
|
|
home: Center(
|
|
child: OutlinedButton(onPressed: () {}, child: const Text('button')),
|
|
),
|
|
),
|
|
);
|
|
|
|
final InkWell buttonInkWell = tester.widget<InkWell>(
|
|
find.descendant(of: find.byType(OutlinedButton), matching: find.byType(InkWell)),
|
|
);
|
|
expect(buttonInkWell.splashFactory, equals(InkRipple.splashFactory));
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('OutlinedButton.icon does not overflow', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/77815
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: SizedBox(
|
|
width: 200,
|
|
child: OutlinedButton.icon(
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.add),
|
|
label: const Text(
|
|
// Much wider than 200
|
|
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut a euismod nibh. Morbi laoreet purus.',
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(tester.takeException(), null);
|
|
});
|
|
|
|
testWidgets('OutlinedButton.icon icon,label layout', (WidgetTester tester) async {
|
|
final Key buttonKey = UniqueKey();
|
|
final Key iconKey = UniqueKey();
|
|
final Key labelKey = UniqueKey();
|
|
final ButtonStyle style = OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.zero,
|
|
visualDensity: VisualDensity.standard, // dx=0, dy=0
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: SizedBox(
|
|
width: 200,
|
|
child: OutlinedButton.icon(
|
|
key: buttonKey,
|
|
style: style,
|
|
onPressed: () {},
|
|
icon: SizedBox(key: iconKey, width: 50, height: 100),
|
|
label: SizedBox(key: labelKey, width: 50, height: 100),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// The button's label and icon are separated by a gap of 8:
|
|
// 46 [icon 50] 8 [label 50] 46
|
|
// The overall button width is 200. So:
|
|
// icon.x = 46
|
|
// label.x = 46 + 50 + 8 = 104
|
|
|
|
expect(tester.getRect(find.byKey(buttonKey)), const Rect.fromLTRB(0.0, 0.0, 200.0, 100.0));
|
|
expect(tester.getRect(find.byKey(iconKey)), const Rect.fromLTRB(46.0, 0.0, 96.0, 100.0));
|
|
expect(tester.getRect(find.byKey(labelKey)), const Rect.fromLTRB(104.0, 0.0, 154.0, 100.0));
|
|
});
|
|
|
|
testWidgets('OutlinedButton maximumSize', (WidgetTester tester) async {
|
|
final Key key0 = UniqueKey();
|
|
final Key key1 = UniqueKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(useMaterial3: false),
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
OutlinedButton(
|
|
key: key0,
|
|
style: OutlinedButton.styleFrom(
|
|
minimumSize: const Size(24, 36),
|
|
maximumSize: const Size.fromWidth(64),
|
|
),
|
|
onPressed: () {},
|
|
child: const Text('A B C D E F G H I J K L M N O P'),
|
|
),
|
|
OutlinedButton.icon(
|
|
key: key1,
|
|
style: OutlinedButton.styleFrom(
|
|
minimumSize: const Size(24, 36),
|
|
maximumSize: const Size.fromWidth(104),
|
|
),
|
|
onPressed: () {},
|
|
icon: Container(color: Colors.red, width: 32, height: 32),
|
|
label: const Text('A B C D E F G H I J K L M N O P'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.getSize(find.byKey(key0)), const Size(64.0, 224.0));
|
|
expect(tester.getSize(find.byKey(key1)), const Size(104.0, 224.0));
|
|
});
|
|
|
|
testWidgets('Fixed size OutlinedButton, same as minimumSize == maximumSize', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
OutlinedButton(
|
|
style: OutlinedButton.styleFrom(fixedSize: const Size(200, 200)),
|
|
onPressed: () {},
|
|
child: const Text('200x200'),
|
|
),
|
|
OutlinedButton(
|
|
style: OutlinedButton.styleFrom(
|
|
minimumSize: const Size(200, 200),
|
|
maximumSize: const Size(200, 200),
|
|
),
|
|
onPressed: () {},
|
|
child: const Text('200,200'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.getSize(find.widgetWithText(OutlinedButton, '200x200')), const Size(200, 200));
|
|
expect(tester.getSize(find.widgetWithText(OutlinedButton, '200,200')), const Size(200, 200));
|
|
});
|
|
|
|
testWidgets('OutlinedButton changes mouse cursor when hovered', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.forbidden,
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(
|
|
enabledMouseCursor: SystemMouseCursors.text,
|
|
disabledMouseCursor: SystemMouseCursors.grab,
|
|
),
|
|
onPressed: () {},
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestGesture gesture = await tester.createGesture(
|
|
kind: PointerDeviceKind.mouse,
|
|
pointer: 1,
|
|
);
|
|
await gesture.addPointer(location: Offset.zero);
|
|
|
|
await tester.pump();
|
|
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.text,
|
|
);
|
|
|
|
// Test cursor when disabled
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.forbidden,
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(
|
|
enabledMouseCursor: SystemMouseCursors.text,
|
|
disabledMouseCursor: SystemMouseCursors.grab,
|
|
),
|
|
onPressed: null,
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.grab,
|
|
);
|
|
|
|
// Test default cursor
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.forbidden,
|
|
child: OutlinedButton(onPressed: () {}, child: const Text('button')),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.click,
|
|
);
|
|
|
|
// Test default cursor when disabled
|
|
await tester.pumpWidget(
|
|
const Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.forbidden,
|
|
child: OutlinedButton(onPressed: null, child: Text('button')),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
|
SystemMouseCursors.basic,
|
|
);
|
|
});
|
|
|
|
testWidgets('OutlinedButton in SelectionArea changes mouse cursor when hovered', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/104595.
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: SelectionArea(
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(
|
|
enabledMouseCursor: SystemMouseCursors.click,
|
|
disabledMouseCursor: SystemMouseCursors.grab,
|
|
),
|
|
onPressed: () {},
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
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('OutlinedButton.styleFrom can be used to set foreground and background colors', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Colors.white,
|
|
backgroundColor: Colors.purple,
|
|
),
|
|
onPressed: () {},
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Material material = tester.widget<Material>(
|
|
find.descendant(of: find.byType(OutlinedButton), matching: find.byType(Material)),
|
|
);
|
|
expect(material.color, Colors.purple);
|
|
expect(material.textStyle!.color, Colors.white);
|
|
});
|
|
|
|
Future<void> testStatesController(Widget? icon, WidgetTester tester) async {
|
|
int count = 0;
|
|
void valueChanged() {
|
|
count += 1;
|
|
}
|
|
|
|
final MaterialStatesController controller = MaterialStatesController();
|
|
addTearDown(controller.dispose);
|
|
controller.addListener(valueChanged);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Center(
|
|
child: icon == null
|
|
? OutlinedButton(
|
|
statesController: controller,
|
|
onPressed: () {},
|
|
child: const Text('button'),
|
|
)
|
|
: OutlinedButton.icon(
|
|
statesController: controller,
|
|
onPressed: () {},
|
|
icon: icon,
|
|
label: const Text('button'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(controller.value, <WidgetState>{});
|
|
expect(count, 0);
|
|
|
|
final Offset center = tester.getCenter(find.byType(Text));
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
await gesture.moveTo(center);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(controller.value, <WidgetState>{WidgetState.hovered});
|
|
expect(count, 1);
|
|
|
|
await gesture.moveTo(Offset.zero);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(controller.value, <WidgetState>{});
|
|
expect(count, 2);
|
|
|
|
await gesture.moveTo(center);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(controller.value, <WidgetState>{WidgetState.hovered});
|
|
expect(count, 3);
|
|
|
|
await gesture.down(center);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(controller.value, <WidgetState>{WidgetState.hovered, WidgetState.pressed});
|
|
expect(count, 4);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(controller.value, <WidgetState>{WidgetState.hovered});
|
|
expect(count, 5);
|
|
|
|
await gesture.moveTo(Offset.zero);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(controller.value, <WidgetState>{});
|
|
expect(count, 6);
|
|
|
|
await gesture.down(center);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.value, <WidgetState>{WidgetState.hovered, WidgetState.pressed});
|
|
expect(count, 8); // adds hovered and pressed - two changes
|
|
|
|
// If the button is rebuilt disabled, then the pressed state is
|
|
// removed.
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Center(
|
|
child: icon == null
|
|
? OutlinedButton(
|
|
statesController: controller,
|
|
onPressed: null,
|
|
child: const Text('button'),
|
|
)
|
|
: OutlinedButton.icon(
|
|
statesController: controller,
|
|
onPressed: null,
|
|
icon: icon,
|
|
label: const Text('button'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.value, <WidgetState>{WidgetState.hovered, WidgetState.disabled});
|
|
expect(count, 10); // removes pressed and adds disabled - two changes
|
|
await gesture.moveTo(Offset.zero);
|
|
await tester.pumpAndSettle();
|
|
expect(controller.value, <WidgetState>{WidgetState.disabled});
|
|
expect(count, 11);
|
|
await gesture.removePointer();
|
|
}
|
|
|
|
testWidgets('OutlinedButton statesController', (WidgetTester tester) async {
|
|
testStatesController(null, tester);
|
|
});
|
|
|
|
testWidgets('OutlinedButton.icon statesController', (WidgetTester tester) async {
|
|
testStatesController(const Icon(Icons.add), tester);
|
|
});
|
|
|
|
testWidgets('Disabled OutlinedButton statesController', (WidgetTester tester) async {
|
|
int count = 0;
|
|
void valueChanged() {
|
|
count += 1;
|
|
}
|
|
|
|
final MaterialStatesController controller = MaterialStatesController();
|
|
addTearDown(controller.dispose);
|
|
controller.addListener(valueChanged);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Center(
|
|
child: OutlinedButton(
|
|
statesController: controller,
|
|
onPressed: null,
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(controller.value, <WidgetState>{WidgetState.disabled});
|
|
expect(count, 1);
|
|
});
|
|
|
|
testWidgets("OutlinedButton.styleFrom doesn't throw exception on passing only one cursor", (
|
|
WidgetTester tester,
|
|
) async {
|
|
// This is a regression test for https://github.com/flutter/flutter/issues/118071.
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(enabledMouseCursor: SystemMouseCursors.text),
|
|
onPressed: () {},
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('OutlinedButton backgroundBuilder and foregroundBuilder', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const Color backgroundColor = Color(0xFF000011);
|
|
const Color foregroundColor = Color(0xFF000022);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(
|
|
backgroundBuilder: (BuildContext context, Set<WidgetState> states, Widget? child) {
|
|
return DecoratedBox(
|
|
decoration: const BoxDecoration(color: backgroundColor),
|
|
child: child,
|
|
);
|
|
},
|
|
foregroundBuilder: (BuildContext context, Set<WidgetState> states, Widget? child) {
|
|
return DecoratedBox(
|
|
decoration: const BoxDecoration(color: foregroundColor),
|
|
child: child,
|
|
);
|
|
},
|
|
),
|
|
onPressed: () {},
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
);
|
|
|
|
BoxDecoration boxDecorationOf(Finder finder) {
|
|
return tester.widget<DecoratedBox>(finder).decoration as BoxDecoration;
|
|
}
|
|
|
|
final Finder decorations = find.descendant(
|
|
of: find.byType(OutlinedButton),
|
|
matching: find.byType(DecoratedBox),
|
|
);
|
|
|
|
expect(boxDecorationOf(decorations.at(0)).color, backgroundColor);
|
|
expect(boxDecorationOf(decorations.at(1)).color, foregroundColor);
|
|
|
|
Text textChildOf(Finder finder) {
|
|
return tester.widget<Text>(find.descendant(of: finder, matching: find.byType(Text)));
|
|
}
|
|
|
|
expect(textChildOf(decorations.at(0)).data, 'button');
|
|
expect(textChildOf(decorations.at(1)).data, 'button');
|
|
});
|
|
|
|
testWidgets(
|
|
'OutlinedButton backgroundBuilder drops button child and foregroundBuilder return value',
|
|
(WidgetTester tester) async {
|
|
const Color backgroundColor = Color(0xFF000011);
|
|
const Color foregroundColor = Color(0xFF000022);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(
|
|
backgroundBuilder: (BuildContext context, Set<WidgetState> states, Widget? child) {
|
|
return const DecoratedBox(decoration: BoxDecoration(color: backgroundColor));
|
|
},
|
|
foregroundBuilder: (BuildContext context, Set<WidgetState> states, Widget? child) {
|
|
return const DecoratedBox(decoration: BoxDecoration(color: foregroundColor));
|
|
},
|
|
),
|
|
onPressed: () {},
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder background = find.descendant(
|
|
of: find.byType(OutlinedButton),
|
|
matching: find.byType(DecoratedBox),
|
|
);
|
|
|
|
expect(background, findsOneWidget);
|
|
expect(find.text('button'), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets('OutlinedButton foregroundBuilder drops button child', (WidgetTester tester) async {
|
|
const Color foregroundColor = Color(0xFF000022);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundBuilder: (BuildContext context, Set<WidgetState> states, Widget? child) {
|
|
return const DecoratedBox(decoration: BoxDecoration(color: foregroundColor));
|
|
},
|
|
),
|
|
onPressed: () {},
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder foreground = find.descendant(
|
|
of: find.byType(OutlinedButton),
|
|
matching: find.byType(DecoratedBox),
|
|
);
|
|
|
|
expect(foreground, findsOneWidget);
|
|
expect(find.text('button'), findsNothing);
|
|
});
|
|
|
|
testWidgets(
|
|
'OutlinedButton foreground and background builders are applied to the correct states',
|
|
(WidgetTester tester) async {
|
|
Set<WidgetState> foregroundStates = <WidgetState>{};
|
|
Set<WidgetState> backgroundStates = <WidgetState>{};
|
|
final FocusNode focusNode = FocusNode();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: OutlinedButton(
|
|
style: ButtonStyle(
|
|
backgroundBuilder:
|
|
(BuildContext context, Set<WidgetState> states, Widget? child) {
|
|
backgroundStates = states;
|
|
return child!;
|
|
},
|
|
foregroundBuilder:
|
|
(BuildContext context, Set<WidgetState> states, Widget? child) {
|
|
foregroundStates = states;
|
|
return child!;
|
|
},
|
|
),
|
|
onPressed: () {},
|
|
focusNode: focusNode,
|
|
child: const Text('button'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Default.
|
|
expect(backgroundStates.isEmpty, isTrue);
|
|
expect(foregroundStates.isEmpty, isTrue);
|
|
|
|
const Set<WidgetState> focusedStates = <WidgetState>{WidgetState.focused};
|
|
const Set<WidgetState> focusedHoveredStates = <WidgetState>{
|
|
WidgetState.focused,
|
|
WidgetState.hovered,
|
|
};
|
|
const Set<WidgetState> focusedHoveredPressedStates = <WidgetState>{
|
|
WidgetState.focused,
|
|
WidgetState.hovered,
|
|
WidgetState.pressed,
|
|
};
|
|
|
|
bool sameStates(Set<WidgetState> expectedValue, Set<WidgetState> actualValue) {
|
|
return expectedValue.difference(actualValue).isEmpty &&
|
|
actualValue.difference(expectedValue).isEmpty;
|
|
}
|
|
|
|
// Focused.
|
|
focusNode.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
expect(sameStates(focusedStates, backgroundStates), isTrue);
|
|
expect(sameStates(focusedStates, foregroundStates), isTrue);
|
|
|
|
// Hovered.
|
|
final Offset center = tester.getCenter(find.byType(OutlinedButton));
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
await gesture.moveTo(center);
|
|
await tester.pumpAndSettle();
|
|
expect(sameStates(focusedHoveredStates, backgroundStates), isTrue);
|
|
expect(sameStates(focusedHoveredStates, foregroundStates), isTrue);
|
|
|
|
// Highlighted (pressed).
|
|
await gesture.down(center);
|
|
await tester.pump(); // Start the splash and highlight animations.
|
|
await tester.pump(
|
|
const Duration(milliseconds: 800),
|
|
); // Wait for splash and highlight to be well under way.
|
|
expect(sameStates(focusedHoveredPressedStates, backgroundStates), isTrue);
|
|
expect(sameStates(focusedHoveredPressedStates, foregroundStates), isTrue);
|
|
|
|
focusNode.dispose();
|
|
},
|
|
);
|
|
|
|
testWidgets('OutlinedButton styleFrom backgroundColor special case', (WidgetTester tester) async {
|
|
// Regression test for an internal Google issue: b/323399158
|
|
|
|
const Color backgroundColor = Color(0xFF000022);
|
|
|
|
Widget buildFrame({VoidCallback? onPressed}) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(backgroundColor: backgroundColor),
|
|
onPressed: () {},
|
|
child: const Text('button'),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame(onPressed: () {})); // enabled
|
|
final Material material = tester.widget<Material>(
|
|
find.descendant(of: find.byType(OutlinedButton), matching: find.byType(Material)),
|
|
);
|
|
expect(material.color, backgroundColor);
|
|
|
|
await tester.pumpWidget(buildFrame()); // onPressed: null - disabled
|
|
expect(material.color, backgroundColor);
|
|
});
|
|
|
|
testWidgets('Default OutlinedButton icon alignment', (WidgetTester tester) async {
|
|
Widget buildWidget({required TextDirection textDirection}) {
|
|
return MaterialApp(
|
|
home: Directionality(
|
|
textDirection: textDirection,
|
|
child: Center(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('button'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Test default iconAlignment when textDirection is ltr.
|
|
await tester.pumpWidget(buildWidget(textDirection: TextDirection.ltr));
|
|
|
|
final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
|
|
final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
|
|
|
|
// The icon is aligned to the left of the button.
|
|
expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge.
|
|
|
|
// Test default iconAlignment when textDirection is rtl.
|
|
await tester.pumpWidget(buildWidget(textDirection: TextDirection.rtl));
|
|
|
|
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
|
|
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
|
|
|
|
// The icon is aligned to the right of the button.
|
|
expect(
|
|
buttonTopRight.dx,
|
|
iconTopRight.dx + 16.0,
|
|
); // 16.0 - padding between icon and button edge.
|
|
});
|
|
|
|
testWidgets('OutlinedButton icon alignment can be customized', (WidgetTester tester) async {
|
|
Widget buildWidget({
|
|
required TextDirection textDirection,
|
|
required IconAlignment iconAlignment,
|
|
}) {
|
|
return MaterialApp(
|
|
home: Directionality(
|
|
textDirection: textDirection,
|
|
child: Center(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('button'),
|
|
iconAlignment: iconAlignment,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Test iconAlignment when textDirection is ltr.
|
|
await tester.pumpWidget(
|
|
buildWidget(textDirection: TextDirection.ltr, iconAlignment: IconAlignment.start),
|
|
);
|
|
|
|
Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
|
|
Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
|
|
|
|
// The icon is aligned to the left of the button.
|
|
expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge.
|
|
|
|
// Test iconAlignment when textDirection is ltr.
|
|
await tester.pumpWidget(
|
|
buildWidget(textDirection: TextDirection.ltr, iconAlignment: IconAlignment.end),
|
|
);
|
|
|
|
Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
|
|
Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
|
|
|
|
// The icon is aligned to the right of the button.
|
|
expect(
|
|
buttonTopRight.dx,
|
|
iconTopRight.dx + 24.0,
|
|
); // 24.0 - padding between icon and button edge.
|
|
|
|
// Test iconAlignment when textDirection is rtl.
|
|
await tester.pumpWidget(
|
|
buildWidget(textDirection: TextDirection.rtl, iconAlignment: IconAlignment.start),
|
|
);
|
|
|
|
buttonTopRight = tester.getTopRight(find.byType(Material).last);
|
|
iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
|
|
|
|
// The icon is aligned to the right of the button.
|
|
expect(
|
|
buttonTopRight.dx,
|
|
iconTopRight.dx + 16.0,
|
|
); // 16.0 - padding between icon and button edge.
|
|
|
|
// Test iconAlignment when textDirection is rtl.
|
|
await tester.pumpWidget(
|
|
buildWidget(textDirection: TextDirection.rtl, iconAlignment: IconAlignment.end),
|
|
);
|
|
|
|
buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
|
|
iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
|
|
|
|
// The icon is aligned to the left of the button.
|
|
expect(buttonTopLeft.dx, iconTopLeft.dx - 24.0); // 24.0 - padding between icon and button edge.
|
|
});
|
|
|
|
testWidgets('OutlinedButton icon alignment respects ButtonStyle.iconAlignment', (
|
|
WidgetTester tester,
|
|
) async {
|
|
Widget buildButton({IconAlignment? iconAlignment}) {
|
|
return MaterialApp(
|
|
home: Center(
|
|
child: OutlinedButton.icon(
|
|
style: ButtonStyle(iconAlignment: iconAlignment),
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('button'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildButton());
|
|
|
|
final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last);
|
|
final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add));
|
|
|
|
expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0);
|
|
|
|
await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end));
|
|
|
|
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
|
|
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
|
|
|
|
expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
|
|
});
|
|
|
|
testWidgets("OutlinedButton.icon response doesn't hover when disabled", (
|
|
WidgetTester tester,
|
|
) async {
|
|
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
|
|
final FocusNode focusNode = FocusNode(debugLabel: 'OutlinedButton.icon Focus');
|
|
final GlobalKey childKey = GlobalKey();
|
|
bool hovering = false;
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: SizedBox(
|
|
width: 100,
|
|
height: 100,
|
|
child: OutlinedButton.icon(
|
|
autofocus: true,
|
|
onPressed: () {},
|
|
onLongPress: () {},
|
|
onHover: (bool value) {
|
|
hovering = value;
|
|
},
|
|
focusNode: focusNode,
|
|
label: SizedBox(key: childKey),
|
|
icon: const Icon(Icons.add),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
await gesture.moveTo(tester.getCenter(find.byKey(childKey)));
|
|
await tester.pumpAndSettle();
|
|
expect(hovering, isTrue);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: SizedBox(
|
|
width: 100,
|
|
height: 100,
|
|
child: OutlinedButton.icon(
|
|
focusNode: focusNode,
|
|
onHover: (bool value) {
|
|
hovering = value;
|
|
},
|
|
onPressed: null,
|
|
label: SizedBox(key: childKey),
|
|
icon: const Icon(Icons.add),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(focusNode.hasPrimaryFocus, isFalse);
|
|
focusNode.dispose();
|
|
});
|
|
|
|
testWidgets('Disabled and hovered OutlinedButton.icon responds to mouse-exit', (
|
|
WidgetTester tester,
|
|
) async {
|
|
int onHoverCount = 0;
|
|
late bool hover;
|
|
const Key key = Key('OutlinedButton.icon');
|
|
Widget buildFrame({required bool enabled}) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: 100,
|
|
height: 100,
|
|
child: OutlinedButton.icon(
|
|
key: key,
|
|
onPressed: enabled ? () {} : null,
|
|
onHover: (bool value) {
|
|
onHoverCount += 1;
|
|
hover = value;
|
|
},
|
|
label: const Text('OutlinedButton'),
|
|
icon: const Icon(Icons.add),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame(enabled: true));
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
|
|
await gesture.moveTo(tester.getCenter(find.byKey(key)));
|
|
await tester.pumpAndSettle();
|
|
expect(onHoverCount, 1);
|
|
expect(hover, true);
|
|
|
|
await tester.pumpWidget(buildFrame(enabled: false));
|
|
await tester.pumpAndSettle();
|
|
await gesture.moveTo(Offset.zero);
|
|
// Even though the OutlinedButton has been disabled, the mouse-exit still
|
|
// causes onHover(false) to be called.
|
|
expect(onHoverCount, 2);
|
|
expect(hover, false);
|
|
|
|
await gesture.moveTo(tester.getCenter(find.byKey(key)));
|
|
await tester.pumpAndSettle();
|
|
// We no longer see hover events because the OutlinedButton is disabled
|
|
// and it's no longer in the "hovering" state.
|
|
expect(onHoverCount, 2);
|
|
expect(hover, false);
|
|
|
|
await tester.pumpWidget(buildFrame(enabled: true));
|
|
await tester.pumpAndSettle();
|
|
// The OutlinedButton was enabled while it contained the mouse, however
|
|
// we do not call onHover() because it may call setState().
|
|
expect(onHoverCount, 2);
|
|
expect(hover, false);
|
|
|
|
await gesture.moveTo(tester.getCenter(find.byKey(key)) - const Offset(1, 1));
|
|
await tester.pumpAndSettle();
|
|
// Moving the mouse a little within the OutlinedButton doesn't change anything.
|
|
expect(onHoverCount, 2);
|
|
expect(hover, false);
|
|
});
|
|
|
|
testWidgets('OutlinedButton.icon can be focused/unfocused', (WidgetTester tester) async {
|
|
final FocusNode node = FocusNode(debugLabel: 'OutlinedButton.icon Focus');
|
|
bool gotFocus = false;
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: OutlinedButton.icon(
|
|
focusNode: node,
|
|
onFocusChange: (bool focused) => gotFocus = focused,
|
|
onPressed: () {},
|
|
label: const SizedBox(),
|
|
icon: const Icon(Icons.add),
|
|
),
|
|
),
|
|
);
|
|
|
|
node.requestFocus();
|
|
await tester.pump();
|
|
expect(gotFocus, isTrue);
|
|
expect(node.hasFocus, isTrue);
|
|
node.unfocus();
|
|
await tester.pump();
|
|
expect(gotFocus, isFalse);
|
|
expect(node.hasFocus, isFalse);
|
|
node.dispose();
|
|
});
|
|
|
|
testWidgets('Disabled OutlinedButton.icon cannot receive focus', (WidgetTester tester) async {
|
|
final FocusNode node = FocusNode(debugLabel: 'OutlinedButton.icon Focus');
|
|
bool gotFocus = false;
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: OutlinedButton.icon(
|
|
focusNode: node,
|
|
onFocusChange: (bool focused) => gotFocus = focused,
|
|
onPressed: null,
|
|
label: const SizedBox(),
|
|
icon: const Icon(Icons.add),
|
|
),
|
|
),
|
|
);
|
|
|
|
node.requestFocus();
|
|
await tester.pump();
|
|
expect(gotFocus, isFalse);
|
|
expect(node.hasFocus, isFalse);
|
|
node.dispose();
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/154798.
|
|
testWidgets('OutlinedButton.styleFrom can customize the button icon', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const Color iconColor = Color(0xFFF000FF);
|
|
const double iconSize = 32.0;
|
|
const Color disabledIconColor = Color(0xFFFFF000);
|
|
Widget buildButton({bool enabled = true}) {
|
|
return MaterialApp(
|
|
home: Material(
|
|
child: Center(
|
|
child: OutlinedButton.icon(
|
|
style: OutlinedButton.styleFrom(
|
|
iconColor: iconColor,
|
|
iconSize: iconSize,
|
|
iconAlignment: IconAlignment.end,
|
|
disabledIconColor: disabledIconColor,
|
|
),
|
|
onPressed: enabled ? () {} : null,
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('Button'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Test enabled button.
|
|
await tester.pumpWidget(buildButton());
|
|
expect(tester.getSize(find.byIcon(Icons.add)), const Size(iconSize, iconSize));
|
|
expect(iconStyle(tester, Icons.add).color, iconColor);
|
|
|
|
// Test disabled button.
|
|
await tester.pumpWidget(buildButton(enabled: false));
|
|
await tester.pumpAndSettle();
|
|
expect(iconStyle(tester, Icons.add).color, disabledIconColor);
|
|
|
|
final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last);
|
|
final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add));
|
|
expect(buttonTopRight.dx, iconTopRight.dx + 24.0);
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/162839.
|
|
testWidgets('OutlinedButton icon uses provided foregroundColor over default icon color', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const Color foregroundColor = Color(0xFFFF1234);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Center(
|
|
child: OutlinedButton.icon(
|
|
style: OutlinedButton.styleFrom(foregroundColor: foregroundColor),
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('Button'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(iconStyle(tester, Icons.add).color, foregroundColor);
|
|
});
|
|
|
|
testWidgets('OutlinedButton text and icon respect animation duration', (
|
|
WidgetTester tester,
|
|
) async {
|
|
const String buttonText = 'Button';
|
|
const IconData buttonIcon = Icons.add;
|
|
const Color hoveredColor = Color(0xFFFF0000);
|
|
const Color idleColor = Color(0xFF000000);
|
|
|
|
Widget buildButton({Duration? animationDuration}) {
|
|
return MaterialApp(
|
|
home: Material(
|
|
child: Center(
|
|
child: OutlinedButton.icon(
|
|
style: ButtonStyle(
|
|
animationDuration: animationDuration,
|
|
iconColor: const WidgetStateProperty<Color>.fromMap(<WidgetStatesConstraint, Color>{
|
|
WidgetState.hovered: hoveredColor,
|
|
WidgetState.any: idleColor,
|
|
}),
|
|
foregroundColor: const WidgetStateProperty<Color>.fromMap(
|
|
<WidgetStatesConstraint, Color>{
|
|
WidgetState.hovered: hoveredColor,
|
|
WidgetState.any: idleColor,
|
|
},
|
|
),
|
|
),
|
|
onPressed: () {},
|
|
icon: const Icon(buttonIcon),
|
|
label: const Text(buttonText),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Test default animation duration.
|
|
await tester.pumpWidget(buildButton());
|
|
|
|
expect(textColor(tester, buttonText), idleColor);
|
|
expect(iconStyle(tester, buttonIcon).color, idleColor);
|
|
|
|
final Offset buttonCenter = tester.getCenter(find.text(buttonText));
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
addTearDown(gesture.removePointer);
|
|
await gesture.moveTo(buttonCenter);
|
|
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
expect(textColor(tester, buttonText), hoveredColor.withValues(red: 0.5));
|
|
expect(iconStyle(tester, buttonIcon).color, hoveredColor.withValues(red: 0.5));
|
|
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
expect(textColor(tester, buttonText), hoveredColor);
|
|
expect(iconStyle(tester, buttonIcon).color, hoveredColor);
|
|
|
|
await gesture.removePointer();
|
|
|
|
// Test custom animation duration.
|
|
await tester.pumpWidget(buildButton(animationDuration: const Duration(seconds: 2)));
|
|
await tester.pumpAndSettle();
|
|
|
|
await gesture.moveTo(buttonCenter);
|
|
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1));
|
|
expect(textColor(tester, buttonText), hoveredColor.withValues(red: 0.5));
|
|
expect(iconStyle(tester, buttonIcon).color, hoveredColor.withValues(red: 0.5));
|
|
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1));
|
|
expect(textColor(tester, buttonText), hoveredColor);
|
|
expect(iconStyle(tester, buttonIcon).color, hoveredColor);
|
|
});
|
|
|
|
testWidgets("OutlinedButton's outline should be behind its child", (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/167431
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Center(
|
|
child: RepaintBoundary(
|
|
child: OutlinedButton(
|
|
style: OutlinedButton.styleFrom(backgroundColor: Colors.transparent),
|
|
onPressed: () {},
|
|
child: const Badge(
|
|
backgroundColor: Colors.green,
|
|
label: Text('Ad', style: TextStyle(fontSize: 18, color: Colors.red)),
|
|
child: Icon(Icons.lightbulb_rounded),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await expectLater(
|
|
find.byType(OutlinedButton),
|
|
matchesGoldenFile('outlined_button.badge.outline.png'),
|
|
);
|
|
});
|
|
}
|