// 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. // reduced-test-set: // This file is run as part of a reduced test set in CI on Mac and Windows // machines. @Tags(['reduced-test-set']) library; import 'package:flutter/cupertino.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { const expansionDuration = Duration(milliseconds: 250); const infinitesimalDuration = Duration(microseconds: 1); testWidgets('Toggles expansion on tap', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: CupertinoPageScaffold( child: CupertinoExpansionTile(title: Text('Title'), child: Text('Content')), ), ), ); expect(find.text('Content'), findsNothing); await tester.tap(find.text('Title')); await tester.pump(); // The child animating its height and a clone fading in. expect(find.text('Content'), findsNWidgets(2)); await tester.tap(find.text('Title')); await tester.pump(); expect(find.text('Content'), findsNothing); }); testWidgets('Can be controlled by ExpansibleController', (WidgetTester tester) async { final controller = ExpansibleController(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( child: CupertinoExpansionTile( controller: controller, title: const Text('Title'), child: const Text('Content'), ), ), ), ); expect(controller.isExpanded, isFalse); expect(find.text('Content'), findsNothing); controller.expand(); await tester.pump(); expect(controller.isExpanded, isTrue); expect(find.text('Content'), findsOneWidget); controller.collapse(); await tester.pump(); expect(controller.isExpanded, isFalse); expect(find.text('Content'), findsNothing); controller.dispose(); }); testWidgets('Controller can set the tile to be initially expanded', (WidgetTester tester) async { final controller = ExpansibleController(); await tester.pumpWidget( CupertinoApp( home: CupertinoPageScaffold( child: CupertinoExpansionTile( controller: controller, title: const Text('Title'), child: const Text('Content'), ), ), ), ); controller.expand(); await tester.pump(); expect(controller.isExpanded, isTrue); expect(find.text('Content'), findsOneWidget); await tester.tap(find.text('Title')); await tester.pump(); expect(controller.isExpanded, isFalse); expect(find.text('Content'), findsNothing); controller.dispose(); }); testWidgets('Nested expansion tile', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: CupertinoPageScaffold( child: CupertinoExpansionTile( title: Text('Outer'), child: CupertinoExpansionTile(title: Text('Inner'), child: Text('Content')), ), ), ), ); expect(find.text('Content'), findsNothing); await tester.tap(find.text('Outer')); await tester.pump(); await tester.pump(expansionDuration + infinitesimalDuration); expect(find.text('Content'), findsNothing); await tester.tap(find.text('Inner')); await tester.pump(); await tester.pump(expansionDuration + infinitesimalDuration); expect(find.text('Content'), findsOneWidget); await tester.tap(find.text('Inner')); await tester.pump(); await tester.pump(expansionDuration + infinitesimalDuration); expect(find.text('Content'), findsNothing); await tester.tap(find.text('Outer')); await tester.pump(); await tester.pump(expansionDuration + infinitesimalDuration); expect(find.text('Content'), findsNothing); }); testWidgets('Default expansion animation and icon rotation', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Center( child: RepaintBoundary( child: CupertinoExpansionTile( title: Text('Title'), child: SizedBox(height: 50.0, child: ColoredBox(color: Color(0xffff0000))), ), ), ), ), ); await expectLater( find.byType(CupertinoExpansionTile), matchesGoldenFile('expansion_tile.default.collapsed.png'), ); await tester.tap(find.text('Title')); await tester.pump(); // Pump until halfway through the animation. await tester.pump(expansionDuration ~/ 2); await expectLater( find.byType(CupertinoExpansionTile), matchesGoldenFile('expansion_tile.default.forward.png'), ); await tester.pumpAndSettle(); await expectLater( find.byType(CupertinoExpansionTile), matchesGoldenFile('expansion_tile.default.expanded.png'), ); await tester.tap(find.text('Title')); await tester.pump(); // Pump until halfway through the animation. await tester.pump(expansionDuration ~/ 2); await expectLater( find.byType(CupertinoExpansionTile), matchesGoldenFile('expansion_tile.default.reverse.png'), ); }); testWidgets('Expansion animation in scroll mode', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Center( child: RepaintBoundary( child: CupertinoExpansionTile( title: Text('Title'), transitionMode: ExpansionTileTransitionMode.scroll, child: SizedBox(height: 50.0, child: ColoredBox(color: Color(0xffff0000))), ), ), ), ), ); await tester.tap(find.text('Title')); await tester.pump(); // Pump until halfway through the animation. await tester.pump(expansionDuration ~/ 2); await expectLater( find.byType(CupertinoExpansionTile), matchesGoldenFile('expansion_tile.scroll_mode.forward.png'), ); await tester.pumpAndSettle(); await tester.tap(find.text('Title')); await tester.pump(); // Pump until halfway through the animation. await tester.pump(expansionDuration ~/ 2); await expectLater( find.byType(CupertinoExpansionTile), matchesGoldenFile('expansion_tile.scroll_mode.reverse.png'), ); await tester.pumpAndSettle(); }); testWidgets('Nested CupertinoListTile Semantics', (WidgetTester tester) async { final controller = ExpansibleController(); addTearDown(controller.dispose); final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget( CupertinoApp( home: Column( children: [ const CupertinoExpansionTile( title: Text('First Expansion Tile'), child: SizedBox(height: 50.0), ), CupertinoExpansionTile( controller: controller, title: const Text('Second Expansion Tile'), child: const SizedBox(height: 50.0), ), ], ), ), ); controller.expand(); await tester.pumpAndSettle(); expect( tester.getSemantics(find.byType(CupertinoListTile).first), matchesSemantics( hasTapAction: true, label: 'First Expansion Tile', textDirection: TextDirection.ltr, ), ); expect( tester.getSemantics(find.byType(CupertinoListTile).last), matchesSemantics( hasTapAction: true, label: 'Second Expansion Tile', textDirection: TextDirection.ltr, ), ); handle.dispose(); }); testWidgets('Semantics with the onTapHint is an ancestor of CupertinoListTile', ( WidgetTester tester, ) async { final controller = ExpansibleController(); addTearDown(controller.dispose); final SemanticsHandle handle = tester.ensureSemantics(); const localizations = DefaultCupertinoLocalizations(); await tester.pumpWidget( CupertinoApp( home: Column( children: [ const CupertinoExpansionTile( title: Text('First Expansion Tile'), child: SizedBox(height: 100, width: 100), ), CupertinoExpansionTile( controller: controller, title: const Text('Second Expansion Tile'), child: const SizedBox(height: 100, width: 100), ), ], ), ), ); controller.expand(); await tester.pumpAndSettle(); SemanticsNode semantics = tester.getSemantics( find .ancestor(of: find.byType(CupertinoListTile).first, matching: find.byType(Semantics)) .first, ); expect(semantics, isNotNull); // The onTapHint is passed to semantics properties's hintOverrides. expect(semantics.hintOverrides, isNotNull); // The hint should be the opposite of the current state. // The first CupertinoExpansionTile is collapsed, so the hint should be // "double tap to expand". expect(semantics.hintOverrides!.onTapHint, localizations.expansionTileCollapsedTapHint); semantics = tester.getSemantics( find .ancestor(of: find.byType(CupertinoListTile).last, matching: find.byType(Semantics)) .first, ); expect(semantics, isNotNull); // The onTapHint is passed to semantics properties's hintOverrides. expect(semantics.hintOverrides, isNotNull); // The hint should be the opposite of the current state. // The second CupertinoExpansionTile is expanded, so the hint should be // "double tap to collapse". expect(semantics.hintOverrides!.onTapHint, localizations.expansionTileExpandedTapHint); handle.dispose(); }); testWidgets( 'Semantics hint for iOS and macOS', (WidgetTester tester) async { final controller = ExpansibleController(); addTearDown(controller.dispose); final SemanticsHandle handle = tester.ensureSemantics(); const localizations = DefaultCupertinoLocalizations(); await tester.pumpWidget( CupertinoApp( home: Column( children: [ const CupertinoExpansionTile( title: Text('First Expansion Tile'), child: SizedBox(height: 100, width: 100), ), CupertinoExpansionTile( controller: controller, title: const Text('Second Expansion Tile'), child: const SizedBox(height: 100, width: 100), ), ], ), ), ); controller.expand(); await tester.pumpAndSettle(); SemanticsNode semantics = tester.getSemantics( find .ancestor(of: find.byType(CupertinoListTile).first, matching: find.byType(Semantics)) .first, ); expect(semantics, isNotNull); expect( semantics.hint, '${localizations.expandedHint}\n ${localizations.expansionTileCollapsedHint}', ); semantics = tester.getSemantics( find .ancestor(of: find.byType(CupertinoListTile).last, matching: find.byType(Semantics)) .first, ); expect(semantics, isNotNull); expect( semantics.hint, '${localizations.collapsedHint}\n ${localizations.expansionTileExpandedHint}', ); handle.dispose(); }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS, }), ); testWidgets('CupertinoExpansionTile does not crash at zero area', (WidgetTester tester) async { tester.view.physicalSize = Size.zero; final controller = ExpansibleController(); addTearDown(tester.view.reset); addTearDown(controller.dispose); await tester.pumpWidget( CupertinoApp( home: Center( child: CupertinoExpansionTile( controller: controller, title: const Text('X'), child: const Text('Y'), ), ), ), ); expect(tester.getSize(find.byType(CupertinoExpansionTile)), Size.zero); controller.expand(); await tester.pumpAndSettle(); }); }