// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('The Ink widget expands when no dimensions are set', (WidgetTester tester) async { await tester.pumpWidget( Material( child: Ink(), ), ); expect(find.byType(Ink), findsOneWidget); expect(tester.getSize(find.byType(Ink)), const Size(800.0, 600.0)); }); testWidgets('The Ink widget fits the specified size', (WidgetTester tester) async { const double height = 150.0; const double width = 200.0; await tester.pumpWidget( Material( child: Center( // used to constrain to child's size child: Ink( height: height, width: width, ), ), ), ); await tester.pumpAndSettle(); expect(find.byType(Ink), findsOneWidget); expect(tester.getSize(find.byType(Ink)), const Size(width, height)); }); testWidgets('The Ink widget expands on a unspecified dimension', (WidgetTester tester) async { const double height = 150.0; await tester.pumpWidget( Material( child: Center( // used to constrain to child's size child: Ink( height: height, ), ), ), ); await tester.pumpAndSettle(); expect(find.byType(Ink), findsOneWidget); expect(tester.getSize(find.byType(Ink)), const Size(800, height)); }); testWidgets('The InkWell widget renders an ink splash', (WidgetTester tester) async { const Color highlightColor = Color(0xAAFF0000); const Color splashColor = Color(0xAA0000FF); const BorderRadius borderRadius = BorderRadius.all(Radius.circular(6.0)); await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: SizedBox( width: 200.0, height: 60.0, child: InkWell( borderRadius: borderRadius, highlightColor: highlightColor, splashColor: splashColor, onTap: () { }, ), ), ), ), ), ); final Offset center = tester.getCenter(find.byType(InkWell)); final TestGesture gesture = await tester.startGesture(center); await tester.pump(); // start gesture await tester.pump(const Duration(milliseconds: 200)); // wait for splash to be well under way final RenderBox box = Material.of(tester.element(find.byType(InkWell))) as RenderBox; expect( box, paints ..translate(x: 0.0, y: 0.0) ..save() ..translate(x: 300.0, y: 270.0) ..clipRRect(rrect: RRect.fromLTRBR(0.0, 0.0, 200.0, 60.0, const Radius.circular(6.0))) ..circle(x: 100.0, y: 30.0, radius: 21.0, color: splashColor) ..restore() ..rrect( rrect: RRect.fromLTRBR(300.0, 270.0, 500.0, 330.0, const Radius.circular(6.0)), color: highlightColor, ), ); await gesture.up(); }); testWidgets('The InkWell widget renders an ink ripple', (WidgetTester tester) async { const Color highlightColor = Color(0xAAFF0000); const Color splashColor = Color(0xB40000FF); const BorderRadius borderRadius = BorderRadius.all(Radius.circular(6.0)); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: SizedBox( width: 100.0, height: 100.0, child: InkWell( borderRadius: borderRadius, highlightColor: highlightColor, splashColor: splashColor, onTap: () { }, radius: 100.0, splashFactory: InkRipple.splashFactory, ), ), ), ), ), ); final Offset tapDownOffset = tester.getTopLeft(find.byType(InkWell)); final Offset inkWellCenter = tester.getCenter(find.byType(InkWell)); //final TestGesture gesture = await tester.startGesture(tapDownOffset); await tester.tapAt(tapDownOffset); await tester.pump(); // start gesture final RenderBox box = Material.of(tester.element(find.byType(InkWell)))as RenderBox; bool offsetsAreClose(Offset a, Offset b) => (a - b).distance < 1.0; bool radiiAreClose(double a, double b) => (a - b).abs() < 1.0; PaintPattern ripplePattern(Offset expectedCenter, double expectedRadius, int expectedAlpha) { return paints ..translate(x: 0.0, y: 0.0) ..translate(x: tapDownOffset.dx, y: tapDownOffset.dy) ..something((Symbol method, List<dynamic> arguments) { if (method != #drawCircle) { return false; } final Offset center = arguments[0] as Offset; final double radius = arguments[1] as double; final Paint paint = arguments[2] as Paint; if (offsetsAreClose(center, expectedCenter) && radiiAreClose(radius, expectedRadius) && paint.color.alpha == expectedAlpha) { return true; } throw ''' Expected: center == $expectedCenter, radius == $expectedRadius, alpha == $expectedAlpha Found: center == $center radius == $radius alpha == ${paint.color.alpha}'''; } ); } // Initially the ripple's center is where the tap occurred; // ripplePattern always add a translation of tapDownOffset. expect(box, ripplePattern(Offset.zero, 30.0, 0)); // The ripple fades in for 75ms. During that time its alpha is eased from // 0 to the splashColor's alpha value and its center moves towards the // center of the ink well. await tester.pump(const Duration(milliseconds: 50)); expect(box, ripplePattern(const Offset(17.0, 17.0), 56.0, 120)); // At 75ms the ripple has fade in: it's alpha matches the splashColor's // alpha and its center has moved closer to the ink well's center. await tester.pump(const Duration(milliseconds: 25)); expect(box, ripplePattern(const Offset(29.0, 29.0), 73.0, 180)); // At this point the splash radius has expanded to its limit: 5 past the // ink well's radius parameter. The splash center has moved to its final // location at the inkwell's center and the fade-out is about to start. // The fade-out begins at 225ms = 50ms + 25ms + 150ms. await tester.pump(const Duration(milliseconds: 150)); expect(box, ripplePattern(inkWellCenter - tapDownOffset, 105.0, 180)); // After another 150ms the fade-out is complete. await tester.pump(const Duration(milliseconds: 150)); expect(box, ripplePattern(inkWellCenter - tapDownOffset, 105.0, 0)); }); testWidgets('Does the Ink widget render anything', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: Ink( color: Colors.blue, width: 200.0, height: 200.0, child: InkWell( splashColor: Colors.green, onTap: () { }, ), ), ), ), ), ); final Offset center = tester.getCenter(find.byType(InkWell)); final TestGesture gesture = await tester.startGesture(center); await tester.pump(); // start gesture await tester.pump(const Duration(milliseconds: 200)); // wait for splash to be well under way final RenderBox box = Material.of(tester.element(find.byType(InkWell)))as RenderBox; expect( box, paints ..rect(rect: const Rect.fromLTRB(300.0, 200.0, 500.0, 400.0), color: Color(Colors.blue.value)) ..circle(color: Color(Colors.green.value)), ); await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: Ink( color: Colors.red, width: 200.0, height: 200.0, child: InkWell( splashColor: Colors.green, onTap: () { }, ), ), ), ), ), ); expect(Material.of(tester.element(find.byType(InkWell))), same(box)); expect( box, paints ..rect(rect: const Rect.fromLTRB(300.0, 200.0, 500.0, 400.0), color: Color(Colors.red.value)) ..circle(color: Color(Colors.green.value)), ); await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: InkWell( // this is at a different depth in the tree so it's now a new InkWell splashColor: Colors.green, onTap: () { }, ), ), ), ), ); expect(Material.of(tester.element(find.byType(InkWell))), same(box)); expect(box, isNot(paints..rect())); expect(box, isNot(paints..circle())); await gesture.up(); }); testWidgets('The InkWell widget renders an SelectAction or ActivateAction-induced ink ripple', (WidgetTester tester) async { const Color highlightColor = Color(0xAAFF0000); const Color splashColor = Color(0xB40000FF); const BorderRadius borderRadius = BorderRadius.all(Radius.circular(6.0)); final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); addTearDown(focusNode.dispose); Future<void> buildTest(Intent intent) async { return tester.pumpWidget( Shortcuts( shortcuts: <ShortcutActivator, Intent>{ const SingleActivator(LogicalKeyboardKey.space): intent, }, child: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: SizedBox( width: 100.0, height: 100.0, child: InkWell( borderRadius: borderRadius, highlightColor: highlightColor, splashColor: splashColor, focusNode: focusNode, onTap: () { }, radius: 100.0, splashFactory: InkRipple.splashFactory, ), ), ), ), ), ), ); } await buildTest(const ActivateIntent()); focusNode.requestFocus(); await tester.pumpAndSettle(); final Offset topLeft = tester.getTopLeft(find.byType(InkWell)); final Offset inkWellCenter = tester.getCenter(find.byType(InkWell)) - topLeft; bool offsetsAreClose(Offset a, Offset b) => (a - b).distance < 1.0; bool radiiAreClose(double a, double b) => (a - b).abs() < 1.0; PaintPattern ripplePattern(double expectedRadius, int expectedAlpha) { return paints ..translate(x: 0.0, y: 0.0) ..translate(x: topLeft.dx, y: topLeft.dy) ..something((Symbol method, List<dynamic> arguments) { if (method != #drawCircle) { return false; } final Offset center = arguments[0] as Offset; final double radius = arguments[1] as double; final Paint paint = arguments[2] as Paint; if (offsetsAreClose(center, inkWellCenter) && radiiAreClose(radius, expectedRadius) && paint.color.alpha == expectedAlpha) { return true; } throw ''' Expected: center == $inkWellCenter, radius == $expectedRadius, alpha == $expectedAlpha Found: center == $center radius == $radius alpha == ${paint.color.alpha}'''; }, ); } await buildTest(const ActivateIntent()); await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pump(); final RenderBox box = Material.of(tester.element(find.byType(InkWell)))as RenderBox; // ripplePattern always add a translation of topLeft. expect(box, ripplePattern(30.0, 0)); // The ripple fades in for 75ms. During that time its alpha is eased from // 0 to the splashColor's alpha value. await tester.pump(const Duration(milliseconds: 50)); expect(box, ripplePattern(56.0, 120)); // At 75ms the ripple has faded in: it's alpha matches the splashColor's // alpha. await tester.pump(const Duration(milliseconds: 25)); expect(box, ripplePattern(73.0, 180)); // At this point the splash radius has expanded to its limit: 5 past the // ink well's radius parameter. The fade-out is about to start. // The fade-out begins at 225ms = 50ms + 25ms + 150ms. await tester.pump(const Duration(milliseconds: 150)); expect(box, ripplePattern(105.0, 180)); // After another 150ms the fade-out is complete. await tester.pump(const Duration(milliseconds: 150)); expect(box, ripplePattern(105.0, 0)); }); testWidgets('Cancel an InkRipple that was disposed when its animation ended', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/14391 await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: SizedBox( width: 100.0, height: 100.0, child: InkWell( onTap: () { }, radius: 100.0, splashFactory: InkRipple.splashFactory, ), ), ), ), ), ); final Offset tapDownOffset = tester.getTopLeft(find.byType(InkWell)); await tester.tapAt(tapDownOffset); await tester.pump(); // start splash await tester.pump(const Duration(milliseconds: 375)); // _kFadeOutDuration, in_ripple.dart final TestGesture gesture = await tester.startGesture(tapDownOffset); await tester.pump(); // start gesture await gesture.moveTo(Offset.zero); await gesture.up(); // generates a tap cancel await tester.pumpAndSettle(); }); testWidgets('Cancel an InkRipple that was disposed when its animation ended', (WidgetTester tester) async { const Color highlightColor = Color(0xAAFF0000); const Color splashColor = Color(0xB40000FF); // Regression test for https://github.com/flutter/flutter/issues/14391 await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: SizedBox( width: 100.0, height: 100.0, child: InkWell( splashColor: splashColor, highlightColor: highlightColor, onTap: () { }, radius: 100.0, splashFactory: InkRipple.splashFactory, ), ), ), ), ), ); final Offset tapDownOffset = tester.getTopLeft(find.byType(InkWell)); await tester.tapAt(tapDownOffset); await tester.pump(); // start splash // No delay here so _fadeInController.value=1.0 (InkRipple.dart) // Generate a tap cancel; Will cancel the ink splash before it started final TestGesture gesture = await tester.startGesture(tapDownOffset); await tester.pump(); // start gesture await gesture.moveTo(Offset.zero); await gesture.up(); // generates a tap cancel final RenderBox box = Material.of(tester.element(find.byType(InkWell)))as RenderBox; expect(box, paints..everything((Symbol method, List<dynamic> arguments) { if (method != #drawCircle) { return true; } final Paint paint = arguments[2] as Paint; if (paint.color.alpha == 0) { return true; } throw 'Expected: paint.color.alpha == 0, found: ${paint.color.alpha}'; })); }); testWidgets('The InkWell widget on OverlayPortal does not throw', (WidgetTester tester) async { final OverlayPortalController controller = OverlayPortalController(); controller.show(); late OverlayEntry overlayEntry; addTearDown(() => overlayEntry..remove()..dispose()); await tester.pumpWidget( Center( child: RepaintBoundary( child: SizedBox.square( dimension: 200, child: Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ overlayEntry = OverlayEntry( builder: (BuildContext context) { return Center( child: SizedBox.square( dimension: 100, // The material partially overlaps the overlayChild. // This is to verify that the `overlayChild`'s ink // features aren't clipped by it. child: Material( color: Colors.black, child: OverlayPortal( controller: controller, overlayChildBuilder: (BuildContext context) { return Positioned( right: 0, bottom: 0, child: InkWell( splashColor: Colors.red, onTap: () {}, child: const SizedBox.square(dimension: 100), ), ); }, ), ), ), ); }, ), ], ), ), ), ), ), ); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(InkWell))); addTearDown(() async { await gesture.up(); }); await tester.pump(); // start gesture await tester.pump(const Duration(seconds: 2)); expect(tester.takeException(), isNull); }); testWidgets('Custom rectCallback renders an ink splash from its center', (WidgetTester tester) async { const Color splashColor = Color(0xff00ff00); Widget buildWidget({InteractiveInkFeatureFactory? splashFactory}) { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Material( child: Center( child: SizedBox( width: 100.0, height: 200.0, child: InkResponse( splashColor: splashColor, containedInkWell: true, highlightShape: BoxShape.rectangle, splashFactory: splashFactory, onTap: () { }, ), ), ), ), ); } await tester.pumpWidget(buildWidget()); final Offset center = tester.getCenter(find.byType(SizedBox)); TestGesture gesture = await tester.startGesture(center); await tester.pump(); // start gesture await tester.pumpAndSettle(); // Finish rendering ink splash. RenderBox box = Material.of(tester.element(find.byType(InkResponse))) as RenderBox; expect( box, paints ..circle(x: 50.0, y: 100.0, color: splashColor) ); await gesture.up(); await tester.pumpWidget(buildWidget(splashFactory: _InkRippleFactory())); await tester.pumpAndSettle(); // Finish rendering ink splash. gesture = await tester.startGesture(center); await tester.pump(); // start gesture await tester.pumpAndSettle(); // Finish rendering ink splash. box = Material.of(tester.element(find.byType(InkResponse))) as RenderBox; expect( box, paints ..circle(x: 50.0, y: 50.0, color: splashColor) ); }); testWidgets('Ink with isVisible=false does not paint', (WidgetTester tester) async { const Color testColor = Color(0xffff1234); Widget inkWidget({required bool isVisible}) { return Material( child: Visibility.maintain( visible: isVisible, child: Ink( decoration: const BoxDecoration(color: testColor), ), ), ); } await tester.pumpWidget(inkWidget(isVisible: true)); RenderBox box = tester.renderObject(find.byType(Material)); expect(box, paints..rect(color: testColor)); await tester.pumpWidget(inkWidget(isVisible: false)); box = tester.renderObject(find.byType(Material)); expect(box, isNot(paints..rect(color: testColor))); }); } class _InkRippleFactory extends InteractiveInkFeatureFactory { @override InteractiveInkFeature create({ required MaterialInkController controller, required RenderBox referenceBox, required Offset position, required Color color, required TextDirection textDirection, bool containedInkWell = false, RectCallback? rectCallback, BorderRadius? borderRadius, ShapeBorder? customBorder, double? radius, VoidCallback? onRemoved, }) { return InkRipple( controller: controller, referenceBox: referenceBox, position: position, color: color, containedInkWell: containedInkWell, rectCallback: () => Offset.zero & const Size(100, 100), borderRadius: borderRadius, customBorder: customBorder, radius: radius, onRemoved: onRemoved, textDirection: textDirection, ); } }