// 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/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/src/services/keyboard_key.g.dart'; import 'package:flutter_test/flutter_test.dart'; import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; void main() { testWidgets('InkWell gestures control test', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: InkWell( onTap: () { log.add('tap'); }, onDoubleTap: () { log.add('double-tap'); }, onLongPress: () { log.add('long-press'); }, onTapDown: (TapDownDetails details) { log.add('tap-down'); }, onTapUp: (TapUpDetails details) { log.add('tap-up'); }, onTapCancel: () { log.add('tap-cancel'); }, ), ), ), )); await tester.tap(find.byType(InkWell), pointer: 1); expect(log, isEmpty); await tester.pump(const Duration(seconds: 1)); expect(log, equals(<String>['tap-down', 'tap-up', 'tap'])); log.clear(); await tester.tap(find.byType(InkWell), pointer: 2); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.byType(InkWell), pointer: 3); expect(log, equals(<String>['double-tap'])); log.clear(); await tester.longPress(find.byType(InkWell), pointer: 4); expect(log, equals(<String>['tap-down', 'tap-cancel', 'long-press'])); log.clear(); TestGesture gesture = await tester.startGesture(tester.getRect(find.byType(InkWell)).center); await tester.pump(const Duration(milliseconds: 100)); expect(log, equals(<String>['tap-down'])); await gesture.up(); await tester.pump(const Duration(seconds: 1)); expect(log, equals(<String>['tap-down', 'tap-up', 'tap'])); log.clear(); gesture = await tester.startGesture(tester.getRect(find.byType(InkWell)).center); await tester.pump(const Duration(milliseconds: 100)); await gesture.moveBy(const Offset(0.0, 200.0)); await gesture.cancel(); expect(log, equals(<String>['tap-down', 'tap-cancel'])); }); testWidgets('InkWell only onTapDown enables gestures', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/96030 bool downTapped = false; await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: InkWell( onTapDown: (TapDownDetails details) { downTapped = true; }, ), ), ), )); await tester.tap(find.byType(InkWell)); expect(downTapped, true); }); testWidgets('InkWell invokes activation actions when expected', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Shortcuts( shortcuts: const <ShortcutActivator, Intent>{ SingleActivator(LogicalKeyboardKey.space): ActivateIntent(), SingleActivator(LogicalKeyboardKey.enter): ButtonActivateIntent(), }, child: Material( child: Center( child: InkWell( autofocus: true, onTap: () { log.add('tap'); }, ), ), ), ), )); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pump(); expect(log, equals(<String>['tap'])); log.clear(); await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pump(); expect(log, equals(<String>['tap'])); }); testWidgets('long-press and tap on disabled should not throw', (WidgetTester tester) async { await tester.pumpWidget(const Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: InkWell(), ), ), )); await tester.tap(find.byType(InkWell), pointer: 1); await tester.pump(const Duration(seconds: 1)); await tester.longPress(find.byType(InkWell), pointer: 1); await tester.pump(const Duration(seconds: 1)); }); testWidgets('ink well changes color on hover', (WidgetTester tester) async { await tester.pumpWidget(Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: InkWell( hoverColor: const Color(0xff00ff00), splashColor: const Color(0xffff0000), focusColor: const Color(0xff0000ff), highlightColor: const Color(0xf00fffff), onTap: () { }, onLongPress: () { }, onHover: (bool hover) { }, ), ), ), ), )); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(tester.getCenter(find.byType(SizedBox))); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0xff00ff00))); }); testWidgets('ink well changes color on hover with overlayColor', (WidgetTester tester) async { // Same test as 'ink well changes color on hover' except that the // hover color is specified with the overlayColor parameter. await tester.pumpWidget(Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: InkWell( overlayColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) { if (states.contains(MaterialState.hovered)) { return const Color(0xff00ff00); } if (states.contains(MaterialState.focused)) { return const Color(0xff0000ff); } if (states.contains(MaterialState.pressed)) { return const Color(0xf00fffff); } return const Color(0xffbadbad); // Shouldn't happen. }), onTap: () { }, onLongPress: () { }, onHover: (bool hover) { }, ), ), ), ), )); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(tester.getCenter(find.byType(SizedBox))); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0xff00ff00))); }); testWidgets('ink response changes color on focus', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: InkWell( focusNode: focusNode, hoverColor: const Color(0xff00ff00), splashColor: const Color(0xffff0000), focusColor: const Color(0xff0000ff), highlightColor: const Color(0xf00fffff), onTap: () { }, onLongPress: () { }, onHover: (bool hover) { }, ), ), ), ), ), ); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paintsExactlyCountTimes(#drawRect, 0)); focusNode.requestFocus(); await tester.pumpAndSettle(); expect( inkFeatures, paints ..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0xff0000ff)), ); }); testWidgets('ink response changes color on focus with overlayColor', (WidgetTester tester) async { // Same test as 'ink well changes color on focus' except that the // hover color is specified with the overlayColor parameter. FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: InkWell( focusNode: focusNode, overlayColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) { if (states.contains(MaterialState.hovered)) { return const Color(0xff00ff00); } if (states.contains(MaterialState.focused)) { return const Color(0xff0000ff); } if (states.contains(MaterialState.pressed)) { return const Color(0xf00fffff); } return const Color(0xffbadbad); // Shouldn't happen. }), highlightColor: const Color(0xf00fffff), onTap: () { }, onLongPress: () { }, onHover: (bool hover) { }, ), ), ), ), ), ); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paintsExactlyCountTimes(#drawRect, 0)); focusNode.requestFocus(); await tester.pumpAndSettle(); expect( inkFeatures, paints..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0xff0000ff)), ); }); testWidgets('ink well changes color on pressed with overlayColor', (WidgetTester tester) async { const Color pressedColor = Color(0xffdd00ff); await tester.pumpWidget(Material( child: Directionality( textDirection: TextDirection.ltr, child: Container( alignment: Alignment.topLeft, child: SizedBox( width: 100, height: 100, child: InkWell( splashFactory: NoSplash.splashFactory, overlayColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) { if (states.contains(MaterialState.pressed)) { return pressedColor; } return const Color(0xffbadbad); // Shouldn't happen. }), onTap: () { }, ), ), ), ), )); await tester.pumpAndSettle(); final TestGesture gesture = await tester.startGesture(tester.getRect(find.byType(InkWell)).center); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..rect(rect: const Rect.fromLTRB(0, 0, 100, 100), color: pressedColor.withAlpha(0))); await tester.pumpAndSettle(); // Let the press highlight animation finish. expect(inkFeatures, paints..rect(rect: const Rect.fromLTRB(0, 0, 100, 100), color: pressedColor)); await gesture.up(); }); testWidgets('ink response splashColor matches splashColor parameter', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); const Color splashColor = Color(0xffff0000); await tester.pumpWidget(Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: Focus( focusNode: focusNode, child: SizedBox( width: 100, height: 100, child: InkWell( hoverColor: const Color(0xff00ff00), splashColor: splashColor, focusColor: const Color(0xff0000ff), highlightColor: const Color(0xf00fffff), onTap: () { }, onLongPress: () { }, onHover: (bool hover) { }, ), ), ), ), ), )); await tester.pumpAndSettle(); final TestGesture gesture = await tester.startGesture(tester.getRect(find.byType(InkWell)).center); await tester.pump(const Duration(milliseconds: 200)); // unconfirmed splash is well underway final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..circle(x: 50, y: 50, color: splashColor)); await gesture.up(); }); testWidgets('ink response splashColor matches resolved overlayColor for MaterialState.pressed', (WidgetTester tester) async { // Same test as 'ink response splashColor matches splashColor // parameter' except that the splash color is specified with the // overlayColor parameter. FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); const Color splashColor = Color(0xffff0000); await tester.pumpWidget(Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: Focus( focusNode: focusNode, child: SizedBox( width: 100, height: 100, child: InkWell( overlayColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) { if (states.contains(MaterialState.hovered)) { return const Color(0xff00ff00); } if (states.contains(MaterialState.focused)) { return const Color(0xff0000ff); } if (states.contains(MaterialState.pressed)) { return splashColor; } return const Color(0xffbadbad); // Shouldn't happen. }), onTap: () { }, onLongPress: () { }, onHover: (bool hover) { }, ), ), ), ), ), )); await tester.pumpAndSettle(); final TestGesture gesture = await tester.startGesture(tester.getRect(find.byType(InkWell)).center); await tester.pump(const Duration(milliseconds: 200)); // unconfirmed splash is well underway final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paints..circle(x: 50, y: 50, color: splashColor)); await gesture.up(); }); testWidgets('ink response uses radius for focus highlight', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: InkResponse( focusNode: focusNode, radius: 20, focusColor: const Color(0xff0000ff), onTap: () { }, ), ), ), ), ), ); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paintsExactlyCountTimes(#drawCircle, 0)); focusNode.requestFocus(); await tester.pumpAndSettle(); expect(inkFeatures, paints..circle(radius: 20, color: const Color(0xff0000ff))); }); testWidgets('InkWell uses borderRadius for focus highlight', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: InkWell( focusNode: focusNode, borderRadius: BorderRadius.circular(10), focusColor: const Color(0xff0000ff), onTap: () { }, ), ), ), ), ), ); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 0)); focusNode.requestFocus(); await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 1)); expect(inkFeatures, paints..rrect( rrect: RRect.fromLTRBR(350.0, 250.0, 450.0, 350.0, const Radius.circular(10)), color: const Color(0xff0000ff), )); }); testWidgets('InkWell uses borderRadius for hover highlight', (WidgetTester tester) async { await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: MouseRegion( child: InkWell( borderRadius: BorderRadius.circular(10), hoverColor: const Color(0xff00ff00), onTap: () { }, ), ), ), ), ), ), ); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 0)); // Hover the ink well. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); await gesture.addPointer(location: tester.getRect(find.byType(InkWell)).center); await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 1)); expect(inkFeatures, paints..rrect( rrect: RRect.fromLTRBR(350.0, 250.0, 450.0, 350.0, const Radius.circular(10)), color: const Color(0xff00ff00), )); }); testWidgets('InkWell customBorder clips for focus highlight', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: Align( alignment: Alignment.topLeft, child: SizedBox( width: 100, height: 100, child: MouseRegion( child: InkWell( focusNode: focusNode, borderRadius: BorderRadius.circular(10), customBorder: const CircleBorder(), hoverColor: const Color(0xff00ff00), onTap: () { }, ), ), ), ), ), ), ); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paintsExactlyCountTimes(#clipPath, 0)); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 0)); focusNode.requestFocus(); await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#clipPath, 1)); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 1)); // Create a rounded rectangle path with a radius that makes it similar to the custom border circle. const Rect expectedClipRect = Rect.fromLTRB(0, 0, 100, 100); final Path expectedClipPath = Path() ..addRRect(RRect.fromRectAndRadius( expectedClipRect, const Radius.circular(50.0), )); // The ink well custom border path should match the rounded rectangle path. expect( inkFeatures, paints..clipPath(pathMatcher: coversSameAreaAs( expectedClipPath, areaToCompare: expectedClipRect.inflate(20.0), sampleSize: 100, )), ); }); testWidgets('InkWell customBorder clips for hover highlight', (WidgetTester tester) async { await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: Align( alignment: Alignment.topLeft, child: SizedBox( width: 100, height: 100, child: MouseRegion( child: InkWell( borderRadius: BorderRadius.circular(10), customBorder: const CircleBorder(), hoverColor: const Color(0xff00ff00), onTap: () { }, ), ), ), ), ), ), ); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paintsExactlyCountTimes(#clipPath, 0)); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 0)); // Hover the ink well. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); await gesture.addPointer(location: tester.getRect(find.byType(InkWell)).center); await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#clipPath, 1)); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 1)); // Create a rounded rectangle path with a radius that makes it similar to the custom border circle. const Rect expectedClipRect = Rect.fromLTRB(0, 0, 100, 100); final Path expectedClipPath = Path() ..addRRect(RRect.fromRectAndRadius( expectedClipRect, const Radius.circular(50.0), )); // The ink well custom border path should match the rounded rectangle path. expect( inkFeatures, paints..clipPath(pathMatcher: coversSameAreaAs( expectedClipPath, areaToCompare: expectedClipRect.inflate(20.0), sampleSize: 100, )), ); }); testWidgets('InkResponse radius can be updated', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); Widget boilerplate(double radius) { return Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: InkResponse( focusNode: focusNode, radius: radius, focusColor: const Color(0xff0000ff), onTap: () { }, ), ), ), ), ); } await tester.pumpWidget(boilerplate(10)); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paintsExactlyCountTimes(#drawCircle, 0)); focusNode.requestFocus(); await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#drawCircle, 1)); expect(inkFeatures, paints..circle(radius: 10, color: const Color(0xff0000ff))); await tester.pumpWidget(boilerplate(20)); await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#drawCircle, 1)); expect(inkFeatures, paints..circle(radius: 20, color: const Color(0xff0000ff))); }); testWidgets('InkResponse highlightShape can be updated', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); Widget boilerplate(BoxShape shape) { return Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: InkResponse( focusNode: focusNode, highlightShape: shape, borderRadius: BorderRadius.circular(10), focusColor: const Color(0xff0000ff), onTap: () { }, ), ), ), ), ); } await tester.pumpWidget(boilerplate(BoxShape.circle)); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paintsExactlyCountTimes(#drawCircle, 0)); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 0)); focusNode.requestFocus(); await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#drawCircle, 1)); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 0)); await tester.pumpWidget(boilerplate(BoxShape.rectangle)); await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#drawCircle, 0)); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 1)); }); testWidgets('InkWell borderRadius can be updated', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); Widget boilerplate(BorderRadius borderRadius) { return Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: InkWell( focusNode: focusNode, borderRadius: borderRadius, focusColor: const Color(0xff0000ff), onTap: () { }, ), ), ), ), ); } await tester.pumpWidget(boilerplate(BorderRadius.circular(10))); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 0)); focusNode.requestFocus(); await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 1)); expect(inkFeatures, paints..rrect( rrect: RRect.fromLTRBR(350.0, 250.0, 450.0, 350.0, const Radius.circular(10)), color: const Color(0xff0000ff), )); await tester.pumpWidget(boilerplate(BorderRadius.circular(30))); await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#drawRRect, 1)); expect(inkFeatures, paints..rrect( rrect: RRect.fromLTRBR(350.0, 250.0, 450.0, 350.0, const Radius.circular(30)), color: const Color(0xff0000ff), )); }); testWidgets('InkWell customBorder can be updated', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); Widget boilerplate(BorderRadius borderRadius) { return Material( child: Directionality( textDirection: TextDirection.ltr, child: Align( alignment: Alignment.topLeft, child: SizedBox( width: 100, height: 100, child: MouseRegion( child: InkWell( focusNode: focusNode, customBorder: RoundedRectangleBorder(borderRadius: borderRadius), hoverColor: const Color(0xff00ff00), onTap: () { }, ), ), ), ), ), ); } await tester.pumpWidget(boilerplate(BorderRadius.circular(20))); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paintsExactlyCountTimes(#clipPath, 0)); focusNode.requestFocus(); await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#clipPath, 1)); const Rect expectedClipRect = Rect.fromLTRB(0, 0, 100, 100); Path expectedClipPath = Path() ..addRRect(RRect.fromRectAndRadius( expectedClipRect, const Radius.circular(20), )); expect( inkFeatures, paints..clipPath(pathMatcher: coversSameAreaAs( expectedClipPath, areaToCompare: expectedClipRect.inflate(20.0), sampleSize: 100, )), ); await tester.pumpWidget(boilerplate(BorderRadius.circular(40))); await tester.pumpAndSettle(); expectedClipPath = Path() ..addRRect(RRect.fromRectAndRadius( expectedClipRect, const Radius.circular(40), )); expect( inkFeatures, paints..clipPath(pathMatcher: coversSameAreaAs( expectedClipPath, areaToCompare: expectedClipRect.inflate(20.0), sampleSize: 100, )), ); }); testWidgets("ink response doesn't change color on focus when on touch device", (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); await tester.pumpWidget(Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: InkWell( focusNode: focusNode, hoverColor: const Color(0xff00ff00), splashColor: const Color(0xffff0000), focusColor: const Color(0xff0000ff), highlightColor: const Color(0xf00fffff), onTap: () { }, onLongPress: () { }, onHover: (bool hover) { }, ), ), ), ), )); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paintsExactlyCountTimes(#drawRect, 0)); focusNode.requestFocus(); await tester.pumpAndSettle(); expect(inkFeatures, paintsExactlyCountTimes(#drawRect, 0)); }); testWidgets('InkWell.mouseCursor changes cursor on hover', (WidgetTester tester) async { final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); await gesture.addPointer(location: const Offset(1, 1)); // Test argument works await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: MouseRegion( cursor: SystemMouseCursors.forbidden, child: InkWell( mouseCursor: SystemMouseCursors.click, onTap: () {}, ), ), ), ), ); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); // Test default of InkWell() await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: MouseRegion( cursor: SystemMouseCursors.forbidden, child: InkWell( onTap: () {}, ), ), ), ), ); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); // Test disabled await tester.pumpWidget( const Material( child: Directionality( textDirection: TextDirection.ltr, child: MouseRegion( cursor: SystemMouseCursors.forbidden, child: InkWell(), ), ), ), ); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); // Test default of InkResponse() await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: MouseRegion( cursor: SystemMouseCursors.forbidden, child: InkResponse( onTap: () {}, ), ), ), ), ); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); // Test disabled await tester.pumpWidget( const Material( child: Directionality( textDirection: TextDirection.ltr, child: MouseRegion( cursor: SystemMouseCursors.forbidden, child: InkResponse(), ), ), ), ); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); }); group('feedback', () { late FeedbackTester feedback; setUp(() { feedback = FeedbackTester(); }); tearDown(() { feedback.dispose(); }); testWidgets('enabled (default)', (WidgetTester tester) async { await tester.pumpWidget(Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: InkWell( onTap: () { }, onLongPress: () { }, ), ), ), )); await tester.tap(find.byType(InkWell), pointer: 1); await tester.pump(const Duration(seconds: 1)); expect(feedback.clickSoundCount, 1); expect(feedback.hapticCount, 0); await tester.tap(find.byType(InkWell), pointer: 1); await tester.pump(const Duration(seconds: 1)); expect(feedback.clickSoundCount, 2); expect(feedback.hapticCount, 0); await tester.longPress(find.byType(InkWell), pointer: 1); await tester.pump(const Duration(seconds: 1)); expect(feedback.clickSoundCount, 2); expect(feedback.hapticCount, 1); }); testWidgets('disabled', (WidgetTester tester) async { await tester.pumpWidget(Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: InkWell( onTap: () { }, onLongPress: () { }, enableFeedback: false, ), ), ), )); await tester.tap(find.byType(InkWell), pointer: 1); await tester.pump(const Duration(seconds: 1)); expect(feedback.clickSoundCount, 0); expect(feedback.hapticCount, 0); await tester.longPress(find.byType(InkWell), pointer: 1); await tester.pump(const Duration(seconds: 1)); expect(feedback.clickSoundCount, 0); expect(feedback.hapticCount, 0); }); }); testWidgets('splashing survives scrolling when keep-alive is enabled', (WidgetTester tester) async { Future<void> runTest(bool keepAlive) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Material( child: CompositedTransformFollower( // forces a layer, which makes the paints easier to separate out link: LayerLink(), child: ListView( addAutomaticKeepAlives: keepAlive, dragStartBehavior: DragStartBehavior.down, children: <Widget>[ SizedBox(height: 500.0, child: InkWell(onTap: () {}, child: const Placeholder())), const SizedBox(height: 500.0), const SizedBox(height: 500.0), ], ), ), ), ), ); expect(tester.renderObject<RenderProxyBox>(find.byType(PhysicalModel)).child, isNot(paints..circle())); await tester.tap(find.byType(InkWell)); await tester.pump(); await tester.pump(const Duration(milliseconds: 10)); expect(tester.renderObject<RenderProxyBox>(find.byType(PhysicalModel)).child, paints..circle()); await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0)); await tester.pump(const Duration(milliseconds: 10)); await tester.drag(find.byType(ListView), const Offset(0.0, 1000.0)); await tester.pump(const Duration(milliseconds: 10)); expect( tester.renderObject<RenderProxyBox>(find.byType(PhysicalModel)).child, keepAlive ? (paints..circle()) : isNot(paints..circle()), ); } await runTest(true); await runTest(false); }); testWidgets('excludeFromSemantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Material( child: InkWell( onTap: () { }, child: const Text('Button'), ), ), )); expect(semantics, includesNodeWith(label: 'Button', actions: <SemanticsAction>[SemanticsAction.tap])); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Material( child: InkWell( onTap: () { }, excludeFromSemantics: true, child: const Text('Button'), ), ), )); expect(semantics, isNot(includesNodeWith(label: 'Button', actions: <SemanticsAction>[SemanticsAction.tap]))); semantics.dispose(); }); testWidgets("ink response doesn't focus when disabled", (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); final GlobalKey childKey = GlobalKey(); await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: InkWell( autofocus: true, onTap: () {}, onLongPress: () {}, onHover: (bool hover) {}, focusNode: focusNode, child: Container(key: childKey), ), ), ), ); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: InkWell( focusNode: focusNode, child: Container(key: childKey), ), ), ), ); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); }); testWidgets('ink response accepts focus when disabled in directional navigation mode', (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); final GlobalKey childKey = GlobalKey(); await tester.pumpWidget( Material( child: MediaQuery( data: const MediaQueryData( navigationMode: NavigationMode.directional, ), child: Directionality( textDirection: TextDirection.ltr, child: InkWell( autofocus: true, onTap: () {}, onLongPress: () {}, onHover: (bool hover) {}, focusNode: focusNode, child: Container(key: childKey), ), ), ), ), ); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); await tester.pumpWidget( Material( child: MediaQuery( data: const MediaQueryData( navigationMode: NavigationMode.directional, ), child: Directionality( textDirection: TextDirection.ltr, child: InkWell( focusNode: focusNode, child: Container(key: childKey), ), ), ), ), ); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); }); testWidgets("ink response doesn't hover when disabled", (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus'); final GlobalKey childKey = GlobalKey(); bool hovering = false; await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: SizedBox( width: 100, height: 100, child: InkWell( autofocus: true, onTap: () {}, 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( Material( child: Directionality( textDirection: TextDirection.ltr, child: SizedBox( width: 100, height: 100, child: InkWell( focusNode: focusNode, onHover: (bool value) { hovering = value; }, child: SizedBox(key: childKey), ), ), ), ), ); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); }); testWidgets('When ink wells are nested, only the inner one is triggered by tap splash', (WidgetTester tester) async { final GlobalKey middleKey = GlobalKey(); final GlobalKey innerKey = GlobalKey(); Widget paddedInkWell({Key? key, Widget? child}) { return InkWell( key: key, onTap: () {}, child: Padding( padding: const EdgeInsets.all(50), child: child, ), ); } await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: paddedInkWell( child: paddedInkWell( key: middleKey, child: paddedInkWell( key: innerKey, child: const SizedBox(width: 50, height: 50), ), ), ), ), ), ), ); final MaterialInkController material = Material.of(tester.element(find.byKey(innerKey))); // Press final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(innerKey)), pointer: 1); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); // Up await gesture.up(); await tester.pumpAndSettle(); expect(material, paintsNothing); // Press again await gesture.down(tester.getCenter(find.byKey(innerKey))); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); // Cancel await gesture.cancel(); await tester.pumpAndSettle(); expect(material, paintsNothing); // Press again await gesture.down(tester.getCenter(find.byKey(innerKey))); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); // Use a second pointer to press final TestGesture gesture2 = await tester.startGesture(tester.getCenter(find.byKey(innerKey)), pointer: 2); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); await gesture2.up(); }); testWidgets('Reparenting parent should allow both inkwells to show splash afterwards', (WidgetTester tester) async { final GlobalKey middleKey = GlobalKey(); final GlobalKey innerKey = GlobalKey(); Widget paddedInkWell({Key? key, Widget? child}) { return InkWell( key: key, onTap: () {}, child: Padding( padding: const EdgeInsets.all(50), child: child, ), ); } await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: Align( alignment: Alignment.topLeft, child: SizedBox( width: 200, height: 100, child: Row( children: <Widget>[ paddedInkWell( key: middleKey, child: paddedInkWell( key: innerKey, ), ), const SizedBox(), ], ), ), ), ), ), ); final MaterialInkController material = Material.of(tester.element(find.byKey(innerKey))); // Press final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(innerKey)), pointer: 1); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); // Reparent parent await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: Align( alignment: Alignment.topLeft, child: SizedBox( width: 200, height: 100, child: Row( children: <Widget>[ paddedInkWell( key: innerKey, ), paddedInkWell( key: middleKey, ), ], ), ), ), ), ), ); // Up await gesture.up(); await tester.pumpAndSettle(); expect(material, paintsNothing); // Press the previous parent await gesture.down(tester.getCenter(find.byKey(middleKey))); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); // Use a second pointer to press the previous child await tester.startGesture(tester.getCenter(find.byKey(innerKey)), pointer: 2); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 2)); }); testWidgets('Parent inkwell does not block child inkwells from splashes', (WidgetTester tester) async { final GlobalKey middleKey = GlobalKey(); final GlobalKey innerKey = GlobalKey(); Widget paddedInkWell({Key? key, Widget? child}) { return InkWell( key: key, onTap: () {}, child: Padding( padding: const EdgeInsets.all(50), child: child, ), ); } await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: paddedInkWell( child: paddedInkWell( key: middleKey, child: paddedInkWell( key: innerKey, child: const SizedBox(width: 50, height: 50), ), ), ), ), ), ), ); final MaterialInkController material = Material.of(tester.element(find.byKey(innerKey))); // Press middle await tester.startGesture(tester.getTopLeft(find.byKey(middleKey)) + const Offset(1, 1), pointer: 1); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); // Press inner await tester.startGesture(tester.getCenter(find.byKey(innerKey)), pointer: 2); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 2)); }); testWidgets('Parent inkwell can count the number of pressed children to prevent splash', (WidgetTester tester) async { final GlobalKey parentKey = GlobalKey(); final GlobalKey leftKey = GlobalKey(); final GlobalKey rightKey = GlobalKey(); await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: InkWell( key: parentKey, onTap: () {}, child: Center( child: SizedBox( width: 100, height: 50, child: Row( children: <Widget>[ SizedBox( width: 50, height: 50, child: InkWell( key: leftKey, onTap: () {}, ), ), SizedBox( width: 50, height: 50, child: InkWell( key: rightKey, onTap: () {}, ), ), ], ), ), ), ), ), ), ), ), ); final MaterialInkController material = Material.of(tester.element(find.byKey(leftKey))); final Offset parentPosition = tester.getTopLeft(find.byKey(parentKey)) + const Offset(1, 1); // Press left child final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.byKey(leftKey)), pointer: 1); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); // Press right child final TestGesture gesture2 = await tester.startGesture(tester.getCenter(find.byKey(rightKey)), pointer: 2); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 2)); // Press parent final TestGesture gesture3 = await tester.startGesture(parentPosition, pointer: 3); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 2)); await gesture3.up(); // Release left child await gesture1.up(); await tester.pumpAndSettle(); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); // Press parent await gesture3.down(parentPosition); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); await gesture3.up(); // Release right child await gesture2.up(); await tester.pumpAndSettle(); expect(material, paintsExactlyCountTimes(#drawCircle, 0)); // Press parent await gesture3.down(parentPosition); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); await gesture3.up(); }); testWidgets('When ink wells are reparented, the old parent can display splash while the new parent can not', (WidgetTester tester) async { final GlobalKey innerKey = GlobalKey(); final GlobalKey leftKey = GlobalKey(); final GlobalKey rightKey = GlobalKey(); Widget doubleInkWellRow({ required double leftWidth, required double rightWidth, Widget? leftChild, Widget? rightChild, }) { return Material( child: Directionality( textDirection: TextDirection.ltr, child: Align( alignment: Alignment.topLeft, child: SizedBox( width: leftWidth+rightWidth, height: 100, child: Row( children: <Widget>[ SizedBox( width: leftWidth, height: 100, child: InkWell( key: leftKey, onTap: () {}, child: Center( child: SizedBox( width: leftWidth, height: 50, child: leftChild, ), ), ), ), SizedBox( width: rightWidth, height: 100, child: InkWell( key: rightKey, onTap: () {}, child: Center( child: SizedBox( width: leftWidth, height: 50, child: rightChild, ), ), ), ), ], ), ), ), ), ); } await tester.pumpWidget( doubleInkWellRow( leftWidth: 110, rightWidth: 90, leftChild: InkWell( key: innerKey, onTap: () {}, ), ), ); final MaterialInkController material = Material.of(tester.element(find.byKey(innerKey))); // Press inner final TestGesture gesture = await tester.startGesture(const Offset(100, 50), pointer: 1); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); // Switch side await tester.pumpWidget( doubleInkWellRow( leftWidth: 90, rightWidth: 110, rightChild: InkWell( key: innerKey, onTap: () {}, ), ), ); expect(material, paintsExactlyCountTimes(#drawCircle, 0)); // A second pointer presses inner final TestGesture gesture2 = await tester.startGesture(const Offset(100, 50), pointer: 2); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); await gesture.up(); await gesture2.up(); await tester.pumpAndSettle(); // Press inner await gesture.down(const Offset(100, 50)); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); // Press left await gesture2.down(const Offset(50, 50)); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 2)); await gesture.up(); await gesture2.up(); }); testWidgets("Ink wells's splash starts before tap is confirmed and disappear after tap is canceled", (WidgetTester tester) async { final GlobalKey innerKey = GlobalKey(); await tester.pumpWidget( Material( child: Directionality( textDirection: TextDirection.ltr, child: GestureDetector( onHorizontalDragStart: (_) {}, child: Center( child: SizedBox( width: 100, height: 100, child: InkWell( onTap: () {}, child: Center( child: SizedBox( width: 50, height: 50, child: InkWell( key: innerKey, onTap: () {}, ), ), ), ), ), ), ), ), ), ); final MaterialInkController material = Material.of(tester.element(find.byKey(innerKey))); // Press final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(innerKey)), pointer: 1); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); // Scroll upward await gesture.moveBy(const Offset(0, -100)); await tester.pumpAndSettle(); expect(material, paintsNothing); // Up await gesture.up(); await tester.pumpAndSettle(); expect(material, paintsNothing); // Press again await gesture.down(tester.getCenter(find.byKey(innerKey))); await tester.pump(const Duration(milliseconds: 200)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); }); testWidgets('disabled and hovered inkwell responds to mouse-exit', (WidgetTester tester) async { int onHoverCount = 0; late bool hover; Widget buildFrame({ required bool enabled }) { return Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: InkWell( onTap: enabled ? () { } : null, onHover: (bool value) { onHoverCount += 1; hover = value; }, ), ), ), ), ); } 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(InkWell))); 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 InkWell 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(InkWell))); await tester.pumpAndSettle(); // We no longer see hover events because the InkWell 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 InkWell 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(InkWell)) - const Offset(1, 1)); await tester.pumpAndSettle(); // Moving the mouse a little within the InkWell doesn't change anything. expect(onHoverCount, 2); expect(hover, false); }); testWidgets('hovered ink well draws a transparent highlight when disabled', (WidgetTester tester) async { Widget buildFrame({ required bool enabled }) { return Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: InkWell( onTap: enabled ? () { } : null, onHover: (bool value) { }, hoverColor: const Color(0xff00ff00), ), ), ), ), ); } await tester.pumpWidget(buildFrame(enabled: true)); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); // Hover the enabled InkWell. await gesture.moveTo(tester.getCenter(find.byType(InkWell))); await tester.pumpAndSettle(); expect( find.byType(Material), paints ..rect( color: const Color(0xff00ff00), rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), ) ); // Disable the hovered InkWell. await tester.pumpWidget(buildFrame(enabled: false)); await tester.pumpAndSettle(); expect( find.byType(Material), paints ..rect( color: const Color(0x0000ff00), rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), ) ); }); testWidgets('Changing InkWell.enabled should not trigger TextButton setState()', (WidgetTester tester) async { Widget buildFrame({ required bool enabled }) { return Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: TextButton( onPressed: enabled ? () { } : null, child: const Text('button'), ), ), ), ); } await tester.pumpWidget(buildFrame(enabled: false)); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(tester.getCenter(find.byType(TextButton))); await tester.pumpAndSettle(); // Rebuilding the button with enabled:true causes InkWell.didUpdateWidget() // to be called per the change in its enabled flag. If onHover() was called, // this test would crash. await tester.pumpWidget(buildFrame(enabled: true)); await tester.pumpAndSettle(); // Rebuild again, with enabled:false await gesture.moveBy(const Offset(1, 1)); await tester.pumpWidget(buildFrame(enabled: false)); await tester.pumpAndSettle(); }); testWidgets('InkWell does not attach semantics handler for onTap if it was not provided an onTap handler', (WidgetTester tester) async { await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: InkWell( onLongPress: () { }, child: const Text('Foo'), ), ), ), )); expect(tester.getSemantics(find.bySemanticsLabel('Foo')), matchesSemantics( label: 'Foo', hasLongPressAction: true, isFocusable: true, textDirection: TextDirection.ltr, )); // Add tap handler and confirm addition to semantic actions. await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: InkWell( onLongPress: () { }, onTap: () { }, child: const Text('Foo'), ), ), ), )); expect(tester.getSemantics(find.bySemanticsLabel('Foo')), matchesSemantics( label: 'Foo', hasTapAction: true, hasLongPressAction: true, isFocusable: true, textDirection: TextDirection.ltr, )); }); testWidgets('InkWell highlight should not survive after [onTapDown, onDoubleTap] sequence', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: InkWell( onTap: () { log.add('tap'); }, onDoubleTap: () { log.add('double-tap'); }, onTapDown: (TapDownDetails details) { log.add('tap-down'); }, onTapCancel: () { log.add('tap-cancel'); }, ), ), ), )); final Offset taplocation = tester.getRect(find.byType(InkWell)).center; final TestGesture gesture = await tester.startGesture(taplocation); await tester.pump(const Duration(milliseconds: 100)); expect(log, equals(<String>['tap-down'])); await gesture.up(); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.byType(InkWell)); await tester.pump(const Duration(milliseconds: 100)); expect(log, equals(<String>['tap-down', 'double-tap'])); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paintsExactlyCountTimes(#drawRect, 0)); }); testWidgets('InkWell splash should not survive after [onTapDown, onTapDown, onTapCancel, onDoubleTap] sequence', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: InkWell( onTap: () { log.add('tap'); }, onDoubleTap: () { log.add('double-tap'); }, onTapDown: (TapDownDetails details) { log.add('tap-down'); }, onTapCancel: () { log.add('tap-cancel'); }, ), ), ), )); final Offset tapLocation = tester.getRect(find.byType(InkWell)).center; final TestGesture gesture1 = await tester.startGesture(tapLocation); await tester.pump(const Duration(milliseconds: 100)); expect(log, equals(<String>['tap-down'])); await gesture1.up(); await tester.pump(const Duration(milliseconds: 100)); final TestGesture gesture2 = await tester.startGesture(tapLocation); await tester.pump(const Duration(milliseconds: 100)); expect(log, equals(<String>['tap-down', 'tap-down'])); await gesture2.up(); await tester.pump(const Duration(milliseconds: 100)); expect(log, equals(<String>['tap-down', 'tap-down', 'tap-cancel', 'double-tap'])); await tester.pumpAndSettle(); final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); expect(inkFeatures, paintsExactlyCountTimes(#drawCircle, 0)); }); testWidgets('InkWell dispose statesController', (WidgetTester tester) async { int tapCount = 0; Widget buildFrame(MaterialStatesController? statesController) { return MaterialApp( home: Scaffold( body: Center( child: InkWell( statesController: statesController, onTap: () { tapCount += 1; }, child: const Text('inkwell'), ), ), ), ); } final MaterialStatesController controller = MaterialStatesController(); int pressedCount = 0; controller.addListener(() { if (controller.value.contains(MaterialState.pressed)) { pressedCount += 1; } }); await tester.pumpWidget(buildFrame(controller)); await tester.tap(find.byType(InkWell)); await tester.pumpAndSettle(); expect(tapCount, 1); expect(pressedCount, 1); await tester.pumpWidget(buildFrame(null)); await tester.tap(find.byType(InkWell)); await tester.pumpAndSettle(); expect(tapCount, 2); expect(pressedCount, 1); await tester.pumpWidget(buildFrame(controller)); await tester.tap(find.byType(InkWell)); await tester.pumpAndSettle(); expect(tapCount, 3); expect(pressedCount, 2); }); testWidgets('ink well overlayColor opacity fades from 0xff when hover ends', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/110266 await tester.pumpWidget(Material( child: Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( width: 100, height: 100, child: InkWell( overlayColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) { if (states.contains(MaterialState.hovered)) { return const Color(0xff00ff00); } return null; }), onTap: () { }, onLongPress: () { }, onHover: (bool hover) { }, ), ), ), ), )); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(tester.getCenter(find.byType(SizedBox))); await tester.pumpAndSettle(); await gesture.moveTo(const Offset(10, 10)); // fade out the overlay await tester.pump(); // trigger the fade out animation final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); // Fadeout begins with the MaterialStates.hovered overlay color expect(inkFeatures, paints..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0xff00ff00))); // 50ms fadeout is 50% complete, overlay color alpha goes from 0xff to 0x80 await tester.pump(const Duration(milliseconds: 25)); expect(inkFeatures, paints..rect(rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), color: const Color(0x8000ff00))); }); }