Skip to content

Bootstrap + routing tests: main.dart, router config, DI wiring, deep links #546

@TaprootFreak

Description

@TaprootFreak

Context

The bootstrap layer is untested today:

File Lines Direct tests
lib/main.dart 140 0
lib/setup/routing/router_config.dart 303 0
lib/setup/di.dart 254 0 (consumed transitively)
lib/setup/lifecycle_initializer.dart 76 1 file, 3 testWidgets cases (hidden, paused, resumed)
lib/setup/routing/routes/*.dart 53 total 0

A wrong DI registration, a broken route name, or a missed guard transition makes the app crash on cold start. Today no spec catches this before the iOS/Android build.

How navigation actually works in the repo

This is intentionally spelled out because previous iterations assumed GoRouter redirects — that's not how it works here:

  • router_config.dart:51-303 contains zero redirect: clauses on GoRouter or on any GoRoute. The route table is flat name-and-path declarations only.
  • Routing decisions are imperative in lib/main.dart:113-138 (_WalletAppState._navigate()). It reads HomeBloc.state + PinAuthCubit.state and calls routerConfig.goNamed(targetRoute).
  • The ladder has eight branches (one early-return plus seven explicit conditions):
    1. isLoadingWallet → early return, no navigation (lib/main.dart:117)
    2. !softwareTermsAcceptedAppRoutes.home
    3. !hasWalletOnboardingRoutes.welcome
    4. !onboardingCompletedOnboardingRoutes.completed
    5. !isPinSetupPinRoutes.setup
    6. !isPinVerifiedPinRoutes.verify
    7. openWallet == null → trigger LoadCurrentWalletEvent and wait
    8. otherwise → AppRoutes.dashboard

Tests must drive HomeBloc / PinAuthCubit states and assert which goNamed(...) fires, not mock GoRouter.redirect.

Actual route paths (taken from router_config.dart:51-303)

/home (initial), /welcome, /createWallet, /restoreWallet, /verifySeed, /onboardingComplete, /debugAuth (kDebugMode-only at lib/setup/routing/router_config.dart:90-95), /pinGate, /setupPin, /verifyPin, /dashboard (+ child transactionHistory), /buy, /sell, /sellBitbox, /legalDisclaimer, /legalDocument, /termsOfUse, /kyc, /receive, /settings (+ 11 direct children including userData which has 3 sub-children), /support (+ tickets, create, chat/:uid), /webView.

Route paths are camelCase. Route names (e.g. AppRoutes.dashboard = 'dashboard') and route paths (/dashboard) are two separate namespaces — tests should use the named API. Note also that the route name LegalRoutes.terms = 'terms' maps to path /termsOfUse.

Scope

setup_di_test.dart (new)

  • Boot the GetIt registration chain in lib/setup/di.dart (setupBlocs ends at line 223 — the section through line 223 is the load-bearing registration; lines 224-254 are unrelated migration helpers). Assert every registered type resolves (getIt<WalletService>(), getIt<DfxAuthService>(), every cubit factory). Mock path_provider, FlutterSecureStorage, and the Drift AppDatabase so finishSetup() doesn't open native channels.
  • Assert that swapping NetworkMode.mainnet ↔ testnet (defined at lib/packages/config/network_mode.dart:4-20) rebinds: apiHost (api.dfx.swissdev.api.dfx.swiss), asset, ethAssetId, zchfAssetId (all per lib/packages/config/api_config.dart:13-22). Storage and wallet bindings (di.dart:78-93) are environment-independent.

router_config_test.dart (new)

  • One named-route spec per top-level path listed above. Each spec: pump MaterialApp.router(routerConfig: routerConfig), navigate via context.goNamed(...) using the constants in routes/*.dart, assert the expected page widget rendered.
  • Cover the sub-route trees: /dashboard/transactionHistory, /support/tickets, /support/create, /support/chat/:uid, and every /settings/* child (11 direct + 3 user_data sub-children).
  • Cover the kDebugMode guard on /debugAuth — in release-mode builds the route must not be registered.

main_navigate_test.dart (new)

  • Cover all eight branches of _navigate() (lib/main.dart:113-138) by faking HomeBloc.state and PinAuthCubit.state. The first branch is the isLoadingWallet early-return — assert no navigation fires.
  • For branch 7 (wallet-not-loaded with hasWallet == true), assert LoadCurrentWalletEvent is added and no navigation fires.

lifecycle_initializer_test.dart (extend)

  • Existing spec covers hiddenwalletService.lockCurrentWallet(). paused is NOT a no-op_onPaused() at lib/setup/lifecycle_initializer.dart:64-66 calls BalanceService.cancelSync(). Current test only asserts lockCurrentWallet is NOT called; add an explicit assertion that cancelSync IS called.
  • resumed branch: no-op today; assert nothing fires (regression guard for unexpected resume logic creep).
  • The previously-proposed extensions (locale-change handler, biometric re-prompt timing) describe behaviour that does not exist in lifecycle_initializer.dart. Drop them from this issue or implement the behaviour first and then test it.

Deep-link / custom-URI handling

The repo is in an inconsistent state today:

  • ios/Runner/Info.plist:25-37 declares CFBundleURLTypes with scheme realunit-wallet (URL name realunit) and FlutterDeepLinkingEnabled: true (line 40-41).
  • android/app/src/main/AndroidManifest.xml:36-39 has only the LAUNCHER intent — no <intent-filter> with android:scheme.
  • lib/** has no Dart handler for getInitialLink/uni_links/app_links.

This is owned by a separate decision-issue (see Related). When that lands, the deep-link tests get added here as a follow-up commit.

Acceptance criteria

  • New test files live under test/setup/ (mirror lib structure)
  • All specs run inside the existing Analyze & Test job; no native plugin channels opened in test
  • The eight-branch coverage of _navigate() is the load-bearing assertion — if any branch lacks an explicit spec, the issue is not done
  • Mock surface documented at the top of each new test file

Estimated effort

Sub-task Days
setup_di_test.dart (incl. mocking platform channels) 1.5
router_config_test.dart (22 top-level + child routes) 1.5
main_navigate_test.dart (8 branches) 0.5
lifecycle_initializer_test.dart extension (cancelSync assertion) 0.25
Allow-list / convention docs in docs/testing.md 0.25
Total ~4 engineer-days

Excludes the deep-link decision (separate issue) and any handler implementation (additional 2-4 days if Option B is picked).

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions