blob: cdaf52cadee04fbd1854e74c8070c378547e7ec3 [file] [log] [blame]
// 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/foundation.dart';
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 {
var outerTickCount = 0;
var 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: const Row(
children: <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);
});
testWidgets('TickerMode.forceFrames propagates to SingleTickerProviderStateMixin', (
WidgetTester tester,
) async {
Widget widgetUnderTest({required bool forceFrames}) {
return TickerMode(enabled: true, forceFrames: forceFrames, child: const _TickingWidget());
}
_TickingWidgetState state() => tester.state<_TickingWidgetState>(find.byType(_TickingWidget));
await tester.pumpWidget(widgetUnderTest(forceFrames: false));
expect(state().ticker.forceFrames, isFalse);
expect(state().buildCount, 1);
await tester.pumpWidget(widgetUnderTest(forceFrames: true));
expect(state().ticker.forceFrames, isTrue);
expect(state().buildCount, 1);
await tester.pumpWidget(widgetUnderTest(forceFrames: false));
expect(state().ticker.forceFrames, isFalse);
expect(state().buildCount, 1);
});
testWidgets('TickerMode.forceFrames propagates to TickerProviderStateMixin', (
WidgetTester tester,
) async {
Widget widgetUnderTest({required bool forceFrames}) {
return TickerMode(
enabled: true,
forceFrames: forceFrames,
child: const _MultiTickingWidget(),
);
}
_MultiTickingWidgetState state() =>
tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget));
await tester.pumpWidget(widgetUnderTest(forceFrames: false));
expect(state().ticker.forceFrames, isFalse);
expect(state().buildCount, 1);
await tester.pumpWidget(widgetUnderTest(forceFrames: true));
expect(state().ticker.forceFrames, isTrue);
expect(state().buildCount, 1);
await tester.pumpWidget(widgetUnderTest(forceFrames: false));
expect(state().ticker.forceFrames, isFalse);
expect(state().buildCount, 1);
});
testWidgets('Nested TickerMode.forceFrames uses OR semantics', (WidgetTester tester) async {
Widget nestedTickerModes({required bool innerForce, required bool outerForce}) {
return Directionality(
textDirection: TextDirection.rtl,
child: TickerMode(
enabled: true,
forceFrames: outerForce,
child: Row(
children: <Widget>[
const _TickingWidget(key: ValueKey<String>('outer')),
TickerMode(
enabled: true,
forceFrames: innerForce,
child: const _TickingWidget(key: ValueKey<String>('inner')),
),
],
),
),
);
}
_TickingWidgetState outerState() =>
tester.state<_TickingWidgetState>(find.byKey(const ValueKey<String>('outer')));
_TickingWidgetState innerState() =>
tester.state<_TickingWidgetState>(find.byKey(const ValueKey<String>('inner')));
// Both false -> both should not force frames
await tester.pumpWidget(nestedTickerModes(outerForce: false, innerForce: false));
expect(outerState().ticker.forceFrames, isFalse);
expect(innerState().ticker.forceFrames, isFalse);
// Outer true -> both should force frames (OR semantics)
await tester.pumpWidget(nestedTickerModes(outerForce: true, innerForce: false));
expect(outerState().ticker.forceFrames, isTrue);
expect(innerState().ticker.forceFrames, isTrue);
// Inner true -> only inner should force frames
await tester.pumpWidget(nestedTickerModes(outerForce: false, innerForce: true));
expect(outerState().ticker.forceFrames, isFalse);
expect(innerState().ticker.forceFrames, isTrue);
// Both true -> both should force frames
await tester.pumpWidget(nestedTickerModes(outerForce: true, innerForce: true));
expect(outerState().ticker.forceFrames, isTrue);
expect(innerState().ticker.forceFrames, isTrue);
});
testWidgets('TickerMode.merge preserves ambient enabled and overrides forceFrames', (
WidgetTester tester,
) async {
await tester.pumpWidget(
TickerMode(
enabled: false,
child: TickerMode.merge(forceFrames: true, child: const _TickingWidget()),
),
);
final _TickingWidgetState state = tester.state<_TickingWidgetState>(
find.byType(_TickingWidget),
);
// enabled should be false (inherited from ancestor)
expect(state.ticker.muted, isTrue);
// forceFrames should be true (merged override)
expect(state.ticker.forceFrames, isTrue);
});
testWidgets('TickerMode.merge respects AND semantics for enabled', (WidgetTester tester) async {
// Test that merge cannot override parent's enabled=false due to AND semantics
await tester.pumpWidget(
TickerMode(
enabled: false,
child: TickerMode.merge(enabled: true, child: const _TickingWidget()),
),
);
final _TickingWidgetState state = tester.state<_TickingWidgetState>(
find.byType(_TickingWidget),
);
// enabled uses AND semantics - child cannot re-enable when parent disables
expect(state.ticker.muted, isTrue);
// forceFrames should be false (inherited)
expect(state.ticker.forceFrames, isFalse);
});
testWidgets('TickerMode.merge can disable when parent is enabled', (WidgetTester tester) async {
// Test that merge can set enabled=false when parent is enabled=true
await tester.pumpWidget(
TickerMode(
enabled: true,
child: TickerMode.merge(enabled: false, child: const _TickingWidget()),
),
);
final _TickingWidgetState state = tester.state<_TickingWidgetState>(
find.byType(_TickingWidget),
);
// enabled=false overrides parent's enabled=true (AND: true && false = false)
expect(state.ticker.muted, isTrue);
// forceFrames should be false (inherited)
expect(state.ticker.forceFrames, isFalse);
});
testWidgets('TickerMode.merge with no ancestor uses fallback values', (
WidgetTester tester,
) async {
await tester.pumpWidget(TickerMode.merge(forceFrames: true, child: const _TickingWidget()));
final _TickingWidgetState state = tester.state<_TickingWidgetState>(
find.byType(_TickingWidget),
);
// enabled should be true (fallback)
expect(state.ticker.muted, isFalse);
// forceFrames should be true (merged override)
expect(state.ticker.forceFrames, isTrue);
});
testWidgets('TickerMode.valuesOf returns correct values', (WidgetTester tester) async {
late TickerModeData capturedValues;
await tester.pumpWidget(
TickerMode(
enabled: false,
forceFrames: true,
child: Builder(
builder: (BuildContext context) {
capturedValues = TickerMode.valuesOf(context);
return Container();
},
),
),
);
expect(capturedValues.enabled, isFalse);
expect(capturedValues.forceFrames, isTrue);
});
testWidgets('TickerMode.valuesOf returns fallback when no ancestor', (WidgetTester tester) async {
late TickerModeData capturedValues;
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
capturedValues = TickerMode.valuesOf(context);
return Container();
},
),
);
expect(capturedValues.enabled, isTrue);
expect(capturedValues.forceFrames, isFalse);
expect(capturedValues, equals(TickerModeData.fallback));
});
testWidgets('TickerMode.getValuesNotifier notifies listeners', (WidgetTester tester) async {
late ValueListenable<TickerModeData> notifier;
final notifiedValues = <TickerModeData>[];
await tester.pumpWidget(
TickerMode(
enabled: true,
child: Builder(
builder: (BuildContext context) {
notifier = TickerMode.getValuesNotifier(context);
return Container();
},
),
),
);
notifier.addListener(() {
notifiedValues.add(notifier.value);
});
expect(notifier.value.enabled, isTrue);
expect(notifier.value.forceFrames, isFalse);
// Change forceFrames
await tester.pumpWidget(
TickerMode(
enabled: true,
forceFrames: true,
child: Builder(
builder: (BuildContext context) {
return Container();
},
),
),
);
expect(notifiedValues.length, 1);
expect(notifiedValues.last.enabled, isTrue);
expect(notifiedValues.last.forceFrames, isTrue);
});
test('TickerModeData equality works correctly', () {
const TickerModeData data1 = TickerModeData.fallback;
const TickerModeData data2 = TickerModeData.fallback;
const data3 = TickerModeData(enabled: false, forceFrames: false);
const data4 = TickerModeData(enabled: true, forceFrames: true);
expect(data1, equals(data2));
expect(data1, isNot(equals(data3)));
expect(data1, isNot(equals(data4)));
expect(data1.hashCode, equals(data2.hashCode));
expect(data1, equals(TickerModeData.fallback));
});
testWidgets('Deprecated TickerMode.of still works', (WidgetTester tester) async {
late bool capturedEnabled;
await tester.pumpWidget(
TickerMode(
enabled: false,
forceFrames: true,
child: Builder(
builder: (BuildContext context) {
// ignore: deprecated_member_use
capturedEnabled = TickerMode.of(context);
return Container();
},
),
),
);
expect(capturedEnabled, isFalse);
});
testWidgets('Deprecated TickerMode.getNotifier still works', (WidgetTester tester) async {
late ValueListenable<bool> notifier;
await tester.pumpWidget(
TickerMode(
enabled: false,
forceFrames: true,
child: Builder(
builder: (BuildContext context) {
// ignore: deprecated_member_use
notifier = TickerMode.getNotifier(context);
return Container();
},
),
),
);
expect(notifier.value, isFalse);
});
}
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();
}
}