From bad25b47e07bac0fb1d7aa75e5efeeb2dc128193 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 1 Sep 2026 10:10:53 +0200 Subject: [PATCH] port drawer tests over from flutter/widgets --- .../change_2026_09_01_1788252909308.yaml | 3 + packages/material_ui/test/drawer_test.dart | 413 +++++++++++++++++- 2 files changed, 396 insertions(+), 20 deletions(-) create mode 100644 packages/material_ui/pending_changelogs/change_2026_09_01_1788252909308.yaml diff --git a/packages/material_ui/pending_changelogs/change_2026_09_01_1788252909308.yaml b/packages/material_ui/pending_changelogs/change_2026_09_01_1788252909308.yaml new file mode 100644 index 000000000000..2f534e4ec064 --- /dev/null +++ b/packages/material_ui/pending_changelogs/change_2026_09_01_1788252909308.yaml @@ -0,0 +1,3 @@ +changelog: | + - Port drawer tests from flutter/widgets into material_ui +version: skip diff --git a/packages/material_ui/test/drawer_test.dart b/packages/material_ui/test/drawer_test.dart index 27c240efd484..397159729db8 100644 --- a/packages/material_ui/test/drawer_test.dart +++ b/packages/material_ui/test/drawer_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -14,20 +15,28 @@ void main() { testWidgets('Material2 - Drawer control test', (WidgetTester tester) async { const containerKey = Key('container'); + late BuildContext savedContext; + await tester.pumpWidget( MaterialApp( theme: ThemeData(useMaterial3: false), - home: Scaffold( - drawer: Drawer( - child: ListView( - children: [ - DrawerHeader( - child: Container(key: containerKey, child: const Text('header')), + home: Builder( + builder: (BuildContext context) { + savedContext = context; + + return Scaffold( + drawer: Drawer( + child: ListView( + children: [ + DrawerHeader( + child: Container(key: containerKey, child: const Text('header')), + ), + const ListTile(leading: Icon(Icons.archive), title: Text('Archive')), + ], ), - const ListTile(leading: Icon(Icons.archive), title: Text('Archive')), - ], - ), - ), + ), + ); + }, ), ), ); @@ -37,6 +46,7 @@ void main() { state.openDrawer(); await tester.pump(); + expect(find.text('Archive'), findsOneWidget); await tester.pump(const Duration(seconds: 1)); expect(find.text('Archive'), findsOneWidget); @@ -51,24 +61,38 @@ void main() { expect(box.size.height, equals(drawerHeight - 2 * 16.0)); expect(find.text('header'), findsOneWidget); + + Navigator.pop(savedContext); + await tester.pump(); // drawer should be starting to animate away + expect(find.text('Archive'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); // animation done + expect(find.text('Archive'), findsNothing); }); testWidgets('Material3 - Drawer control test', (WidgetTester tester) async { const containerKey = Key('container'); + late BuildContext savedContext; + await tester.pumpWidget( MaterialApp( - home: Scaffold( - drawer: Drawer( - child: ListView( - children: [ - DrawerHeader( - child: Container(key: containerKey, child: const Text('header')), + home: Builder( + builder: (BuildContext context) { + savedContext = context; + + return Scaffold( + drawer: Drawer( + child: ListView( + children: [ + DrawerHeader( + child: Container(key: containerKey, child: const Text('header')), + ), + const ListTile(leading: Icon(Icons.archive), title: Text('Archive')), + ], ), - const ListTile(leading: Icon(Icons.archive), title: Text('Archive')), - ], - ), - ), + ), + ); + }, ), ), ); @@ -78,6 +102,7 @@ void main() { state.openDrawer(); await tester.pump(); + expect(find.text('Archive'), findsOneWidget); await tester.pump(const Duration(seconds: 1)); expect(find.text('Archive'), findsOneWidget); @@ -95,6 +120,12 @@ void main() { ); // Header divider thickness is 1.0 in Material 3. expect(find.text('header'), findsOneWidget); + + Navigator.pop(savedContext); + await tester.pump(); // drawer should be starting to animate away + expect(find.text('Archive'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); // animation done + expect(find.text('Archive'), findsNothing); }); testWidgets( @@ -148,6 +179,110 @@ void main() { semantics.dispose(); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); + testWidgets('Drawer tap test', (WidgetTester tester) async { + final scaffoldKey = GlobalKey(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold(key: scaffoldKey, drawer: const Text('drawer'), body: Container()), + ), + ); + await tester.pump(); // no effect + expect(find.text('drawer'), findsNothing); + scaffoldKey.currentState!.openDrawer(); + await tester.pump(); // drawer should be starting to animate in + expect(find.text('drawer'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); // animation done + expect(find.text('drawer'), findsOneWidget); + await tester.tap(find.text('drawer')); + await tester.pump(); // nothing should have happened + expect(find.text('drawer'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); // ditto + expect(find.text('drawer'), findsOneWidget); + await tester.tapAt(const Offset(750.0, 100.0)); // on the mask + await tester.pump(); + await tester.pump(const Duration(milliseconds: 10)); + // drawer should be starting to animate away + expect(find.text('drawer'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); // animation done + expect(find.text('drawer'), findsNothing); + }); + + testWidgets('Drawer hover test', (WidgetTester tester) async { + final scaffoldKey = GlobalKey(); + final logs = []; + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + // Start out of hoverTarget + await gesture.addPointer(location: const Offset(100, 100)); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + key: scaffoldKey, + drawer: const Text('drawer'), + body: Align( + alignment: Alignment.topLeft, + child: MouseRegion( + onEnter: (_) { + logs.add('enter'); + }, + onHover: (_) { + logs.add('hover'); + }, + onExit: (_) { + logs.add('exit'); + }, + child: const SizedBox(width: 10, height: 10), + ), + ), + ), + ), + ); + expect(logs, isEmpty); + expect(find.text('drawer'), findsNothing); + + // When drawer is closed, hover is interactable + await gesture.moveTo(const Offset(5, 5)); + await tester.pump(); // no effect + expect(logs, ['enter', 'hover']); + logs.clear(); + + await gesture.moveTo(const Offset(20, 20)); + await tester.pump(); // no effect + expect(logs, ['exit']); + logs.clear(); + + // When drawer is open, hover is uninteractable + scaffoldKey.currentState!.openDrawer(); + await tester.pump(const Duration(seconds: 1)); // animation done + expect(find.text('drawer'), findsOneWidget); + + await gesture.moveTo(const Offset(5, 5)); + await tester.pump(); // no effect + expect(logs, isEmpty); + logs.clear(); + + await gesture.moveTo(const Offset(20, 20)); + await tester.pump(); // no effect + expect(logs, isEmpty); + logs.clear(); + + // Close drawer, hover is interactable again + await tester.tapAt(const Offset(750.0, 100.0)); // on the mask + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); // animation done + expect(find.text('drawer'), findsNothing); + + await gesture.moveTo(const Offset(5, 5)); + await tester.pump(); // no effect + expect(logs, ['enter', 'hover']); + logs.clear(); + + await gesture.moveTo(const Offset(20, 20)); + await tester.pump(); // no effect + expect(logs, ['exit']); + logs.clear(); + }); + testWidgets('Scaffold drawerScrimColor', (WidgetTester tester) async { // The scrim is a ColoredBox within a Semantics node labeled "Dismiss", // within a DrawerController. Sorry. @@ -305,6 +440,152 @@ void main() { expect(finder, findsNothing); }); + testWidgets('Close drawer with navigator pop', (WidgetTester tester) async { + final scaffoldKey = GlobalKey(); + var buttonPressed = false; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + return Scaffold( + key: scaffoldKey, + drawer: Drawer( + child: ListView( + children: [ + const Text('drawer'), + TextButton(child: const Text('close'), onPressed: () => Navigator.pop(context)), + ], + ), + ), + body: TextButton( + child: const Text('button'), + onPressed: () { + buttonPressed = true; + }, + ), + ); + }, + ), + ), + ); + + // Open the drawer. + scaffoldKey.currentState!.openDrawer(); + await tester.pump(); // drawer should be starting to animate in + expect(find.text('drawer'), findsOneWidget); + + // Tap the close button to pop the drawer route. + await tester.pump(const Duration(milliseconds: 100)); + await tester.tap(find.text('close')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + expect(find.text('drawer'), findsNothing); + + // Confirm that a button in the scaffold body is still clickable. + await tester.tap(find.text('button')); + expect(buttonPressed, equals(true)); + }); + + testWidgets('Drawer closing can be canceled by gesture (LTR)', (WidgetTester tester) async { + final scaffoldKey = GlobalKey(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + drawerDragStartBehavior: DragStartBehavior.down, + key: scaffoldKey, + drawer: Drawer( + child: ListView( + children: [ + const Text('drawer'), + Container(height: 1000.0, color: Colors.blue[500]), + ], + ), + ), + body: Container(), + ), + ), + ); + expect(find.text('drawer'), findsNothing); + scaffoldKey.currentState!.openDrawer(); + await tester.pump(); // drawer should be starting to animate in + expect(find.text('drawer'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); // animation done + expect(find.text('drawer'), findsOneWidget); + + await tester.tapAt(const Offset(750.0, 100.0)); // on the mask + await tester.pump(); + await tester.pump(const Duration(milliseconds: 10)); + // drawer should be starting to animate away + final double textLeft = tester.getTopLeft(find.text('drawer')).dx; + expect(textLeft, lessThan(0.0)); + + final TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0)); + // drawer should be stopped. + await tester.pump(); + await tester.pump(const Duration(milliseconds: 10)); + expect(tester.getTopLeft(find.text('drawer')).dx, equals(textLeft)); + + await gesture.moveBy(const Offset(50.0, 0.0)); + // drawer should be returning to visible + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + expect(tester.getTopLeft(find.text('drawer')).dx, equals(0.0)); + + await gesture.up(); + }); + + testWidgets('Drawer closing can be canceled by gesture (RTL)', (WidgetTester tester) async { + final scaffoldKey = GlobalKey(); + await tester.pumpWidget( + MaterialApp( + home: Directionality( + textDirection: TextDirection.rtl, + child: Scaffold( + drawerDragStartBehavior: DragStartBehavior.down, + key: scaffoldKey, + drawer: Drawer( + child: ListView( + children: [ + const Text('drawer'), + Container(height: 1000.0, color: Colors.blue[500]), + ], + ), + ), + body: Container(), + ), + ), + ), + ); + expect(find.text('drawer'), findsNothing); + scaffoldKey.currentState!.openDrawer(); + await tester.pump(); // drawer should be starting to animate in + expect(find.text('drawer'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); // animation done + expect(find.text('drawer'), findsOneWidget); + + await tester.tapAt(const Offset(50.0, 100.0)); // on the mask + await tester.pump(); + await tester.pump(const Duration(milliseconds: 10)); + // drawer should be starting to animate away + final double textRight = tester.getTopRight(find.text('drawer')).dx; + expect(textRight, greaterThan(800.0)); + + final TestGesture gesture = await tester.startGesture(const Offset(700.0, 100.0)); + // drawer should be stopped. + await tester.pump(); + await tester.pump(const Duration(milliseconds: 10)); + expect(tester.getTopRight(find.text('drawer')).dx, equals(textRight)); + + await gesture.moveBy(const Offset(-50.0, 0.0)); + // drawer should be returning to visible + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + expect(tester.getTopRight(find.text('drawer')).dx, equals(800.0)); + + await gesture.up(); + }); + testWidgets('Scaffold.drawer - null restorationId ', (WidgetTester tester) async { final scaffoldKey = GlobalKey(); await tester.pumpWidget( @@ -843,6 +1124,68 @@ void main() { expect(find.byType(Drawer), findsExactly(0)); }); + testWidgets( + 'Dismissible Drawer includes button in semantic tree', + (WidgetTester tester) async { + final semantics = SemanticsTester(tester); + final scaffoldKey = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + return Scaffold(key: scaffoldKey, drawer: const Drawer()); + }, + ), + ), + ); + + // Open the drawer. + scaffoldKey.currentState!.openDrawer(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(semantics, includesNodeWith(actions: [SemanticsAction.tap])); + expect(semantics, includesNodeWith(label: 'Dismiss')); + + semantics.dispose(); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.iOS, + TargetPlatform.macOS, + }), + ); + + testWidgets('Dismissible Drawer is hidden on Android (back button is used to dismiss)', ( + WidgetTester tester, + ) async { + final semantics = SemanticsTester(tester); + final scaffoldKey = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + return Scaffold(key: scaffoldKey, drawer: const Drawer(), body: Container()); + }, + ), + ), + ); + + // Open the drawer. + scaffoldKey.currentState!.openDrawer(); + await tester.pump(const Duration(milliseconds: 100)); + + expect( + semantics, + isNot( + includesNodeWith(actions: [SemanticsAction.tap, SemanticsAction.focus]), + ), + ); + expect(semantics, isNot(includesNodeWith(label: 'Dismiss'))); + + semantics.dispose(); + }, variant: TargetPlatformVariant.only(TargetPlatform.android)); + testWidgets('Drawer can be configured as not dismissible', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -1033,6 +1376,36 @@ void main() { await pumpDrawerWithTheme(TargetPlatform.iOS); }, variant: TargetPlatformVariant.all()); + testWidgets('Drawer contains route semantics flags', (WidgetTester tester) async { + final semantics = SemanticsTester(tester); + final scaffoldKey = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + return Scaffold(key: scaffoldKey, drawer: const Drawer(), body: Container()); + }, + ), + ), + ); + + // Open the drawer. + scaffoldKey.currentState!.openDrawer(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect( + semantics, + includesNodeWith( + label: 'Navigation menu', + flags: [SemanticsFlag.scopesRoute, SemanticsFlag.namesRoute], + ), + ); + + semantics.dispose(); + }); + group('Material 2', () { // These tests are only relevant for Material 2. Once Material 2 // support is deprecated and the APIs are removed, these tests