// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('TapRegionSurface detects outside tap down events', (WidgetTester tester) async { final tappedOutside = {}; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( children: [ const Text('Outside Surface'), TapRegionSurface( child: Row( children: [ const Text('Outside'), TapRegion( onTapOutside: (PointerEvent event) { tappedOutside.add('No Group'); }, child: const Text('No Group'), ), TapRegion( groupId: 1, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 A'); }, child: const Text('Group 1 A'), ), TapRegion( groupId: 1, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 B'); }, child: const Text('Group 1 B'), ), ], ), ), ], ), ), ); await tester.pump(); Future click(Finder finder) async { final TestGesture gesture = await tester.startGesture( tester.getCenter(finder), kind: PointerDeviceKind.mouse, ); // We intentionally don't call up() here because we're testing the down event. await gesture.cancel(); await gesture.removePointer(); } expect(tappedOutside, isEmpty); await click(find.text('No Group')); expect(tappedOutside, unorderedEquals({'Group 1 A', 'Group 1 B'})); tappedOutside.clear(); await click(find.text('Group 1 A')); expect(tappedOutside, equals({'No Group'})); tappedOutside.clear(); await click(find.text('Group 1 B')); expect(tappedOutside, equals({'No Group'})); tappedOutside.clear(); await click(find.text('Outside')); expect(tappedOutside, unorderedEquals({'No Group', 'Group 1 A', 'Group 1 B'})); tappedOutside.clear(); await click(find.text('Outside Surface')); expect(tappedOutside, isEmpty); }); testWidgets('TapRegionSurface detects outside tap up events', (WidgetTester tester) async { final tappedOutside = {}; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: TapRegionSurface( child: Row( children: [ const Text('Outside'), TapRegion( onTapUpOutside: (PointerEvent event) { tappedOutside.add('No Group'); }, child: const Text('No Group'), ), TapRegion( groupId: 1, onTapUpOutside: (PointerEvent event) { tappedOutside.add('Group 1 A'); }, child: const Text('Group 1 A'), ), TapRegion( groupId: 1, onTapUpOutside: (PointerEvent event) { tappedOutside.add('Group 1 B'); }, child: const Text('Group 1 B'), ), ], ), ), ), ); await tester.pump(); Future click(Finder finder) async { final TestGesture gesture = await tester.startGesture( tester.getCenter(finder), kind: PointerDeviceKind.mouse, ); expect(tappedOutside, isEmpty); // No callbacks should been called before up event. await gesture.up(); await gesture.removePointer(); } expect(tappedOutside, isEmpty); await click(find.text('No Group')); expect(tappedOutside, unorderedEquals({'Group 1 A', 'Group 1 B'})); tappedOutside.clear(); await click(find.text('Group 1 A')); expect(tappedOutside, equals({'No Group'})); tappedOutside.clear(); await click(find.text('Group 1 B')); expect(tappedOutside, equals({'No Group'})); tappedOutside.clear(); await click(find.text('Outside')); expect(tappedOutside, unorderedEquals({'No Group', 'Group 1 A', 'Group 1 B'})); tappedOutside.clear(); }); testWidgets('TapRegionSurface consumes outside taps when asked', (WidgetTester tester) async { final tappedOutside = {}; var propagatedTaps = 0; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( children: [ const Text('Outside Surface'), TapRegionSurface( child: Row( children: [ GestureDetector( onTap: () { propagatedTaps += 1; }, child: const Text('Outside'), ), TapRegion( consumeOutsideTaps: true, onTapOutside: (PointerEvent event) { tappedOutside.add('No Group'); }, child: const Text('No Group'), ), TapRegion( groupId: 1, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 A'); }, child: const Text('Group 1 A'), ), TapRegion( groupId: 1, consumeOutsideTaps: true, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 B'); }, child: const Text('Group 1 B'), ), ], ), ), ], ), ), ); await tester.pump(); Future click(Finder finder) async { final TestGesture gesture = await tester.startGesture( tester.getCenter(finder), kind: PointerDeviceKind.mouse, ); await gesture.up(); await gesture.removePointer(); } expect(tappedOutside, isEmpty); expect(propagatedTaps, equals(0)); await click(find.text('No Group')); expect(tappedOutside, unorderedEquals({'Group 1 A', 'Group 1 B'})); expect(propagatedTaps, equals(0)); tappedOutside.clear(); await click(find.text('Group 1 A')); expect(tappedOutside, equals({'No Group'})); expect(propagatedTaps, equals(0)); tappedOutside.clear(); await click(find.text('Group 1 B')); expect(tappedOutside, equals({'No Group'})); expect(propagatedTaps, equals(0)); tappedOutside.clear(); await click(find.text('Outside')); expect(tappedOutside, unorderedEquals({'No Group', 'Group 1 A', 'Group 1 B'})); expect(propagatedTaps, equals(0)); tappedOutside.clear(); await click(find.text('Outside Surface')); expect(tappedOutside, isEmpty); }); testWidgets('TapRegionSurface detects inside tap down events', (WidgetTester tester) async { final tappedInside = {}; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( children: [ const Text('Outside Surface'), TapRegionSurface( child: Row( children: [ const Text('Outside'), TapRegion( onTapInside: (PointerEvent event) { tappedInside.add('No Group'); }, child: const Text('No Group'), ), TapRegion( groupId: 1, onTapInside: (PointerEvent event) { tappedInside.add('Group 1 A'); }, child: const Text('Group 1 A'), ), TapRegion( groupId: 1, onTapInside: (PointerEvent event) { tappedInside.add('Group 1 B'); }, child: const Text('Group 1 B'), ), ], ), ), ], ), ), ); await tester.pump(); Future click(Finder finder) async { final TestGesture gesture = await tester.startGesture( tester.getCenter(finder), kind: PointerDeviceKind.mouse, ); // We intentionally don't call up() here because we're testing the down event. await gesture.cancel(); await gesture.removePointer(); } expect(tappedInside, isEmpty); await click(find.text('No Group')); expect(tappedInside, unorderedEquals({'No Group'})); tappedInside.clear(); await click(find.text('Group 1 A')); expect(tappedInside, equals({'Group 1 A', 'Group 1 B'})); tappedInside.clear(); await click(find.text('Group 1 B')); expect(tappedInside, equals({'Group 1 A', 'Group 1 B'})); tappedInside.clear(); await click(find.text('Outside')); expect(tappedInside, isEmpty); tappedInside.clear(); await click(find.text('Outside Surface')); expect(tappedInside, isEmpty); }); testWidgets('TapRegionSurface detects inside tap up events', (WidgetTester tester) async { final tappedInside = {}; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: TapRegionSurface( child: Row( children: [ const Text('Outside'), TapRegion( onTapUpInside: (PointerEvent event) { tappedInside.add('No Group'); }, child: const Text('No Group'), ), TapRegion( groupId: 1, onTapUpInside: (PointerEvent event) { tappedInside.add('Group 1 A'); }, child: const Text('Group 1 A'), ), TapRegion( groupId: 1, onTapUpInside: (PointerEvent event) { tappedInside.add('Group 1 B'); }, child: const Text('Group 1 B'), ), ], ), ), ), ); await tester.pump(); Future click(Finder finder) async { final TestGesture gesture = await tester.startGesture( tester.getCenter(finder), kind: PointerDeviceKind.mouse, ); expect(tappedInside, isEmpty); // No callbacks should been called before up event. await gesture.up(); await gesture.removePointer(); } expect(tappedInside, isEmpty); await click(find.text('No Group')); expect(tappedInside, unorderedEquals({'No Group'})); tappedInside.clear(); await click(find.text('Group 1 A')); expect(tappedInside, equals({'Group 1 A', 'Group 1 B'})); tappedInside.clear(); await click(find.text('Group 1 B')); expect(tappedInside, equals({'Group 1 A', 'Group 1 B'})); tappedInside.clear(); await click(find.text('Outside')); expect(tappedInside, isEmpty); tappedInside.clear(); }); testWidgets('TapRegionSurface detects inside taps correctly with behavior', ( WidgetTester tester, ) async { final tappedInside = {}; const noGroupKey = ValueKey('No Group'); const group1AKey = ValueKey('Group 1 A'); const group1BKey = ValueKey('Group 1 B'); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( children: [ const Text('Outside Surface'), TapRegionSurface( child: Row( children: [ ConstrainedBox( constraints: const BoxConstraints.tightFor(width: 100, height: 100), child: TapRegion( onTapInside: (PointerEvent event) { tappedInside.add(noGroupKey.value); }, child: const Stack(key: noGroupKey), ), ), ConstrainedBox( constraints: const BoxConstraints.tightFor(width: 100, height: 100), child: TapRegion( groupId: 1, behavior: HitTestBehavior.opaque, onTapInside: (PointerEvent event) { tappedInside.add(group1AKey.value); }, child: const Stack(key: group1AKey), ), ), ConstrainedBox( constraints: const BoxConstraints.tightFor(width: 100, height: 100), child: TapRegion( groupId: 1, behavior: HitTestBehavior.translucent, onTapInside: (PointerEvent event) { tappedInside.add(group1BKey.value); }, child: const Stack(key: group1BKey), ), ), ], ), ), ], ), ), ); await tester.pump(); Future click(Finder finder) async { final TestGesture gesture = await tester.startGesture( tester.getCenter(finder), kind: PointerDeviceKind.mouse, ); await gesture.up(); await gesture.removePointer(); } expect(tappedInside, isEmpty); await click(find.byKey(noGroupKey)); expect(tappedInside, isEmpty); // No hittable children, so no hit. await click(find.byKey(group1AKey)); // No hittable children, but set to opaque, so it hits, triggering the // group. expect(tappedInside, equals({'Group 1 A', 'Group 1 B'})); tappedInside.clear(); await click(find.byKey(group1BKey)); expect(tappedInside, isEmpty); // No hittable children while translucent, so no hit. tappedInside.clear(); }); testWidgets('Setting the group updates the registration', (WidgetTester tester) async { final tappedOutside = {}; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: TapRegionSurface( child: Row( children: [ const Text('Outside'), TapRegion( groupId: 1, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 A'); }, child: const Text('Group 1 A'), ), TapRegion( groupId: 1, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 B'); }, child: const Text('Group 1 B'), ), ], ), ), ), ); await tester.pump(); Future click(Finder finder) async { final TestGesture gesture = await tester.startGesture( tester.getCenter(finder), kind: PointerDeviceKind.mouse, ); await gesture.up(); await gesture.removePointer(); } expect(tappedOutside, isEmpty); await click(find.text('Group 1 A')); expect(tappedOutside, isEmpty); await click(find.text('Group 1 B')); expect(tappedOutside, isEmpty); await click(find.text('Outside')); expect(tappedOutside, equals(['Group 1 A', 'Group 1 B'])); tappedOutside.clear(); // Now change out the groups. await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: TapRegionSurface( child: Row( children: [ const Text('Outside'), TapRegion( groupId: 1, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 A'); }, child: const Text('Group 1 A'), ), TapRegion( groupId: 2, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 2 A'); }, child: const Text('Group 2 A'), ), ], ), ), ), ); await click(find.text('Group 1 A')); expect(tappedOutside, equals(['Group 2 A'])); tappedOutside.clear(); await click(find.text('Group 2 A')); expect(tappedOutside, equals(['Group 1 A'])); tappedOutside.clear(); await click(find.text('Outside')); expect(tappedOutside, equals(['Group 1 A', 'Group 2 A'])); tappedOutside.clear(); }); testWidgets('TapRegionSurface detects outside right click', (WidgetTester tester) async { final tappedOutside = {}; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( children: [ const Text('Outside Surface'), TapRegionSurface( child: Row( children: [ const Text('Outside'), TapRegion( onTapOutside: (PointerEvent event) { tappedOutside.add('No Group'); }, child: const Text('No Group'), ), TapRegion( groupId: 1, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 A'); }, child: const Text('Group 1 A'), ), TapRegion( groupId: 1, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 B'); }, child: const Text('Group 1 B'), ), ], ), ), ], ), ), ); await tester.pump(); Future click(Finder finder) async { final TestGesture gesture = await tester.startGesture( tester.getCenter(finder), kind: PointerDeviceKind.mouse, buttons: kSecondaryButton, ); await gesture.up(); await gesture.removePointer(); } expect(tappedOutside, isEmpty); await click(find.text('No Group')); expect(tappedOutside, unorderedEquals({'Group 1 A', 'Group 1 B'})); tappedOutside.clear(); await click(find.text('Group 1 A')); expect(tappedOutside, equals({'No Group'})); tappedOutside.clear(); await click(find.text('Group 1 B')); expect(tappedOutside, equals({'No Group'})); tappedOutside.clear(); await click(find.text('Outside')); expect(tappedOutside, unorderedEquals({'No Group', 'Group 1 A', 'Group 1 B'})); tappedOutside.clear(); await click(find.text('Outside Surface')); expect(tappedOutside, isEmpty); }); testWidgets('TapRegionSurface detects outside middle click', (WidgetTester tester) async { final tappedOutside = {}; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( children: [ const Text('Outside Surface'), TapRegionSurface( child: Row( children: [ const Text('Outside'), TapRegion( onTapOutside: (PointerEvent event) { tappedOutside.add('No Group'); }, child: const Text('No Group'), ), TapRegion( groupId: 1, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 A'); }, child: const Text('Group 1 A'), ), TapRegion( groupId: 1, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 B'); }, child: const Text('Group 1 B'), ), ], ), ), ], ), ), ); await tester.pump(); Future click(Finder finder) async { final TestGesture gesture = await tester.startGesture( tester.getCenter(finder), kind: PointerDeviceKind.mouse, buttons: kTertiaryButton, ); await gesture.up(); await gesture.removePointer(); } expect(tappedOutside, isEmpty); await click(find.text('No Group')); expect(tappedOutside, unorderedEquals({'Group 1 A', 'Group 1 B'})); tappedOutside.clear(); await click(find.text('Group 1 A')); expect(tappedOutside, equals({'No Group'})); tappedOutside.clear(); await click(find.text('Group 1 B')); expect(tappedOutside, equals({'No Group'})); tappedOutside.clear(); await click(find.text('Outside')); expect(tappedOutside, unorderedEquals({'No Group', 'Group 1 A', 'Group 1 B'})); tappedOutside.clear(); await click(find.text('Outside Surface')); expect(tappedOutside, isEmpty); }); testWidgets('TapRegionSurface consumes outside right click when asked', ( WidgetTester tester, ) async { final tappedOutside = {}; var propagatedTaps = 0; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( children: [ const Text('Outside Surface'), TapRegionSurface( child: Row( children: [ GestureDetector( onTap: () { propagatedTaps += 1; }, child: const Text('Outside'), ), TapRegion( consumeOutsideTaps: true, onTapOutside: (PointerEvent event) { tappedOutside.add('No Group'); }, child: const Text('No Group'), ), TapRegion( groupId: 1, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 A'); }, child: const Text('Group 1 A'), ), TapRegion( groupId: 1, consumeOutsideTaps: true, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 B'); }, child: const Text('Group 1 B'), ), ], ), ), ], ), ), ); await tester.pump(); Future click(Finder finder) async { final TestGesture gesture = await tester.startGesture( tester.getCenter(finder), kind: PointerDeviceKind.mouse, buttons: kSecondaryButton, ); await gesture.up(); await gesture.removePointer(); } expect(tappedOutside, isEmpty); expect(propagatedTaps, equals(0)); await click(find.text('No Group')); expect(tappedOutside, unorderedEquals({'Group 1 A', 'Group 1 B'})); expect(propagatedTaps, equals(0)); tappedOutside.clear(); await click(find.text('Group 1 A')); expect(tappedOutside, equals({'No Group'})); expect(propagatedTaps, equals(0)); tappedOutside.clear(); await click(find.text('Group 1 B')); expect(tappedOutside, equals({'No Group'})); expect(propagatedTaps, equals(0)); tappedOutside.clear(); await click(find.text('Outside')); expect(tappedOutside, unorderedEquals({'No Group', 'Group 1 A', 'Group 1 B'})); expect(propagatedTaps, equals(0)); tappedOutside.clear(); await click(find.text('Outside Surface')); expect(tappedOutside, isEmpty); }); testWidgets('TapRegionSurface consumes outside middle click when asked', ( WidgetTester tester, ) async { final tappedOutside = {}; var propagatedTaps = 0; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( children: [ const Text('Outside Surface'), TapRegionSurface( child: Row( children: [ GestureDetector( onTap: () { propagatedTaps += 1; }, child: const Text('Outside'), ), TapRegion( consumeOutsideTaps: true, onTapOutside: (PointerEvent event) { tappedOutside.add('No Group'); }, child: const Text('No Group'), ), TapRegion( groupId: 1, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 A'); }, child: const Text('Group 1 A'), ), TapRegion( groupId: 1, consumeOutsideTaps: true, onTapOutside: (PointerEvent event) { tappedOutside.add('Group 1 B'); }, child: const Text('Group 1 B'), ), ], ), ), ], ), ), ); await tester.pump(); Future click(Finder finder) async { final TestGesture gesture = await tester.startGesture( tester.getCenter(finder), kind: PointerDeviceKind.mouse, buttons: kTertiaryButton, ); await gesture.up(); await gesture.removePointer(); } expect(tappedOutside, isEmpty); expect(propagatedTaps, equals(0)); await click(find.text('No Group')); expect(tappedOutside, unorderedEquals({'Group 1 A', 'Group 1 B'})); expect(propagatedTaps, equals(0)); tappedOutside.clear(); await click(find.text('Group 1 A')); expect(tappedOutside, equals({'No Group'})); expect(propagatedTaps, equals(0)); tappedOutside.clear(); await click(find.text('Group 1 B')); expect(tappedOutside, equals({'No Group'})); expect(propagatedTaps, equals(0)); tappedOutside.clear(); await click(find.text('Outside')); expect(tappedOutside, unorderedEquals({'No Group', 'Group 1 A', 'Group 1 B'})); expect(propagatedTaps, equals(0)); tappedOutside.clear(); await click(find.text('Outside Surface')); expect(tappedOutside, isEmpty); }); testWidgets('TapRegionSurface detects inside right click', (WidgetTester tester) async { final tappedInside = {}; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( children: [ const Text('Outside Surface'), TapRegionSurface( child: Row( children: [ const Text('Outside'), TapRegion( onTapInside: (PointerEvent event) { tappedInside.add('No Group'); }, child: const Text('No Group'), ), TapRegion( groupId: 1, onTapInside: (PointerEvent event) { tappedInside.add('Group 1 A'); }, child: const Text('Group 1 A'), ), TapRegion( groupId: 1, onTapInside: (PointerEvent event) { tappedInside.add('Group 1 B'); }, child: const Text('Group 1 B'), ), ], ), ), ], ), ), ); await tester.pump(); Future click(Finder finder) async { final TestGesture gesture = await tester.startGesture( tester.getCenter(finder), kind: PointerDeviceKind.mouse, buttons: kSecondaryButton, ); await gesture.up(); await gesture.removePointer(); } expect(tappedInside, isEmpty); await click(find.text('No Group')); expect(tappedInside, unorderedEquals({'No Group'})); tappedInside.clear(); await click(find.text('Group 1 A')); expect(tappedInside, equals({'Group 1 A', 'Group 1 B'})); tappedInside.clear(); await click(find.text('Group 1 B')); expect(tappedInside, equals({'Group 1 A', 'Group 1 B'})); tappedInside.clear(); await click(find.text('Outside')); expect(tappedInside, isEmpty); tappedInside.clear(); await click(find.text('Outside Surface')); expect(tappedInside, isEmpty); }); testWidgets('TapRegionSurface detects inside middle click', (WidgetTester tester) async { final tappedInside = {}; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( children: [ const Text('Outside Surface'), TapRegionSurface( child: Row( children: [ const Text('Outside'), TapRegion( onTapInside: (PointerEvent event) { tappedInside.add('No Group'); }, child: const Text('No Group'), ), TapRegion( groupId: 1, onTapInside: (PointerEvent event) { tappedInside.add('Group 1 A'); }, child: const Text('Group 1 A'), ), TapRegion( groupId: 1, onTapInside: (PointerEvent event) { tappedInside.add('Group 1 B'); }, child: const Text('Group 1 B'), ), ], ), ), ], ), ), ); await tester.pump(); Future click(Finder finder) async { final TestGesture gesture = await tester.startGesture( tester.getCenter(finder), kind: PointerDeviceKind.mouse, buttons: kTertiaryButton, ); await gesture.up(); await gesture.removePointer(); } expect(tappedInside, isEmpty); await click(find.text('No Group')); expect(tappedInside, unorderedEquals({'No Group'})); tappedInside.clear(); await click(find.text('Group 1 A')); expect(tappedInside, equals({'Group 1 A', 'Group 1 B'})); tappedInside.clear(); await click(find.text('Group 1 B')); expect(tappedInside, equals({'Group 1 A', 'Group 1 B'})); tappedInside.clear(); await click(find.text('Outside')); expect(tappedInside, isEmpty); tappedInside.clear(); await click(find.text('Outside Surface')); expect(tappedInside, isEmpty); }); // Regression test for https://github.com/flutter/flutter/issues/153093. testWidgets('TapRegion onTapOutside should only trigger on the current route during navigation', ( WidgetTester tester, ) async { const tapRegion1Key = ValueKey('TapRegion'); const tapRegion2Key = ValueKey('TapRegion2'); var count1 = 0; var count2 = 0; final tapRegion1 = TapRegion( key: tapRegion1Key, onTapOutside: (PointerEvent event) { count1 += 1; }, behavior: HitTestBehavior.opaque, child: const SizedBox.square(dimension: 100), ); final tapRegion2 = TapRegion( key: tapRegion2Key, onTapOutside: (PointerEvent event) { count2 += 1; }, behavior: HitTestBehavior.opaque, child: const SizedBox.square(dimension: 100), ); Future tapOutside(WidgetTester tester, Finder regionFinder) async { // Find the RenderBox of the region. final RenderBox renderBox = tester.firstRenderObject(find.byType(Scaffold).last); final Offset outsidePoint = renderBox.localToGlobal(Offset.zero) + const Offset(200, 200); await tester.tapAt(outsidePoint); await tester.pump(); } await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center(child: tapRegion1), floatingActionButton: FloatingActionButton( onPressed: () { Navigator.push( tester.element(find.byType(FloatingActionButton)), MaterialPageRoute( builder: (BuildContext context) => Scaffold(body: Center(child: tapRegion2)), ), ); }, child: const Icon(Icons.add), ), ), ), ); await tester.pumpAndSettle(); // Tap outside the first TapRegion to trigger onTapOutside. await tapOutside(tester, find.byKey(tapRegion1Key)); expect(count1, 1); expect(count2, 0); await tester.tap(find.byType(FloatingActionButton)); await tester.pumpAndSettle(); // Tap outside the second TapRegion to trigger onTapOutside await tapOutside(tester, find.byKey(tapRegion2Key)); expect(count1, 2); // When the Fab is pressed, the first TapRegion is still active. expect(count2, 1); // Back to the first page. Navigator.pop(tester.element(find.byType(Scaffold).last)); await tester.pumpAndSettle(); // Tap outside the first TapRegion to trigger onTapOutside await tapOutside(tester, find.byKey(tapRegion1Key)); expect(count1, 3); expect(count2, 1); }); // Regression test for https://github.com/flutter/flutter/issues/153093. testWidgets('TapRegion on non-current routes should not respond to onTapOutside events', ( WidgetTester tester, ) async { const tapRegion1Key = ValueKey('TapRegion1'); const tapRegion2Key = ValueKey('TapRegion2'); var count1 = 0; var count2 = 0; final tapRegion1 = TapRegion( key: tapRegion1Key, onTapOutside: (PointerEvent event) { count1 += 1; }, behavior: HitTestBehavior.opaque, child: const SizedBox.square(dimension: 100), ); final tapRegion2 = TapRegion( key: tapRegion2Key, onTapOutside: (PointerEvent event) { count2 += 1; }, behavior: HitTestBehavior.opaque, child: const SizedBox.square(dimension: 100), ); Future tapOutside(WidgetTester tester, Finder regionFinder) async { // Find the RenderBox of the region. final RenderBox renderBox = tester.firstRenderObject(find.byType(Scaffold).last); final Offset outsidePoint = renderBox.localToGlobal(Offset.zero) + const Offset(200, 200); await tester.tapAt(outsidePoint); await tester.pump(); } await tester.pumpWidget( MaterialApp( initialRoute: '/', routes: { '/': (BuildContext context) => Scaffold(body: Center(child: tapRegion1)), '/second': (BuildContext context) => Scaffold(body: Center(child: tapRegion2)), }, onGenerateInitialRoutes: (String initialRouteName) { return >[ MaterialPageRoute( builder: (BuildContext context) => Scaffold(body: Center(child: tapRegion1)), ), MaterialPageRoute( builder: (BuildContext context) => Scaffold(body: Center(child: tapRegion2)), ), ]; }, ), ); await tester.pumpAndSettle(); // At this point, tapRegion2 is on top of tapRegion1. // Tap outside tapRegion2. await tapOutside(tester, find.byKey(tapRegion2Key)); expect(count1, 0); // tapRegion1 should not respond. expect(count2, 1); // tapRegion2 should respond. // Now pop the top route to reveal tapRegion1. Navigator.pop(tester.element(find.byType(Scaffold).last)); await tester.pumpAndSettle(); // Tap outside tapRegion1. await tapOutside(tester, find.byKey(tapRegion1Key)); expect(count1, 1); // tapRegion1 should respond. expect(count2, 1); // tapRegion2 should not respond anymore. }); // Regression test for the consumeOutsideTaps issue when navigating between pages testWidgets('TapRegion with consumeOutsideTaps should not consume taps after navigation', ( WidgetTester tester, ) async { const tapRegionKey = ValueKey('TapRegion'); const buttonKey = ValueKey('Button'); var buttonTapped = false; await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: TapRegion( key: tapRegionKey, consumeOutsideTaps: true, onTapOutside: (PointerEvent event) {}, behavior: HitTestBehavior.opaque, child: GestureDetector( onTap: () { Navigator.push( tester.element(find.byType(GestureDetector)), MaterialPageRoute( builder: (BuildContext context) => Scaffold( body: Center( child: ElevatedButton( key: buttonKey, onPressed: () { buttonTapped = true; }, child: const Text('Test Button'), ), ), ), ), ); }, child: Container(width: 250.0, height: 250.0, color: Colors.blue), ), ), ), ), ), ); await tester.pumpAndSettle(); // Navigate to the second page await tester.tap(find.byKey(tapRegionKey)); await tester.pumpAndSettle(); // Verify that the button on the second page can be tapped // If consumeOutsideTaps is still active from the first page's TapRegion, // this tap would be consumed and buttonTapped would remain false await tester.tap(find.byKey(buttonKey)); await tester.pumpAndSettle(); expect( buttonTapped, true, reason: 'Button tap was not consumed by a TapRegion on a non-current route', ); }); }