// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('Nested TickerMode cannot turn tickers back on', (WidgetTester tester) async { int outerTickCount = 0; int innerTickCount = 0; Widget nestedTickerModes({required bool innerEnabled, required bool outerEnabled}) { return Directionality( textDirection: TextDirection.rtl, child: TickerMode( enabled: outerEnabled, child: Row( children: <Widget>[ _TickingWidget( onTick: () { outerTickCount++; }, ), TickerMode( enabled: innerEnabled, child: _TickingWidget( onTick: () { innerTickCount++; }, ), ), ], ), ), ); } await tester.pumpWidget( nestedTickerModes( outerEnabled: false, innerEnabled: true, ), ); expect(outerTickCount, 0); expect(innerTickCount, 0); await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1)); expect(outerTickCount, 0); expect(innerTickCount, 0); await tester.pumpWidget( nestedTickerModes( outerEnabled: true, innerEnabled: false, ), ); outerTickCount = 0; innerTickCount = 0; await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1)); expect(outerTickCount, 4); expect(innerTickCount, 0); await tester.pumpWidget( nestedTickerModes( outerEnabled: true, innerEnabled: true, ), ); outerTickCount = 0; innerTickCount = 0; await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1)); expect(outerTickCount, 4); expect(innerTickCount, 4); await tester.pumpWidget( nestedTickerModes( outerEnabled: false, innerEnabled: false, ), ); outerTickCount = 0; innerTickCount = 0; await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1)); expect(outerTickCount, 0); expect(innerTickCount, 0); }); testWidgets('Changing TickerMode does not rebuild widgets with SingleTickerProviderStateMixin', (WidgetTester tester) async { Widget widgetUnderTest({required bool tickerEnabled}) { return TickerMode( enabled: tickerEnabled, child: const _TickingWidget(), ); } _TickingWidgetState state() => tester.state<_TickingWidgetState>(find.byType(_TickingWidget)); await tester.pumpWidget(widgetUnderTest(tickerEnabled: true)); expect(state().ticker.isTicking, isTrue); expect(state().buildCount, 1); await tester.pumpWidget(widgetUnderTest(tickerEnabled: false)); expect(state().ticker.isTicking, isFalse); expect(state().buildCount, 1); await tester.pumpWidget(widgetUnderTest(tickerEnabled: true)); expect(state().ticker.isTicking, isTrue); expect(state().buildCount, 1); }); testWidgets('Changing TickerMode does not rebuild widgets with TickerProviderStateMixin', (WidgetTester tester) async { Widget widgetUnderTest({required bool tickerEnabled}) { return TickerMode( enabled: tickerEnabled, child: const _MultiTickingWidget(), ); } _MultiTickingWidgetState state() => tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget)); await tester.pumpWidget(widgetUnderTest(tickerEnabled: true)); expect(state().ticker.isTicking, isTrue); expect(state().buildCount, 1); await tester.pumpWidget(widgetUnderTest(tickerEnabled: false)); expect(state().ticker.isTicking, isFalse); expect(state().buildCount, 1); await tester.pumpWidget(widgetUnderTest(tickerEnabled: true)); expect(state().ticker.isTicking, isTrue); expect(state().buildCount, 1); }); testWidgets('Moving widgets with SingleTickerProviderStateMixin to a new TickerMode ancestor works', (WidgetTester tester) async { final GlobalKey tickingWidgetKey = GlobalKey(); Widget widgetUnderTest({required LocalKey tickerModeKey, required bool tickerEnabled}) { return TickerMode( key: tickerModeKey, enabled: tickerEnabled, child: _TickingWidget(key: tickingWidgetKey), ); } // Using different local keys to simulate changing TickerMode ancestors. await tester.pumpWidget(widgetUnderTest(tickerEnabled: true, tickerModeKey: UniqueKey())); final State tickerModeState = tester.state(find.byType(TickerMode)); final _TickingWidgetState tickingState = tester.state<_TickingWidgetState>(find.byType(_TickingWidget)); expect(tickingState.ticker.isTicking, isTrue); await tester.pumpWidget(widgetUnderTest(tickerEnabled: false, tickerModeKey: UniqueKey())); expect(tester.state(find.byType(TickerMode)), isNot(same(tickerModeState))); expect(tickingState, same(tester.state<_TickingWidgetState>(find.byType(_TickingWidget)))); expect(tickingState.ticker.isTicking, isFalse); }); testWidgets('Moving widgets with TickerProviderStateMixin to a new TickerMode ancestor works', (WidgetTester tester) async { final GlobalKey tickingWidgetKey = GlobalKey(); Widget widgetUnderTest({required LocalKey tickerModeKey, required bool tickerEnabled}) { return TickerMode( key: tickerModeKey, enabled: tickerEnabled, child: _MultiTickingWidget(key: tickingWidgetKey), ); } // Using different local keys to simulate changing TickerMode ancestors. await tester.pumpWidget(widgetUnderTest(tickerEnabled: true, tickerModeKey: UniqueKey())); final State tickerModeState = tester.state(find.byType(TickerMode)); final _MultiTickingWidgetState tickingState = tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget)); expect(tickingState.ticker.isTicking, isTrue); await tester.pumpWidget(widgetUnderTest(tickerEnabled: false, tickerModeKey: UniqueKey())); expect(tester.state(find.byType(TickerMode)), isNot(same(tickerModeState))); expect(tickingState, same(tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget)))); expect(tickingState.ticker.isTicking, isFalse); }); testWidgets('Ticking widgets in old route do not rebuild when new route is pushed', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( routes: <String, WidgetBuilder>{ '/foo' : (BuildContext context) => const Text('New route'), }, home: Row( children: const <Widget>[ _TickingWidget(), _MultiTickingWidget(), Text('Old route'), ], ), )); _MultiTickingWidgetState multiTickingState() => tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget, skipOffstage: false)); _TickingWidgetState tickingState() => tester.state<_TickingWidgetState>(find.byType(_TickingWidget, skipOffstage: false)); expect(find.text('Old route'), findsOneWidget); expect(find.text('New route'), findsNothing); expect(multiTickingState().ticker.isTicking, isTrue); expect(multiTickingState().buildCount, 1); expect(tickingState().ticker.isTicking, isTrue); expect(tickingState().buildCount, 1); tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/foo'); await tester.pumpAndSettle(); expect(find.text('Old route'), findsNothing); expect(find.text('New route'), findsOneWidget); expect(multiTickingState().ticker.isTicking, isFalse); expect(multiTickingState().buildCount, 1); expect(tickingState().ticker.isTicking, isFalse); expect(tickingState().buildCount, 1); }); } class _TickingWidget extends StatefulWidget { const _TickingWidget({super.key, this.onTick}); final VoidCallback? onTick; @override State<_TickingWidget> createState() => _TickingWidgetState(); } class _TickingWidgetState extends State<_TickingWidget> with SingleTickerProviderStateMixin { late Ticker ticker; int buildCount = 0; @override void initState() { super.initState(); ticker = createTicker((Duration _) { widget.onTick?.call(); })..start(); } @override Widget build(BuildContext context) { buildCount += 1; return Container(); } @override void dispose() { ticker.dispose(); super.dispose(); } } class _MultiTickingWidget extends StatefulWidget { const _MultiTickingWidget({super.key}); @override State<_MultiTickingWidget> createState() => _MultiTickingWidgetState(); } class _MultiTickingWidgetState extends State<_MultiTickingWidget> with TickerProviderStateMixin { late Ticker ticker; int buildCount = 0; @override void initState() { super.initState(); ticker = createTicker((Duration _) { })..start(); } @override Widget build(BuildContext context) { buildCount += 1; return Container(); } @override void dispose() { ticker.dispose(); super.dispose(); } }