flutter_flutter/packages/flutter/test/material/floating_action_button_test.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

1490 lines
47 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'])
@TestOn('!chrome')
library;
import 'dart:ui';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/feedback_tester.dart';
import '../widgets/semantics_tester.dart';
void main() {
final material3Theme = ThemeData();
final material2Theme = ThemeData(useMaterial3: false);
testWidgets('Floating Action Button control test', (WidgetTester tester) async {
var didPressButton = false;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: FloatingActionButton(
onPressed: () {
didPressButton = true;
},
child: const Icon(Icons.add),
),
),
),
);
expect(didPressButton, isFalse);
await tester.tap(find.byType(Icon));
expect(didPressButton, isTrue);
});
testWidgets('Floating Action Button tooltip', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Add',
child: const Icon(Icons.add),
),
),
),
);
await tester.tap(find.byType(Icon));
expect(find.byTooltip('Add'), findsOneWidget);
});
// Regression test for: https://github.com/flutter/flutter/pull/21084
testWidgets('Floating Action Button tooltip (long press button edge)', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Add',
child: const Icon(Icons.add),
),
),
),
);
expect(find.text('Add'), findsNothing);
await tester.longPressAt(_rightEdgeOfFab(tester));
await tester.pumpAndSettle();
expect(find.text('Add'), findsOneWidget);
});
// Regression test for: https://github.com/flutter/flutter/pull/21084
testWidgets('Floating Action Button tooltip (long press button edge - no child)', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton(onPressed: () {}, tooltip: 'Add'),
),
),
);
expect(find.text('Add'), findsNothing);
await tester.longPressAt(_rightEdgeOfFab(tester));
await tester.pumpAndSettle();
expect(find.text('Add'), findsOneWidget);
});
testWidgets('Floating Action Button tooltip (no child)', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton(onPressed: () {}, tooltip: 'Add'),
),
),
);
expect(find.text('Add'), findsNothing);
// Test hover for tooltip.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(() => gesture.removePointer());
await gesture.moveTo(tester.getCenter(find.byType(FloatingActionButton)));
await tester.pumpAndSettle();
expect(find.text('Add'), findsOneWidget);
await gesture.moveTo(Offset.zero);
await tester.pumpAndSettle();
expect(find.text('Add'), findsNothing);
// Test long press for tooltip.
await tester.longPress(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
expect(find.text('Add'), findsOneWidget);
});
testWidgets('Floating Action Button tooltip reacts when disabled', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(floatingActionButton: FloatingActionButton(onPressed: null, tooltip: 'Add')),
),
);
expect(find.text('Add'), findsNothing);
// Test hover for tooltip.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(() => gesture.removePointer());
await tester.pumpAndSettle();
await gesture.moveTo(tester.getCenter(find.byType(FloatingActionButton)));
await tester.pumpAndSettle();
expect(find.text('Add'), findsOneWidget);
await gesture.moveTo(Offset.zero);
await tester.pumpAndSettle();
expect(find.text('Add'), findsNothing);
// Test long press for tooltip.
await tester.longPress(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
expect(find.text('Add'), findsOneWidget);
});
testWidgets('Floating Action Button elevation when highlighted - effect', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
theme: material3Theme,
home: Scaffold(floatingActionButton: FloatingActionButton(onPressed: () {})),
),
);
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
final TestGesture gesture = await tester.press(find.byType(PhysicalShape));
await tester.pump();
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton(onPressed: () {}, highlightElevation: 20.0),
),
),
);
await tester.pump();
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 20.0);
await gesture.up();
await tester.pump();
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 20.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
});
testWidgets('Floating Action Button elevation when disabled - defaults', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(floatingActionButton: FloatingActionButton(onPressed: null)),
),
);
// Disabled elevation defaults to regular default elevation.
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
});
testWidgets('Floating Action Button elevation when disabled - override', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton(onPressed: null, disabledElevation: 0),
),
),
);
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 0.0);
});
testWidgets('Floating Action Button elevation when disabled - effect', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(floatingActionButton: FloatingActionButton(onPressed: null)),
),
);
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton(onPressed: null, disabledElevation: 3.0),
),
),
);
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 3.0);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton(onPressed: () {}, disabledElevation: 3.0),
),
),
);
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 3.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
});
testWidgets('Floating Action Button elevation when disabled while highlighted - effect', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
theme: material3Theme,
home: Scaffold(floatingActionButton: FloatingActionButton(onPressed: () {})),
),
);
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.press(find.byType(PhysicalShape));
await tester.pump();
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pumpWidget(
MaterialApp(
theme: material3Theme,
home: const Scaffold(floatingActionButton: FloatingActionButton(onPressed: null)),
),
);
await tester.pump();
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pumpWidget(
MaterialApp(
theme: material3Theme,
home: Scaffold(floatingActionButton: FloatingActionButton(onPressed: () {})),
),
);
await tester.pump();
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
});
testWidgets('Floating Action Button states elevation', (WidgetTester tester) async {
final focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
theme: material3Theme,
home: Scaffold(
body: FloatingActionButton.extended(
label: const Text('tooltip'),
onPressed: () {},
focusNode: focusNode,
),
),
),
);
final Finder fabFinder = find.byType(PhysicalShape);
PhysicalShape getFABWidget(Finder finder) => tester.widget<PhysicalShape>(finder);
// Default, not disabled.
expect(getFABWidget(fabFinder).elevation, 6);
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(getFABWidget(fabFinder).elevation, 6);
// Hovered.
final Offset center = tester.getCenter(fabFinder);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(getFABWidget(fabFinder).elevation, 8);
// 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(getFABWidget(fabFinder).elevation, 6);
focusNode.dispose();
});
testWidgets('FlatActionButton mini size is configurable by ThemeData.materialTapTargetSize', (
WidgetTester tester,
) async {
final Key key1 = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Theme(
data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded),
child: Scaffold(
floatingActionButton: FloatingActionButton(key: key1, mini: true, onPressed: null),
),
),
),
);
expect(tester.getSize(find.byKey(key1)), const Size(48.0, 48.0));
final Key key2 = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Theme(
data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap),
child: Scaffold(
floatingActionButton: FloatingActionButton(key: key2, mini: true, onPressed: null),
),
),
),
);
expect(tester.getSize(find.byKey(key2)), const Size(40.0, 40.0));
});
testWidgets('FloatingActionButton.isExtended', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: material3Theme,
home: const Scaffold(floatingActionButton: FloatingActionButton(onPressed: null)),
),
);
final Finder fabFinder = find.byType(FloatingActionButton);
FloatingActionButton getFabWidget() {
return tester.widget<FloatingActionButton>(fabFinder);
}
final Finder materialButtonFinder = find.byType(RawMaterialButton);
RawMaterialButton getRawMaterialButtonWidget() {
return tester.widget<RawMaterialButton>(materialButtonFinder);
}
expect(getFabWidget().isExtended, false);
expect(
getRawMaterialButtonWidget().shape,
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))),
);
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton.extended(
label: SizedBox(width: 100.0, child: Text('label')),
icon: Icon(Icons.android),
onPressed: null,
),
),
),
);
expect(getFabWidget().isExtended, true);
expect(
getRawMaterialButtonWidget().shape,
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))),
);
expect(find.text('label'), findsOneWidget);
expect(find.byType(Icon), findsOneWidget);
// Verify that the widget's height is 56 and that its internal
/// horizontal layout is: 16 icon 8 label 20
expect(tester.getSize(fabFinder).height, 56.0);
final double fabLeft = tester.getTopLeft(fabFinder).dx;
final double fabRight = tester.getTopRight(fabFinder).dx;
final double iconLeft = tester.getTopLeft(find.byType(Icon)).dx;
final double iconRight = tester.getTopRight(find.byType(Icon)).dx;
final double labelLeft = tester.getTopLeft(find.text('label')).dx;
final double labelRight = tester.getTopRight(find.text('label')).dx;
expect(iconLeft - fabLeft, 16.0);
expect(labelLeft - iconRight, 8.0);
expect(fabRight - labelRight, 20.0);
// The overall width of the button is:
// 168 = 16 + 24(icon) + 8 + 100(label) + 20
expect(tester.getSize(find.byType(Icon)).width, 24.0);
expect(tester.getSize(find.text('label')).width, 100.0);
expect(tester.getSize(fabFinder).width, 168);
});
testWidgets('FloatingActionButton.isExtended (without icon)', (WidgetTester tester) async {
final Finder fabFinder = find.byType(FloatingActionButton);
FloatingActionButton getFabWidget() {
return tester.widget<FloatingActionButton>(fabFinder);
}
final Finder materialButtonFinder = find.byType(RawMaterialButton);
RawMaterialButton getRawMaterialButtonWidget() {
return tester.widget<RawMaterialButton>(materialButtonFinder);
}
await tester.pumpWidget(
MaterialApp(
theme: material3Theme,
home: const Scaffold(
floatingActionButton: FloatingActionButton.extended(
label: SizedBox(width: 100.0, child: Text('label')),
onPressed: null,
),
),
),
);
expect(getFabWidget().isExtended, true);
expect(
getRawMaterialButtonWidget().shape,
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))),
);
expect(find.text('label'), findsOneWidget);
expect(find.byType(Icon), findsNothing);
// Verify that the widget's height is 56 and that its internal
/// horizontal layout is: 20 label 20
expect(tester.getSize(fabFinder).height, 56.0);
final double fabLeft = tester.getTopLeft(fabFinder).dx;
final double fabRight = tester.getTopRight(fabFinder).dx;
final double labelLeft = tester.getTopLeft(find.text('label')).dx;
final double labelRight = tester.getTopRight(find.text('label')).dx;
expect(labelLeft - fabLeft, 20.0);
expect(fabRight - labelRight, 20.0);
// The overall width of the button is:
// 140 = 20 + 100(label) + 20
expect(tester.getSize(find.text('label')).width, 100.0);
expect(tester.getSize(fabFinder).width, 140);
});
testWidgets('Floating Action Button heroTag', (WidgetTester tester) async {
late BuildContext theContext;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
theContext = context;
return const FloatingActionButton(heroTag: 1, onPressed: null);
},
),
floatingActionButton: const FloatingActionButton(heroTag: 2, onPressed: null),
),
),
);
Navigator.push(
theContext,
PageRouteBuilder<void>(
pageBuilder:
(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return const Placeholder();
},
),
);
await tester
.pump(); // this would fail if heroTag was the same on both FloatingActionButtons (see below).
});
testWidgets('Floating Action Button heroTag - with duplicate', (WidgetTester tester) async {
late BuildContext theContext;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
theContext = context;
return const FloatingActionButton(onPressed: null);
},
),
floatingActionButton: const FloatingActionButton(onPressed: null),
),
),
);
Navigator.push(
theContext,
PageRouteBuilder<void>(
pageBuilder:
(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return const Placeholder();
},
),
);
await tester.pump();
expect(tester.takeException().toString(), contains('FloatingActionButton'));
});
testWidgets('Floating Action Button heroTag - with duplicate', (WidgetTester tester) async {
late BuildContext theContext;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
theContext = context;
return const FloatingActionButton(heroTag: 'xyzzy', onPressed: null);
},
),
floatingActionButton: const FloatingActionButton(heroTag: 'xyzzy', onPressed: null),
),
),
);
Navigator.push(
theContext,
PageRouteBuilder<void>(
pageBuilder:
(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return const Placeholder();
},
),
);
await tester.pump();
expect(tester.takeException().toString(), contains('xyzzy'));
});
testWidgets('Floating Action Button semantics (enabled)', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add, semanticLabel: 'Add'),
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'Add',
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
),
],
),
ignoreTransform: true,
ignoreId: true,
ignoreRect: true,
),
);
semantics.dispose();
});
testWidgets('Floating Action Button semantics (disabled)', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: FloatingActionButton(
onPressed: null,
child: Icon(Icons.add, semanticLabel: 'Add'),
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'Add',
flags: <SemanticsFlag>[SemanticsFlag.isButton, SemanticsFlag.hasEnabledState],
),
],
),
ignoreTransform: true,
ignoreId: true,
ignoreRect: true,
),
);
semantics.dispose();
});
testWidgets('Tooltip is used as semantics tooltip', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Add Photo',
child: const Icon(Icons.add_a_photo),
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
tooltip: 'Add Photo',
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
),
],
),
],
),
],
),
],
),
ignoreTransform: true,
ignoreId: true,
ignoreRect: true,
),
);
semantics.dispose();
});
testWidgets('extended FAB hero transitions succeed', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/18782
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
floatingActionButton: Builder(
builder: (BuildContext context) {
// define context of Navigator.push()
return FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: const Text('A long FAB label'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: const Text('X'),
onPressed: () {},
),
body: Center(
child: ElevatedButton(
child: const Text('POP'),
onPressed: () {
Navigator.pop(context);
},
),
),
);
},
),
);
},
);
},
),
body: const Center(child: Text('Hello World')),
),
),
);
final Finder longFAB = find.text('A long FAB label');
final Finder shortFAB = find.text('X');
final Finder helloWorld = find.text('Hello World');
expect(longFAB, findsOneWidget);
expect(shortFAB, findsNothing);
expect(helloWorld, findsOneWidget);
await tester.tap(longFAB);
await tester.pumpAndSettle();
expect(shortFAB, findsOneWidget);
expect(longFAB, findsNothing);
// Trigger a hero transition from shortFAB to longFAB.
await tester.tap(find.text('POP'));
await tester.pumpAndSettle();
expect(longFAB, findsOneWidget);
expect(shortFAB, findsNothing);
expect(helloWorld, findsOneWidget);
});
// This test prevents https://github.com/flutter/flutter/issues/20483
testWidgets('Floating Action Button clips ink splash and highlight', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
MaterialApp(
theme: material3Theme,
home: Scaffold(
body: Center(
child: RepaintBoundary(
key: key,
child: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
),
),
),
),
);
await tester.press(find.byKey(key));
await tester.pump();
await tester.pump(const Duration(milliseconds: 1000));
await expectLater(find.byKey(key), matchesGoldenFile('floating_action_button_test.clip.png'));
});
testWidgets('Floating Action Button changes mouse cursor when hovered', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: FloatingActionButton.extended(
onPressed: () {},
mouseCursor: SystemMouseCursors.text,
label: const Text('label'),
icon: const Icon(Icons.android),
),
),
),
),
);
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
pointer: 1,
);
await gesture.addPointer(location: tester.getCenter(find.byType(FloatingActionButton)));
await tester.pump();
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.text,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: FloatingActionButton(
onPressed: () {},
mouseCursor: SystemMouseCursors.text,
child: const Icon(Icons.add),
),
),
),
),
);
await gesture.moveTo(tester.getCenter(find.byType(FloatingActionButton)));
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.text,
);
// Test default cursor
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
),
),
),
);
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
);
// Test default cursor when disabled
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: FloatingActionButton(onPressed: null, child: Icon(Icons.add)),
),
),
),
);
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.basic,
);
});
testWidgets('Floating Action Button has no clip by default', (WidgetTester tester) async {
final focusNode = FocusNode();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FloatingActionButton(
focusNode: focusNode,
onPressed: () {
/* to make sure the button is enabled */
},
),
),
);
focusNode.unfocus();
await tester.pump();
expect(
tester.renderObject(find.byType(FloatingActionButton)),
paintsExactlyCountTimes(#clipPath, 0),
);
focusNode.dispose();
});
testWidgets('Can find FloatingActionButton semantics', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: FloatingActionButton(onPressed: () {})));
expect(
tester.getSemantics(find.byType(FloatingActionButton)),
matchesSemantics(
hasTapAction: true,
hasFocusAction: true,
hasEnabledState: true,
isButton: true,
isEnabled: true,
isFocusable: true,
),
);
});
testWidgets('Foreground color applies to icon on fab', (WidgetTester tester) async {
const foregroundColor = Color(0xcafefeed);
await tester.pumpWidget(
MaterialApp(
home: FloatingActionButton(
onPressed: () {},
foregroundColor: foregroundColor,
child: const Icon(Icons.access_alarm),
),
),
);
final RichText iconRichText = tester.widget<RichText>(
find.descendant(of: find.byIcon(Icons.access_alarm), matching: find.byType(RichText)),
);
expect(iconRichText.text.style!.color, foregroundColor);
});
testWidgets('FloatingActionButton uses custom splash color', (WidgetTester tester) async {
const splashColor = Color(0xcafefeed);
await tester.pumpWidget(
MaterialApp(
theme: material2Theme,
home: FloatingActionButton(
onPressed: () {},
splashColor: splashColor,
child: const Icon(Icons.access_alarm),
),
),
);
await tester.press(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
expect(find.byType(FloatingActionButton), paints..circle(color: splashColor));
});
testWidgets('extended FAB does not show label when isExtended is false', (
WidgetTester tester,
) async {
const iconKey = Key('icon');
const labelKey = Key('label');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FloatingActionButton.extended(
isExtended: false,
label: const Text('', key: labelKey),
icon: const Icon(Icons.add, key: iconKey),
onPressed: () {},
),
),
);
// Verify that Icon is present and label is not.
expect(find.byKey(iconKey), findsOneWidget);
expect(find.byKey(labelKey), findsNothing);
});
testWidgets('FloatingActionButton.small configures correct size', (WidgetTester tester) async {
final Key key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton.small(
key: key,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: null,
),
),
),
);
expect(tester.getSize(find.byKey(key)), const Size(40.0, 40.0));
});
testWidgets('FloatingActionButton.large configures correct size', (WidgetTester tester) async {
final Key key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(floatingActionButton: FloatingActionButton.large(key: key, onPressed: null)),
),
);
expect(tester.getSize(find.byKey(key)), const Size(96.0, 96.0));
});
testWidgets('FloatingActionButton.extended can customize spacing', (WidgetTester tester) async {
const iconKey = Key('icon');
const labelKey = Key('label');
const spacing = 33.0;
const padding = EdgeInsetsDirectional.only(start: 5.0, end: 6.0);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton.extended(
label: const Text('', key: labelKey),
icon: const Icon(Icons.add, key: iconKey),
extendedIconLabelSpacing: spacing,
extendedPadding: padding,
onPressed: () {},
),
),
),
);
expect(
tester.getTopLeft(find.byKey(labelKey)).dx - tester.getTopRight(find.byKey(iconKey)).dx,
spacing,
);
expect(
tester.getTopLeft(find.byKey(iconKey)).dx -
tester.getTopLeft(find.byType(FloatingActionButton)).dx,
padding.start,
);
expect(
tester.getTopRight(find.byType(FloatingActionButton)).dx -
tester.getTopRight(find.byKey(labelKey)).dx,
padding.end,
);
});
testWidgets('FloatingActionButton.extended can customize text style', (
WidgetTester tester,
) async {
const labelKey = Key('label');
const style = TextStyle(letterSpacing: 2.0);
await tester.pumpWidget(
MaterialApp(
theme: material2Theme,
home: Scaffold(
floatingActionButton: FloatingActionButton.extended(
label: const Text('', key: labelKey),
icon: const Icon(Icons.add),
extendedTextStyle: style,
onPressed: () {},
),
),
),
);
final RawMaterialButton rawMaterialButton = tester.widget<RawMaterialButton>(
find.descendant(
of: find.byType(FloatingActionButton),
matching: find.byType(RawMaterialButton),
),
);
// The color comes from the default color scheme's onSecondary value.
expect(rawMaterialButton.textStyle, style.copyWith(color: const Color(0xffffffff)));
});
group('Material 2', () {
// These tests are only relevant for Material 2. Once Material 2
// support is deprecated and the APIs are removed, these tests
// can be deleted.
testWidgets('Floating Action Button elevation when highlighted - effect', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
theme: material2Theme,
home: Scaffold(floatingActionButton: FloatingActionButton(onPressed: () {})),
),
);
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
final TestGesture gesture = await tester.press(find.byType(PhysicalShape));
await tester.pump();
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 12.0);
await tester.pumpWidget(
MaterialApp(
theme: material2Theme,
home: Scaffold(
floatingActionButton: FloatingActionButton(onPressed: () {}, highlightElevation: 20.0),
),
),
);
await tester.pump();
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 12.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 20.0);
await gesture.up();
await tester.pump();
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 20.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
});
testWidgets('Floating Action Button elevation when disabled while highlighted - effect', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
theme: material2Theme,
home: Scaffold(floatingActionButton: FloatingActionButton(onPressed: () {})),
),
);
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.press(find.byType(PhysicalShape));
await tester.pump();
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 12.0);
await tester.pumpWidget(
MaterialApp(
theme: material2Theme,
home: const Scaffold(floatingActionButton: FloatingActionButton(onPressed: null)),
),
);
await tester.pump();
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 12.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pumpWidget(
MaterialApp(
theme: material2Theme,
home: Scaffold(floatingActionButton: FloatingActionButton(onPressed: () {})),
),
);
await tester.pump();
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
await tester.pump(const Duration(seconds: 1));
expect(tester.widget<PhysicalShape>(find.byType(PhysicalShape)).elevation, 6.0);
});
testWidgets('Floating Action Button states elevation', (WidgetTester tester) async {
final focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
theme: material2Theme,
home: Scaffold(
body: FloatingActionButton.extended(
label: const Text('tooltip'),
onPressed: () {},
focusNode: focusNode,
),
),
),
);
final Finder fabFinder = find.byType(PhysicalShape);
PhysicalShape getFABWidget(Finder finder) => tester.widget<PhysicalShape>(finder);
// Default, not disabled.
expect(getFABWidget(fabFinder).elevation, 6);
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(getFABWidget(fabFinder).elevation, 6);
// Hovered.
final Offset center = tester.getCenter(fabFinder);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(center);
await tester.pumpAndSettle();
expect(getFABWidget(fabFinder).elevation, 8);
// 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(getFABWidget(fabFinder).elevation, 12);
focusNode.dispose();
});
testWidgets('FloatingActionButton.isExtended', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: material2Theme,
home: const Scaffold(floatingActionButton: FloatingActionButton(onPressed: null)),
),
);
final Finder fabFinder = find.byType(FloatingActionButton);
FloatingActionButton getFabWidget() {
return tester.widget<FloatingActionButton>(fabFinder);
}
final Finder materialButtonFinder = find.byType(RawMaterialButton);
RawMaterialButton getRawMaterialButtonWidget() {
return tester.widget<RawMaterialButton>(materialButtonFinder);
}
expect(getFabWidget().isExtended, false);
expect(getRawMaterialButtonWidget().shape, const CircleBorder());
await tester.pumpWidget(
MaterialApp(
theme: material2Theme,
home: const Scaffold(
floatingActionButton: FloatingActionButton.extended(
label: SizedBox(width: 100.0, child: Text('label')),
icon: Icon(Icons.android),
onPressed: null,
),
),
),
);
expect(getFabWidget().isExtended, true);
expect(getRawMaterialButtonWidget().shape, const StadiumBorder());
expect(find.text('label'), findsOneWidget);
expect(find.byType(Icon), findsOneWidget);
// Verify that the widget's height is 48 and that its internal
/// horizontal layout is: 16 icon 8 label 20
expect(tester.getSize(fabFinder).height, 48.0);
final double fabLeft = tester.getTopLeft(fabFinder).dx;
final double fabRight = tester.getTopRight(fabFinder).dx;
final double iconLeft = tester.getTopLeft(find.byType(Icon)).dx;
final double iconRight = tester.getTopRight(find.byType(Icon)).dx;
final double labelLeft = tester.getTopLeft(find.text('label')).dx;
final double labelRight = tester.getTopRight(find.text('label')).dx;
expect(iconLeft - fabLeft, 16.0);
expect(labelLeft - iconRight, 8.0);
expect(fabRight - labelRight, 20.0);
// The overall width of the button is:
// 168 = 16 + 24(icon) + 8 + 100(label) + 20
expect(tester.getSize(find.byType(Icon)).width, 24.0);
expect(tester.getSize(find.text('label')).width, 100.0);
expect(tester.getSize(fabFinder).width, 168);
});
testWidgets('FloatingActionButton.isExtended (without icon)', (WidgetTester tester) async {
final Finder fabFinder = find.byType(FloatingActionButton);
FloatingActionButton getFabWidget() {
return tester.widget<FloatingActionButton>(fabFinder);
}
final Finder materialButtonFinder = find.byType(RawMaterialButton);
RawMaterialButton getRawMaterialButtonWidget() {
return tester.widget<RawMaterialButton>(materialButtonFinder);
}
await tester.pumpWidget(
MaterialApp(
theme: material2Theme,
home: const Scaffold(
floatingActionButton: FloatingActionButton.extended(
label: SizedBox(width: 100.0, child: Text('label')),
onPressed: null,
),
),
),
);
expect(getFabWidget().isExtended, true);
expect(getRawMaterialButtonWidget().shape, const StadiumBorder());
expect(find.text('label'), findsOneWidget);
expect(find.byType(Icon), findsNothing);
// Verify that the widget's height is 48 and that its internal
/// horizontal layout is: 20 label 20
expect(tester.getSize(fabFinder).height, 48.0);
final double fabLeft = tester.getTopLeft(fabFinder).dx;
final double fabRight = tester.getTopRight(fabFinder).dx;
final double labelLeft = tester.getTopLeft(find.text('label')).dx;
final double labelRight = tester.getTopRight(find.text('label')).dx;
expect(labelLeft - fabLeft, 20.0);
expect(fabRight - labelRight, 20.0);
// The overall width of the button is:
// 140 = 20 + 100(label) + 20
expect(tester.getSize(find.text('label')).width, 100.0);
expect(tester.getSize(fabFinder).width, 140);
});
// This test prevents https://github.com/flutter/flutter/issues/20483
testWidgets('Floating Action Button clips ink splash and highlight', (
WidgetTester tester,
) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
MaterialApp(
theme: material2Theme,
home: Scaffold(
body: Center(
child: RepaintBoundary(
key: key,
child: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
),
),
),
),
);
await tester.press(find.byKey(key));
await tester.pump();
await tester.pump(const Duration(milliseconds: 1000));
await expectLater(
find.byKey(key),
matchesGoldenFile('floating_action_button_test_m2.clip.png'),
);
});
});
group('feedback', () {
late FeedbackTester feedback;
setUp(() {
feedback = FeedbackTester();
});
tearDown(() {
feedback.dispose();
});
testWidgets('FloatingActionButton with enabled feedback', (WidgetTester tester) async {
const enableFeedback = true;
await tester.pumpWidget(
MaterialApp(
home: FloatingActionButton(
onPressed: () {},
enableFeedback: enableFeedback,
child: const Icon(Icons.access_alarm),
),
),
);
await tester.tap(find.byType(RawMaterialButton));
await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 1);
expect(feedback.hapticCount, 0);
});
testWidgets('FloatingActionButton with disabled feedback', (WidgetTester tester) async {
const enableFeedback = false;
await tester.pumpWidget(
MaterialApp(
home: FloatingActionButton(
onPressed: () {},
enableFeedback: enableFeedback,
child: const Icon(Icons.access_alarm),
),
),
);
await tester.tap(find.byType(RawMaterialButton));
await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 0);
expect(feedback.hapticCount, 0);
});
testWidgets('FloatingActionButton with enabled feedback by default', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.access_alarm)),
),
);
await tester.tap(find.byType(RawMaterialButton));
await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 1);
expect(feedback.hapticCount, 0);
});
testWidgets('FloatingActionButton with disabled feedback using FloatingActionButtonTheme', (
WidgetTester tester,
) async {
const enableFeedbackTheme = false;
final theme = ThemeData(
floatingActionButtonTheme: const FloatingActionButtonThemeData(
enableFeedback: enableFeedbackTheme,
),
);
await tester.pumpWidget(
MaterialApp(
home: Theme(
data: theme,
child: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.access_alarm)),
),
),
);
await tester.tap(find.byType(RawMaterialButton));
await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 0);
expect(feedback.hapticCount, 0);
});
testWidgets(
'FloatingActionButton.enableFeedback is overridden by FloatingActionButtonThemeData.enableFeedback',
(WidgetTester tester) async {
const enableFeedbackTheme = false;
const enableFeedback = true;
final theme = ThemeData(
floatingActionButtonTheme: const FloatingActionButtonThemeData(
enableFeedback: enableFeedbackTheme,
),
);
await tester.pumpWidget(
MaterialApp(
home: Theme(
data: theme,
child: FloatingActionButton(
enableFeedback: enableFeedback,
onPressed: () {},
child: const Icon(Icons.access_alarm),
),
),
),
);
await tester.tap(find.byType(RawMaterialButton));
await tester.pump(const Duration(seconds: 1));
expect(feedback.clickSoundCount, 1);
expect(feedback.hapticCount, 0);
},
);
});
testWidgets('FloatingActionButton does not crash at zero area', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
floatingActionButton: Center(
child: SizedBox.shrink(child: FloatingActionButton(onPressed: () {})),
),
),
),
);
expect(tester.getSize(find.byType(FloatingActionButton)), Size.zero);
});
}
Offset _rightEdgeOfFab(WidgetTester tester) {
final Finder fab = find.byType(FloatingActionButton);
return tester.getRect(fab).centerRight - const Offset(1.0, 0.0);
}