Milad-Akarie / auto_route_library

Flutter route generator
MIT License
1.56k stars 394 forks source link

`AutoLeadingButton` is not testable #1806

Open mrverdant13 opened 9 months ago

mrverdant13 commented 9 months ago

Hi 👋🏼

I am trying to test an AutoLeadingButton as follows:

// Using `mocktail`

class MockStackRouter extends Mock implements StackRouter {}

testWidgets(
  'AutoLeadingButton test',
  (tester) async {
    final stackRouter = MockStackRouter();
    when(stackRouter.canPop).thenReturn(false);
    await tester.pumpWidget(
      MaterialApp(
        home: StackRouterScope(
          controller: stackRouter,
          stateHash: 0,
          child: const AutoLeadingButton(),
        ),
      ),
    );
  },
);

This approach works just fine for other navigation-related tests, but it throws the following exception when used for the AutoLeadingButton:

══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following _TypeError was thrown building StackRouterScope:
type 'Null' is not a subtype of type 'PagelessRoutesObserver'

The relevant error-causing widget was:
  StackRouterScope
  StackRouterScope:file:///path/to/auto_leading_button_test.dart

When the exception was thrown, this was the stack:
#0      MockStackRouter.pagelessRoutesObserver (file:///path/to/test_helpers/routing_helpers.dart)
#1      _AutoLeadingButtonState.initState (package:auto_route/src/router/widgets/auto_leading_button.dart:99:54)
#2      StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5602:55)
#3      ComponentElement.mount (package:flutter/src/widgets/framework.dart:5447:5)
...     Normal element mounting (226 frames)
#229    Element.inflateWidget (package:flutter/src/widgets/framework.dart:4326:16)
#230    MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:6871:36)
#231    MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6883:32)
...     Normal element mounting (447 frames)
#678    Element.inflateWidget (package:flutter/src/widgets/framework.dart:4326:16)
#679    Element.updateChild (package:flutter/src/widgets/framework.dart:3831:20)
#680    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5496:16)
#681    Element.rebuild (package:flutter/src/widgets/framework.dart:5187:7)
#682    ProxyElement.update (package:flutter/src/widgets/framework.dart:5800:5)
#683    Element.updateChild (package:flutter/src/widgets/framework.dart:3815:15)
#684    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5496:16)
#685    StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5634:11)
#686    Element.rebuild (package:flutter/src/widgets/framework.dart:5187:7)
#687    StatefulElement.update (package:flutter/src/widgets/framework.dart:5657:5)
#688    Element.updateChild (package:flutter/src/widgets/framework.dart:3815:15)
#689    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5496:16)
#690    Element.rebuild (package:flutter/src/widgets/framework.dart:5187:7)
#691    ProxyElement.update (package:flutter/src/widgets/framework.dart:5800:5)
#692    Element.updateChild (package:flutter/src/widgets/framework.dart:3815:15)
#693    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5496:16)
#694    Element.rebuild (package:flutter/src/widgets/framework.dart:5187:7)
#695    ProxyElement.update (package:flutter/src/widgets/framework.dart:5800:5)
#696    Element.updateChild (package:flutter/src/widgets/framework.dart:3815:15)
#697    _RawViewElement._updateChild (package:flutter/src/widgets/view.dart:289:16)
#698    _RawViewElement.update (package:flutter/src/widgets/view.dart:376:5)
#699    Element.updateChild (package:flutter/src/widgets/framework.dart:3815:15)
#700    ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5496:16)
#701    Element.rebuild (package:flutter/src/widgets/framework.dart:5187:7)
#702    StatelessElement.update (package:flutter/src/widgets/framework.dart:5547:5)
#703    Element.updateChild (package:flutter/src/widgets/framework.dart:3815:15)
#704    RootElement._rebuild (package:flutter/src/widgets/binding.dart:1334:16)
#705    RootElement.update (package:flutter/src/widgets/binding.dart:1312:5)
#706    RootElement.performRebuild (package:flutter/src/widgets/binding.dart:1326:7)
#707    Element.rebuild (package:flutter/src/widgets/framework.dart:5187:7)
#708    BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2895:19)
#709    AutomatedTestWidgetsFlutterBinding.drawFrame (package:flutter_test/src/binding.dart:1409:19)
#710    RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:457:5)
#711    SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1325:15)
#712    SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1255:9)
#713    AutomatedTestWidgetsFlutterBinding.pump.<anonymous closure> (package:flutter_test/src/binding.dart:1264:9)
#716    TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#717    AutomatedTestWidgetsFlutterBinding.pump (package:flutter_test/src/binding.dart:1251:27)
#718    WidgetTester.pumpWidget.<anonymous closure> (package:flutter_test/src/widget_tester.dart:578:22)
#721    TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#722    WidgetTester.pumpWidget (package:flutter_test/src/widget_tester.dart:575:27)
#723    main.<anonymous closure> (file:///path/to/auto_leading_button_test.dart)
#724    testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:168:29)
<asynchronous suspension>
#725    TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1013:5)
<asynchronous suspension>
<asynchronous suspension>
(elided 5 frames from dart:async and package:stack_trace)

After reviewing the AutoLeadingButton implementation, I found this:

// _AutoLeadingButtonState

@override
void initState() {
  super.initState();
  _pagelessRoutesObserver = AutoRouter.of(context).pagelessRoutesObserver;
  _pagelessRoutesObserver.addListener(_handleRebuild);
}

So I tried to update the test code as required, but seems like the PagelessRoutesObserver class is not actually exposed by the package itself, so this would make the AutoLeadingButton widget completely untestable, nor by mocking the class nor by bypassing the access to that instance.

Is there anything that I am missing here? Is there any other alternative to test this widget?

Milad-Akarie commented 9 months ago

Hey @mrverdant13 you probably need to add a RouterScope to your testing tree as well

digiboridev commented 5 months ago

+1 PagelessRoutesObserver class is not actually exposed by the package itself to make mock of StackRouter for RouterScope and StackRouterScope.

Is there any solutions? @Milad-Akarie

Example:

import 'package:auto_route/src/router/controller/pageless_routes_observer.dart'; // BAD BAD LINE

//  Mock auto_route router stack
class MockR extends Mock implements StackRouter {
  @override
  PagelessRoutesObserver pagelessRoutesObserver = PagelessRoutesObserver();

  @override
  bool canPop({bool ignoreChildRoutes = false, bool ignoreParentRoutes = false, bool ignorePagelessRoutes = false}) {
    return true;
  }
}

Widget stackRouterWrap(Widget child) {
  return RouterScope(
    controller: MockR(),
    stateHash: 0,
    inheritableObserversBuilder: () => [],
    child: StackRouterScope(
      controller: MockR(),
      stateHash: 0,
      child: child,
    ),
  );
}