// 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/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'test_widgets.dart'; class TestInherited extends InheritedWidget { const TestInherited({super.key, required super.child, this.shouldNotify = true}); final bool shouldNotify; @override bool updateShouldNotify(InheritedWidget oldWidget) { return shouldNotify; } } class ValueInherited extends InheritedWidget { const ValueInherited({super.key, required super.child, required this.value}); final int value; @override bool updateShouldNotify(ValueInherited oldWidget) => value != oldWidget.value; } class ExpectFail extends StatefulWidget { const ExpectFail(this.onError, {super.key}); final VoidCallback onError; @override ExpectFailState createState() => ExpectFailState(); } class ExpectFailState extends State { @override void initState() { super.initState(); try { context.dependOnInheritedWidgetOfExactType(); // should fail } catch (e) { widget.onError(); } } @override Widget build(BuildContext context) => Container(); } class ChangeNotifierInherited extends InheritedNotifier { const ChangeNotifierInherited({super.key, required super.child, super.notifier}); } class ThemedCard extends SingleChildRenderObjectWidget { const ThemedCard({super.key}) : super(child: const SizedBox.expand()); @override RenderPhysicalShape createRenderObject(BuildContext context) { final CardThemeData cardTheme = CardTheme.of(context); return RenderPhysicalShape( clipper: ShapeBorderClipper(shape: cardTheme.shape ?? const RoundedRectangleBorder()), clipBehavior: cardTheme.clipBehavior ?? Clip.antiAlias, color: cardTheme.color ?? Colors.white, elevation: cardTheme.elevation ?? 0.0, shadowColor: cardTheme.shadowColor ?? Colors.black, ); } @override void updateRenderObject(BuildContext context, RenderPhysicalShape renderObject) { final CardThemeData cardTheme = CardTheme.of(context); renderObject ..clipper = ShapeBorderClipper(shape: cardTheme.shape ?? const RoundedRectangleBorder()) ..clipBehavior = cardTheme.clipBehavior ?? Clip.antiAlias ..color = cardTheme.color ?? Colors.white ..elevation = cardTheme.elevation ?? 0.0 ..shadowColor = cardTheme.shadowColor ?? Colors.black; } } void main() { testWidgets('Inherited notifies dependents', (WidgetTester tester) async { final log = []; final builder = Builder( builder: (BuildContext context) { log.add(context.dependOnInheritedWidgetOfExactType()!); return Container(); }, ); final first = TestInherited(child: builder); await tester.pumpWidget(first); expect(log, equals([first])); final second = TestInherited(shouldNotify: false, child: builder); await tester.pumpWidget(second); expect(log, equals([first])); final third = TestInherited(child: builder); await tester.pumpWidget(third); expect(log, equals([first, third])); }); testWidgets('Update inherited when reparenting state', (WidgetTester tester) async { final GlobalKey globalKey = GlobalKey(); final log = []; TestInherited build() { return TestInherited( key: UniqueKey(), child: Container( key: globalKey, child: Builder( builder: (BuildContext context) { log.add(context.dependOnInheritedWidgetOfExactType()!); return Container(); }, ), ), ); } final TestInherited first = build(); await tester.pumpWidget(first); expect(log, equals([first])); final TestInherited second = build(); await tester.pumpWidget(second); expect(log, equals([first, second])); }); testWidgets('Update inherited when removing node', (WidgetTester tester) async { final log = []; await tester.pumpWidget( ValueInherited( value: 1, child: FlipWidget( left: ValueInherited( value: 2, child: ValueInherited( value: 3, child: Builder( builder: (BuildContext context) { final ValueInherited v = context .dependOnInheritedWidgetOfExactType()!; log.add('a: ${v.value}'); return const Text('', textDirection: TextDirection.ltr); }, ), ), ), right: ValueInherited( value: 2, child: Builder( builder: (BuildContext context) { final ValueInherited v = context .dependOnInheritedWidgetOfExactType()!; log.add('b: ${v.value}'); return const Text('', textDirection: TextDirection.ltr); }, ), ), ), ), ); expect(log, equals(['a: 3'])); log.clear(); await tester.pump(); expect(log, equals([])); log.clear(); flipStatefulWidget(tester); await tester.pump(); expect(log, equals(['b: 2'])); log.clear(); flipStatefulWidget(tester); await tester.pump(); expect(log, equals(['a: 3'])); log.clear(); }); testWidgets('Update inherited when removing node and child has global key', ( WidgetTester tester, ) async { final log = []; final Key key = GlobalKey(); await tester.pumpWidget( ValueInherited( value: 1, child: FlipWidget( left: ValueInherited( value: 2, child: ValueInherited( value: 3, child: Container( key: key, child: Builder( builder: (BuildContext context) { final ValueInherited v = context .dependOnInheritedWidgetOfExactType()!; log.add('a: ${v.value}'); return const Text('', textDirection: TextDirection.ltr); }, ), ), ), ), right: ValueInherited( value: 2, child: Container( key: key, child: Builder( builder: (BuildContext context) { final ValueInherited v = context .dependOnInheritedWidgetOfExactType()!; log.add('b: ${v.value}'); return const Text('', textDirection: TextDirection.ltr); }, ), ), ), ), ), ); expect(log, equals(['a: 3'])); log.clear(); await tester.pump(); expect(log, equals([])); log.clear(); flipStatefulWidget(tester); await tester.pump(); expect(log, equals(['b: 2'])); log.clear(); flipStatefulWidget(tester); await tester.pump(); expect(log, equals(['a: 3'])); log.clear(); }); testWidgets('Update inherited when removing node and child has global key with constant child', ( WidgetTester tester, ) async { final log = []; final Key key = GlobalKey(); final Widget child = Builder( builder: (BuildContext context) { final ValueInherited v = context.dependOnInheritedWidgetOfExactType()!; log.add(v.value); return const Text('', textDirection: TextDirection.ltr); }, ); await tester.pumpWidget( ValueInherited( value: 1, child: FlipWidget( left: ValueInherited( value: 2, child: ValueInherited( value: 3, child: Container(key: key, child: child), ), ), right: ValueInherited( value: 2, child: Container(key: key, child: child), ), ), ), ); expect(log, equals([3])); log.clear(); await tester.pump(); expect(log, equals([])); log.clear(); flipStatefulWidget(tester); await tester.pump(); expect(log, equals([2])); log.clear(); flipStatefulWidget(tester); await tester.pump(); expect(log, equals([3])); log.clear(); }); testWidgets( 'Update inherited when removing node and child has global key with constant child, minimised', (WidgetTester tester) async { final log = []; final Widget child = Builder( key: GlobalKey(), builder: (BuildContext context) { final ValueInherited v = context.dependOnInheritedWidgetOfExactType()!; log.add(v.value); return const Text('', textDirection: TextDirection.ltr); }, ); await tester.pumpWidget( ValueInherited( value: 2, child: FlipWidget( left: ValueInherited(value: 3, child: child), right: child, ), ), ); expect(log, equals([3])); log.clear(); await tester.pump(); expect(log, equals([])); log.clear(); flipStatefulWidget(tester); await tester.pump(); expect(log, equals([2])); log.clear(); flipStatefulWidget(tester); await tester.pump(); expect(log, equals([3])); log.clear(); }, ); testWidgets( 'Inherited widget notifies descendants when descendant previously failed to find a match', (WidgetTester tester) async { int? inheritedValue = -1; final Widget inner = Container( key: GlobalKey(), child: Builder( builder: (BuildContext context) { final ValueInherited? widget = context .dependOnInheritedWidgetOfExactType(); inheritedValue = widget?.value; return Container(); }, ), ); await tester.pumpWidget(inner); expect(inheritedValue, isNull); inheritedValue = -2; await tester.pumpWidget(ValueInherited(value: 3, child: inner)); expect(inheritedValue, equals(3)); }, ); testWidgets( "Inherited widget doesn't notify descendants when descendant did not previously fail to find a match and had no dependencies", (WidgetTester tester) async { var buildCount = 0; final Widget inner = Container( key: GlobalKey(), child: Builder( builder: (BuildContext context) { buildCount += 1; return Container(); }, ), ); await tester.pumpWidget(inner); expect(buildCount, equals(1)); await tester.pumpWidget(ValueInherited(value: 3, child: inner)); expect(buildCount, equals(1)); }, ); testWidgets( 'Inherited widget does notify descendants when descendant did not previously fail to find a match but did have other dependencies', (WidgetTester tester) async { var buildCount = 0; final Widget inner = Container( key: GlobalKey(), child: TestInherited( shouldNotify: false, child: Builder( builder: (BuildContext context) { context.dependOnInheritedWidgetOfExactType(); buildCount += 1; return Container(); }, ), ), ); await tester.pumpWidget(inner); expect(buildCount, equals(1)); await tester.pumpWidget(ValueInherited(value: 3, child: inner)); expect(buildCount, equals(2)); }, ); testWidgets("BuildContext.getInheritedWidgetOfExactType doesn't create a dependency", ( WidgetTester tester, ) async { var buildCount = 0; final GlobalKey inheritedKey = GlobalKey(); final notifier = ChangeNotifier(); addTearDown(notifier.dispose); final Widget builder = Builder( builder: (BuildContext context) { expect( context.getInheritedWidgetOfExactType(), equals(inheritedKey.currentWidget), ); buildCount += 1; return Container(); }, ); final Widget inner = ChangeNotifierInherited( key: inheritedKey, notifier: notifier, child: builder, ); await tester.pumpWidget(inner); expect(buildCount, equals(1)); notifier.notifyListeners(); await tester.pumpWidget(inner); expect(buildCount, equals(1)); }); testWidgets('initState() dependency on Inherited asserts', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/5491 var exceptionCaught = false; final parent = TestInherited( child: ExpectFail(() { exceptionCaught = true; }), ); await tester.pumpWidget(parent); expect(exceptionCaught, isTrue); }); testWidgets('InheritedNotifier', (WidgetTester tester) async { var buildCount = 0; final notifier = ChangeNotifier(); addTearDown(notifier.dispose); final Widget builder = Builder( builder: (BuildContext context) { context.dependOnInheritedWidgetOfExactType(); buildCount += 1; return Container(); }, ); final Widget inner = ChangeNotifierInherited(notifier: notifier, child: builder); await tester.pumpWidget(inner); expect(buildCount, equals(1)); await tester.pumpWidget(inner); expect(buildCount, equals(1)); await tester.pump(); expect(buildCount, equals(1)); notifier.notifyListeners(); await tester.pump(); expect(buildCount, equals(2)); await tester.pumpWidget(inner); expect(buildCount, equals(2)); await tester.pumpWidget(ChangeNotifierInherited(child: builder)); expect(buildCount, equals(3)); }); testWidgets('InheritedWidgets can trigger RenderObject updates', (WidgetTester tester) async { var cardThemeData = const CardThemeData(color: Colors.white); late StateSetter setState; // Verifies that the "themed card" is rendered // with the appropriate inherited theme data. void expectCardToMatchTheme() { final RenderPhysicalShape renderShape = tester.renderObject(find.byType(ThemedCard)); if (cardThemeData.color != null) { expect(renderShape.color, cardThemeData.color); } if (cardThemeData.elevation != null) { expect(renderShape.elevation, cardThemeData.elevation); } if (cardThemeData.shadowColor != null) { expect(renderShape.shadowColor, cardThemeData.shadowColor); } if (cardThemeData.shape != null) { final CustomClipper? clipper = renderShape.clipper; expect(clipper, isA()); expect((clipper! as ShapeBorderClipper).shape, cardThemeData.shape); } if (cardThemeData.clipBehavior != null) { expect(renderShape.clipBehavior, cardThemeData.clipBehavior); } } await tester.pumpWidget( StatefulBuilder( builder: (BuildContext context, StateSetter stateSetter) { setState = stateSetter; return Theme( data: ThemeData(cardTheme: cardThemeData), child: const ThemedCard(), ); }, ), ); expectCardToMatchTheme(); setState(() { cardThemeData = const CardThemeData( shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))), ); }); await tester.pump(); expectCardToMatchTheme(); setState(() { cardThemeData = const CardThemeData(clipBehavior: Clip.hardEdge); }); await tester.pump(); expectCardToMatchTheme(); setState(() { cardThemeData = const CardThemeData( elevation: 5.0, shadowColor: Colors.blueGrey, shape: ContinuousRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))), clipBehavior: Clip.antiAliasWithSaveLayer, ); }); await tester.pump(); expectCardToMatchTheme(); }); }