From 818c1046f36cc62914b084b43aa634cc5848aa14 Mon Sep 17 00:00:00 2001 From: Rijad Zuzo Date: Wed, 18 Feb 2026 00:13:45 +0100 Subject: [PATCH] Add interactive graph gestures and person insights drilldown --- docs/progress.md | 32 + lib/features/dashboard/dashboard_view.dart | 822 +++++++++++++++--- .../dashboard_graph_interactions_test.dart | 55 ++ 3 files changed, 809 insertions(+), 100 deletions(-) create mode 100644 test/features/dashboard/dashboard_graph_interactions_test.dart diff --git a/docs/progress.md b/docs/progress.md index c09f9f7..dc4baaa 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -2,6 +2,38 @@ Updated: 2026-02-17 +## Latest Milestone (2026-02-17): Interactive Relationship Graph + Person Insights Drilldown + +Upgraded the dashboard graph from static visualization to interactive +navigation: + +- Graph interactions: + - `lib/features/dashboard/dashboard_view.dart` + - graph area now supports: + - pan by drag gesture + - pinch-to-zoom scaling + - long-press on person bubbles +- Long-press drilldown behavior: + - opens a dedicated person insights page with back navigation to the graph + - page sections are category-separated: + - Ideas + - Promotions & Signals + - Moments + - Reminders + - Tasks & Follow-Ups + - includes profile summary (relationship type, affinity, tags, notes) +- Added interaction test coverage: + - `test/features/dashboard/dashboard_graph_interactions_test.dart` + - verifies: + - graph contains interactive viewer + - long-press on a person node opens insights screen + - back button returns to graph + +Validation for this milestone: + +- `flutter analyze` -> pass +- `flutter test` -> pass + ## Latest Milestone (2026-02-17): Obsidian-Style Relationship Graph on Dashboard Reworked the initial Dashboard section to lead with a relationship network diff --git a/lib/features/dashboard/dashboard_view.dart b/lib/features/dashboard/dashboard_view.dart index 474ef78..4206943 100644 --- a/lib/features/dashboard/dashboard_view.dart +++ b/lib/features/dashboard/dashboard_view.dart @@ -6,6 +6,8 @@ import 'package:relationship_saver/core/config/app_theme.dart'; import 'package:relationship_saver/features/local/local_models.dart'; import 'package:relationship_saver/features/local/local_repository.dart'; import 'package:relationship_saver/features/shared/frosted_card.dart'; +import 'package:relationship_saver/features/signals/signals_controller.dart'; +import 'package:relationship_saver/integrations/backend/models/backend_models.dart'; class DashboardView extends ConsumerWidget { const DashboardView({super.key}); @@ -207,47 +209,69 @@ class _RelationshipGraphCard extends StatelessWidget { compact: compact, ); - return Stack( - children: [ - Positioned.fill( - child: CustomPaint( - painter: _RelationshipEdgePainter( - center: layout.center, - edges: layout.edges, - ), - ), - ), - ...layout.nodes.map( - (_GraphNode node) => - _GraphBubble(node: node, compact: compact), - ), - _CenterNode(center: layout.center, compact: compact), - if (layout.hiddenCount > 0) - Positioned( - right: 12, - top: 12, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 6, - ), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.85), - borderRadius: BorderRadius.circular(99), - ), - child: Text( - '+${layout.hiddenCount} more', - style: Theme.of(context).textTheme.labelLarge - ?.copyWith(color: AppTheme.textSecondary), + return InteractiveViewer( + minScale: 0.65, + maxScale: 2.8, + boundaryMargin: const EdgeInsets.all(220), + panEnabled: true, + scaleEnabled: true, + child: SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + child: Stack( + children: [ + Positioned.fill( + child: CustomPaint( + painter: _RelationshipEdgePainter( + center: layout.center, + edges: layout.edges, + ), ), ), - ), - ], + ...layout.nodes.map( + (_GraphNode node) => _GraphBubble( + node: node, + compact: compact, + onLongPress: () => + _openPersonInsights(context, node.person), + ), + ), + _CenterNode(center: layout.center, compact: compact), + if (layout.hiddenCount > 0) + Positioned( + right: 12, + top: 12, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.85), + borderRadius: BorderRadius.circular(99), + ), + child: Text( + '+${layout.hiddenCount} more', + style: Theme.of(context).textTheme.labelLarge + ?.copyWith(color: AppTheme.textSecondary), + ), + ), + ), + ], + ), + ), ); }, ), ), ), + const SizedBox(height: 8), + Text( + 'Pinch to zoom, drag to pan, long-press a person to open insights.', + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary), + ), const SizedBox(height: 10), Wrap( spacing: 8, @@ -285,6 +309,16 @@ class _RelationshipGraphCard extends StatelessWidget { } return result; } + + void _openPersonInsights(BuildContext context, PersonProfile person) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return _PersonInsightsPage(personId: person.id); + }, + ), + ); + } } class _GraphLayout { @@ -474,10 +508,15 @@ class _RelationshipEdgePainter extends CustomPainter { } class _GraphBubble extends StatelessWidget { - const _GraphBubble({required this.node, required this.compact}); + const _GraphBubble({ + required this.node, + required this.compact, + required this.onLongPress, + }); final _GraphNode node; final bool compact; + final VoidCallback onLongPress; @override Widget build(BuildContext context) { @@ -489,79 +528,85 @@ class _GraphBubble extends StatelessWidget { top: node.position.dy - (diameter / 2), child: Tooltip( message: '${node.person.name} • ${node.visual.label}', - child: SizedBox( - width: labelWidth, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - clipBehavior: Clip.none, - children: [ - Container( - width: diameter, - height: diameter, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - colors: [ - _shiftLightness(node.visual.color, 0.16), - _shiftLightness(node.visual.color, -0.12), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: node.visual.color.withValues(alpha: 0.24), - blurRadius: 14, - offset: const Offset(0, 8), - ), - ], - ), - alignment: Alignment.center, - child: Text( - _initials(node.person.name), - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Colors.white, - fontWeight: FontWeight.w700, - ), - ), - ), - Positioned( - right: -2, - top: -2, - child: Container( - width: compact ? 20 : 22, - height: compact ? 20 : 22, + child: GestureDetector( + key: ValueKey('graph-node-${node.person.id}'), + behavior: HitTestBehavior.translucent, + onLongPress: onLongPress, + child: SizedBox( + width: labelWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: diameter, + height: diameter, decoration: BoxDecoration( shape: BoxShape.circle, - color: Colors.white, - border: Border.all( - color: node.visual.color.withValues(alpha: 0.8), + gradient: LinearGradient( + colors: [ + _shiftLightness(node.visual.color, 0.16), + _shiftLightness(node.visual.color, -0.12), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: node.visual.color.withValues(alpha: 0.24), + blurRadius: 14, + offset: const Offset(0, 8), + ), + ], + ), + alignment: Alignment.center, + child: Text( + _initials(node.person.name), + style: Theme.of(context).textTheme.titleMedium + ?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w700, + ), + ), + ), + Positioned( + right: -2, + top: -2, + child: Container( + width: compact ? 20 : 22, + height: compact ? 20 : 22, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + border: Border.all( + color: node.visual.color.withValues(alpha: 0.8), + ), + ), + child: Icon( + node.visual.icon, + size: compact ? 12 : 13, + color: node.visual.color, ), ), - child: Icon( - node.visual.icon, - size: compact ? 12 : 13, - color: node.visual.color, - ), + ), + ], + ), + if (!compact) ...[ + const SizedBox(height: 5), + Text( + _shortName(node.person.name), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: AppTheme.textSecondary, ), ), ], - ), - if (!compact) ...[ - const SizedBox(height: 5), - Text( - _shortName(node.person.name), - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: AppTheme.textSecondary, - ), - ), ], - ], + ), ), ), ), @@ -652,6 +697,583 @@ class _RelationshipLegendPill extends StatelessWidget { } } +class _PersonInsightsPage extends ConsumerWidget { + const _PersonInsightsPage({required this.personId}); + + final String personId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final AsyncValue localData = ref.watch( + localRepositoryProvider, + ); + final AsyncValue signals = ref.watch( + signalsControllerProvider, + ); + + return Scaffold( + appBar: AppBar(title: const Text('Person Insights')), + body: localData.when( + data: (LocalDataState data) { + final PersonProfile? person = _findPersonById(data.people, personId); + if (person == null) { + return Center( + child: Text( + 'Person was not found in local data.', + style: Theme.of(context).textTheme.bodyLarge, + ), + ); + } + + final List moments = + data.moments + .where( + (RelationshipMoment moment) => moment.personId == person.id, + ) + .toList(growable: false) + ..sort( + (RelationshipMoment a, RelationshipMoment b) => + b.at.compareTo(a.at), + ); + final List ideas = + data.ideas + .where((RelationshipIdea idea) => idea.personId == person.id) + .toList(growable: false) + ..sort( + (RelationshipIdea a, RelationshipIdea b) => + b.createdAt.compareTo(a.createdAt), + ); + final List reminders = + data.reminders + .where( + (ReminderRule reminder) => reminder.personId == person.id, + ) + .toList(growable: false) + ..sort( + (ReminderRule a, ReminderRule b) => + a.nextAt.compareTo(b.nextAt), + ); + final List relatedTasks = _findTasksForPerson( + data.tasks, + person, + ); + + final List personSignals = + signals.asData?.value.items + .where((SignalItem item) => item.personId == person.id) + .toList(growable: false) ?? + const []; + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final bool compact = constraints.maxWidth < 760; + return SingleChildScrollView( + padding: EdgeInsets.fromLTRB( + compact ? 16 : 24, + compact ? 16 : 20, + compact ? 16 : 24, + compact ? 24 : 30, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _personProfileCard(context, person, compact), + const SizedBox(height: 14), + _InsightSectionCard( + title: 'Ideas', + icon: Icons.lightbulb_outline_rounded, + children: ideas.isEmpty + ? [ + const _SectionEmpty( + label: + 'No captured ideas yet for this relationship.', + ), + ] + : ideas + .map( + (RelationshipIdea idea) => _InsightRow( + title: idea.title, + subtitle: idea.details, + meta: + '${idea.type.name.toUpperCase()} • ${_shortDate(idea.createdAt)}', + ), + ) + .toList(growable: false), + ), + const SizedBox(height: 12), + _InsightSectionCard( + title: 'Promotions & Signals', + icon: Icons.local_offer_outlined, + children: _signalRows( + context: context, + signals: personSignals, + sourceState: signals, + ), + ), + const SizedBox(height: 12), + _InsightSectionCard( + title: 'Moments', + icon: Icons.auto_awesome_rounded, + children: moments.isEmpty + ? [ + const _SectionEmpty( + label: 'No logged moments for this person yet.', + ), + ] + : moments + .map( + (RelationshipMoment moment) => _InsightRow( + title: moment.title, + subtitle: moment.summary, + meta: + '${moment.type.toUpperCase()} • ${_shortDate(moment.at)}', + ), + ) + .toList(growable: false), + ), + const SizedBox(height: 12), + _InsightSectionCard( + title: 'Reminders', + icon: Icons.notifications_active_outlined, + children: reminders.isEmpty + ? [ + const _SectionEmpty( + label: + 'No reminders configured for this relationship.', + ), + ] + : reminders + .map( + (ReminderRule reminder) => _InsightRow( + title: reminder.title, + subtitle: reminder.enabled + ? 'Enabled' + : 'Disabled', + meta: + '${reminder.cadence.name.toUpperCase()} • Next ${_shortDate(reminder.nextAt)}', + ), + ) + .toList(growable: false), + ), + const SizedBox(height: 12), + _InsightSectionCard( + title: 'Tasks & Follow-Ups', + icon: Icons.checklist_rounded, + children: relatedTasks.isEmpty + ? [ + const _SectionEmpty( + label: + 'No direct follow-up tasks found for this person.', + ), + ] + : relatedTasks + .map( + (DashboardTask task) => _InsightRow( + title: task.title, + subtitle: task.description, + meta: + '${task.done ? 'DONE' : 'PENDING'} • ${_shortDate(task.when)}', + ), + ) + .toList(growable: false), + ), + ], + ), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (Object error, StackTrace stackTrace) { + return Center( + child: Text( + 'Could not load person insights.', + style: Theme.of(context).textTheme.bodyLarge, + ), + ); + }, + ), + ); + } + + Widget _personProfileCard( + BuildContext context, + PersonProfile person, + bool compact, + ) { + final _RelationshipVisual visual = _relationshipVisualFor( + person.relationship, + ); + + return FrostedCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (compact) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + _insightAvatar(person: person, visual: visual), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + person.name, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 3), + Text( + person.relationship, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: AppTheme.textSecondary), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _miniMetric( + label: 'Affinity', + value: '${person.affinityScore}%', + ), + _miniMetric( + label: 'Next Moment', + value: _shortDate(person.nextMoment), + ), + ], + ), + ], + ) + else + Row( + children: [ + _insightAvatar(person: person, visual: visual), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + person.name, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 3), + Text( + person.relationship, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppTheme.textSecondary, + ), + ), + ], + ), + ), + _miniMetric( + label: 'Affinity', + value: '${person.affinityScore}%', + ), + const SizedBox(width: 10), + _miniMetric( + label: 'Next Moment', + value: _shortDate(person.nextMoment), + ), + ], + ), + if (person.tags.isNotEmpty) ...[ + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: person.tags + .map((String tag) => _TagPill(label: tag)) + .toList(growable: false), + ), + ], + if (person.notes.trim().isNotEmpty) ...[ + const SizedBox(height: 12), + Text( + person.notes, + style: Theme.of( + context, + ).textTheme.bodyLarge?.copyWith(color: AppTheme.textSecondary), + ), + ], + ], + ), + ); + } + + Widget _insightAvatar({ + required PersonProfile person, + required _RelationshipVisual visual, + }) { + return Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [ + _shiftLightness(visual.color, 0.16), + _shiftLightness(visual.color, -0.12), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + alignment: Alignment.center, + child: Text( + _initials(person.name), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + fontSize: 16, + ), + ), + ); + } + + Widget _miniMetric({required String label, required String value}) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF2F8FB), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle(fontSize: 11, color: AppTheme.textSecondary), + ), + const SizedBox(height: 2), + Text( + value, + style: const TextStyle( + fontWeight: FontWeight.w700, + color: AppTheme.textPrimary, + ), + ), + ], + ), + ); + } + + List _signalRows({ + required BuildContext context, + required List signals, + required AsyncValue sourceState, + }) { + if (sourceState.isLoading && sourceState.asData == null) { + return const [ + Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: LinearProgressIndicator(minHeight: 3), + ), + ]; + } + + if (signals.isEmpty) { + if (sourceState.hasError) { + return [ + const _SectionEmpty( + label: + 'Signals unavailable right now. Backend sync can repopulate.', + ), + ]; + } + return [ + const _SectionEmpty( + label: 'No promotions/signals targeted to this person yet.', + ), + ]; + } + + return signals + .map( + (SignalItem item) => _InsightRow( + title: item.title, + subtitle: item.description ?? 'No description', + meta: '${item.type.toUpperCase()} • ${_shortDate(item.createdAt)}', + ), + ) + .toList(growable: false); + } + + PersonProfile? _findPersonById(List people, String id) { + for (final PersonProfile person in people) { + if (person.id == id) { + return person; + } + } + return null; + } + + List _findTasksForPerson( + List tasks, + PersonProfile person, + ) { + final List tokens = person.name + .toLowerCase() + .split(' ') + .where((String token) => token.length > 2) + .toList(growable: false); + if (tokens.isEmpty) { + return const []; + } + return tasks + .where((DashboardTask task) { + final String fullText = '${task.title} ${task.description}' + .toLowerCase(); + for (final String token in tokens) { + if (fullText.contains(token)) { + return true; + } + } + return false; + }) + .toList(growable: false) + ..sort((DashboardTask a, DashboardTask b) => a.when.compareTo(b.when)); + } +} + +class _InsightSectionCard extends StatelessWidget { + const _InsightSectionCard({ + required this.title, + required this.icon, + required this.children, + }); + + final String title; + final IconData icon; + final List children; + + @override + Widget build(BuildContext context) { + return FrostedCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 8, + runSpacing: 6, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Icon(icon, size: 18, color: AppTheme.primary), + Text(title, style: Theme.of(context).textTheme.titleMedium), + ], + ), + const SizedBox(height: 10), + ...children, + ], + ), + ); + } +} + +class _InsightRow extends StatelessWidget { + const _InsightRow({ + required this.title, + required this.subtitle, + required this.meta, + }); + + final String title; + final String subtitle; + final String meta; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFF5FAFB), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: Theme.of(context).textTheme.titleSmall), + const SizedBox(height: 4), + Text( + subtitle, + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary), + ), + const SizedBox(height: 6), + Text( + meta, + style: Theme.of( + context, + ).textTheme.labelSmall?.copyWith(color: AppTheme.textSecondary), + ), + ], + ), + ), + ); + } +} + +class _SectionEmpty extends StatelessWidget { + const _SectionEmpty({required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFF5FAFB), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + label, + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary), + ), + ); + } +} + +class _TagPill extends StatelessWidget { + const _TagPill({required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFEFF7FA), + borderRadius: BorderRadius.circular(99), + ), + child: Text( + label, + style: Theme.of( + context, + ).textTheme.labelLarge?.copyWith(color: AppTheme.textSecondary), + ), + ); + } +} + class _StatCard extends StatelessWidget { const _StatCard({ required this.label, diff --git a/test/features/dashboard/dashboard_graph_interactions_test.dart b/test/features/dashboard/dashboard_graph_interactions_test.dart new file mode 100644 index 0000000..64fae2e --- /dev/null +++ b/test/features/dashboard/dashboard_graph_interactions_test.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:relationship_saver/core/config/app_theme.dart'; +import 'package:relationship_saver/features/dashboard/dashboard_view.dart'; +import 'package:relationship_saver/features/local/storage/local_data_store_in_memory.dart'; +import 'package:relationship_saver/features/local/storage/local_data_store_provider.dart'; +import 'package:relationship_saver/features/sync/storage/sync_state_store_in_memory.dart'; +import 'package:relationship_saver/features/sync/storage/sync_state_store_provider.dart'; + +void main() { + testWidgets( + 'graph supports pan/zoom shell and opens person insights on long press', + (WidgetTester tester) async { + await tester.binding.setSurfaceSize(const Size(390, 844)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + localDataStoreProvider.overrideWithValue(InMemoryLocalDataStore()), + syncStateStoreProvider.overrideWithValue(InMemorySyncStateStore()), + ], + child: MaterialApp( + theme: AppTheme.light(), + home: const Scaffold(body: DashboardView()), + ), + ), + ); + await tester.pumpAndSettle(const Duration(milliseconds: 600)); + + expect(find.text('Relationship Graph'), findsOneWidget); + expect(find.byType(InteractiveViewer), findsOneWidget); + + final Finder avaNode = find.byKey( + const ValueKey('graph-node-p-ava'), + ); + expect(avaNode, findsOneWidget); + + await tester.longPress(avaNode); + await tester.pumpAndSettle(const Duration(milliseconds: 700)); + + expect(find.text('Person Insights'), findsOneWidget); + expect(find.text('Promotions & Signals'), findsOneWidget); + expect(find.text('Ideas'), findsOneWidget); + expect(find.text('Moments'), findsOneWidget); + expect(find.text('Reminders'), findsOneWidget); + + await tester.tap(find.byTooltip('Back')); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + + expect(find.text('Relationship Graph'), findsOneWidget); + }, + ); +}