From e06c2d8f949d80930f6f89484ac866925cf4dada Mon Sep 17 00:00:00 2001 From: Mike Kell Date: Sat, 4 Apr 2026 13:38:51 -0400 Subject: [PATCH] refactor(kell-web): add app-level dependency composition --- .../kell_web/lib/composition/app_scope.dart | 25 ++++++++++ .../lib/composition/app_services.dart | 22 +++++++++ .../apps/kell_web/lib/main.dart | 6 ++- .../apps/kell_web/lib/routing/app_routes.dart | 49 ++++++++++++++----- .../apps/kell_web/test/widget_test.dart | 14 ++++-- .../lib/src/widgets/kc_status_chip.dart | 1 - .../test/design_system_test.dart | 39 ++++++++++++--- .../lib/feature_inventory.dart | 4 +- .../lib/src/presentation/inventory_page.dart | 8 +-- .../packages/feature_inventory/pubspec.yaml | 1 + .../test/feature_inventory_test.dart | 37 ++++++++++++-- .../lib/feature_wordpress.dart | 2 + .../presentation/product_publishing_page.dart | 8 +-- ...ke_product_publishing_repository_test.dart | 1 - .../product_publishing_controller_test.dart | 1 - 15 files changed, 181 insertions(+), 37 deletions(-) create mode 100644 kell_creations_apps/apps/kell_web/lib/composition/app_scope.dart create mode 100644 kell_creations_apps/apps/kell_web/lib/composition/app_services.dart diff --git a/kell_creations_apps/apps/kell_web/lib/composition/app_scope.dart b/kell_creations_apps/apps/kell_web/lib/composition/app_scope.dart new file mode 100644 index 0000000..7cb940c --- /dev/null +++ b/kell_creations_apps/apps/kell_web/lib/composition/app_scope.dart @@ -0,0 +1,25 @@ +import 'package:flutter/widgets.dart'; + +import 'app_services.dart'; + +/// An [InheritedWidget] that exposes [AppServices] to the widget tree. +/// +/// Wrap the app (or a subtree) with [AppScope] and retrieve the services +/// anywhere below via [AppScope.of(context)]. +class AppScope extends InheritedWidget { + final AppServices services; + + const AppScope({super.key, required this.services, required super.child}); + + /// Returns the nearest [AppServices] from the widget tree. + /// + /// Throws if no [AppScope] ancestor is found. + static AppServices of(BuildContext context) { + final scope = context.dependOnInheritedWidgetOfExactType(); + assert(scope != null, 'No AppScope found in the widget tree'); + return scope!.services; + } + + @override + bool updateShouldNotify(AppScope oldWidget) => services != oldWidget.services; +} diff --git a/kell_creations_apps/apps/kell_web/lib/composition/app_services.dart b/kell_creations_apps/apps/kell_web/lib/composition/app_services.dart new file mode 100644 index 0000000..1afa7ff --- /dev/null +++ b/kell_creations_apps/apps/kell_web/lib/composition/app_services.dart @@ -0,0 +1,22 @@ +import 'package:feature_inventory/feature_inventory.dart'; +import 'package:feature_wordpress/feature_wordpress.dart'; + +/// Holds the concrete service implementations used by the app. +/// +/// The [AppServices.fake] factory wires up the in-memory fakes that live +/// inside each feature package. Swap this factory for a real one when +/// production backends are ready. +class AppServices { + final InventoryRepository inventoryRepository; + final ProductPublishingRepository productPublishingRepository; + + const AppServices({required this.inventoryRepository, required this.productPublishingRepository}); + + /// Creates an [AppServices] backed by fake, in-memory repositories. + factory AppServices.fake() { + return AppServices( + inventoryRepository: FakeInventoryRepository(), + productPublishingRepository: FakeProductPublishingRepository(), + ); + } +} diff --git a/kell_creations_apps/apps/kell_web/lib/main.dart b/kell_creations_apps/apps/kell_web/lib/main.dart index f8e12c1..d109acb 100644 --- a/kell_creations_apps/apps/kell_web/lib/main.dart +++ b/kell_creations_apps/apps/kell_web/lib/main.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; + import 'app.dart'; +import 'composition/app_scope.dart'; +import 'composition/app_services.dart'; void main() { - runApp(const KellWebApp()); + final services = AppServices.fake(); + runApp(AppScope(services: services, child: const KellWebApp())); } diff --git a/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart b/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart index dcc3323..2648437 100644 --- a/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart +++ b/kell_creations_apps/apps/kell_web/lib/routing/app_routes.dart @@ -2,6 +2,7 @@ import 'package:feature_inventory/feature_inventory.dart'; import 'package:feature_wordpress/feature_wordpress.dart'; import 'package:flutter/material.dart'; +import '../composition/app_scope.dart'; import '../pages/dashboard_page.dart'; import '../pages/finance_placeholder_page.dart'; import '../pages/integrations_placeholder_page.dart'; @@ -23,41 +24,60 @@ abstract final class AppRoutes { case dashboard: return _buildRoute( settings, - const AppShell(selectedRoute: dashboard, title: 'Dashboard', child: DashboardPage()), + (context) => + const AppShell(selectedRoute: dashboard, title: 'Dashboard', child: DashboardPage()), ); case inventory: return _buildRoute( settings, - const AppShell(selectedRoute: inventory, title: 'Inventory', child: InventoryPage()), + (context) => AppShell( + selectedRoute: inventory, + title: 'Inventory', + child: InventoryPage(repository: AppScope.of(context).inventoryRepository), + ), ); case products: return _buildRoute( settings, - const AppShell( + (context) => AppShell( selectedRoute: products, title: 'Products', - child: ProductPublishingPage(), + child: ProductPublishingPage( + repository: AppScope.of(context).productPublishingRepository, + ), ), ); case orders: return _buildRoute( settings, - const AppShell(selectedRoute: orders, title: 'Orders', child: OrdersPlaceholderPage()), + (context) => const AppShell( + selectedRoute: orders, + title: 'Orders', + child: OrdersPlaceholderPage(), + ), ); case finance: return _buildRoute( settings, - const AppShell(selectedRoute: finance, title: 'Finance', child: FinancePlaceholderPage()), + (context) => const AppShell( + selectedRoute: finance, + title: 'Finance', + child: FinancePlaceholderPage(), + ), ); case policy: return _buildRoute( settings, - const AppShell(selectedRoute: policy, title: 'Policy', child: PolicyPlaceholderPage()), + (context) => const AppShell( + selectedRoute: policy, + title: 'Policy', + child: PolicyPlaceholderPage(), + ), ); case integrations: return _buildRoute( settings, - const AppShell( + (context) => const AppShell( selectedRoute: integrations, title: 'Integrations', child: IntegrationsPlaceholderPage(), @@ -66,12 +86,19 @@ abstract final class AppRoutes { default: return _buildRoute( settings, - const AppShell(selectedRoute: inventory, title: 'Inventory', child: InventoryPage()), + (context) => AppShell( + selectedRoute: inventory, + title: 'Inventory', + child: InventoryPage(repository: AppScope.of(context).inventoryRepository), + ), ); } } - static MaterialPageRoute _buildRoute(RouteSettings settings, Widget page) { - return MaterialPageRoute(settings: settings, builder: (_) => page); + static MaterialPageRoute _buildRoute( + RouteSettings settings, + Widget Function(BuildContext context) pageBuilder, + ) { + return MaterialPageRoute(settings: settings, builder: pageBuilder); } } diff --git a/kell_creations_apps/apps/kell_web/test/widget_test.dart b/kell_creations_apps/apps/kell_web/test/widget_test.dart index 94ab6ee..b8bc38d 100644 --- a/kell_creations_apps/apps/kell_web/test/widget_test.dart +++ b/kell_creations_apps/apps/kell_web/test/widget_test.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:kell_web/app.dart'; +import 'package:kell_web/composition/app_scope.dart'; +import 'package:kell_web/composition/app_services.dart'; + +Widget _buildTestApp() { + return AppScope(services: AppServices.fake(), child: const KellWebApp()); +} void main() { testWidgets('app shell loads dashboard route', (WidgetTester tester) async { - await tester.pumpWidget(const KellWebApp()); + await tester.pumpWidget(_buildTestApp()); await tester.pumpAndSettle(); expect(find.text('Kell Creations'), findsOneWidget); @@ -12,7 +18,7 @@ void main() { }); testWidgets('dashboard shows summary cards', (WidgetTester tester) async { - await tester.pumpWidget(const KellWebApp()); + await tester.pumpWidget(_buildTestApp()); await tester.pumpAndSettle(); expect(find.text('Overview'), findsOneWidget); @@ -23,7 +29,7 @@ void main() { }); testWidgets('dashboard shows quick actions section', (WidgetTester tester) async { - await tester.pumpWidget(const KellWebApp()); + await tester.pumpWidget(_buildTestApp()); await tester.pumpAndSettle(); // Scroll down to reveal the quick actions section. @@ -41,7 +47,7 @@ void main() { }); testWidgets('dashboard shows recent activity empty state', (WidgetTester tester) async { - await tester.pumpWidget(const KellWebApp()); + await tester.pumpWidget(_buildTestApp()); await tester.pumpAndSettle(); // Scroll down to reveal the recent activity section. diff --git a/kell_creations_apps/packages/design_system/lib/src/widgets/kc_status_chip.dart b/kell_creations_apps/packages/design_system/lib/src/widgets/kc_status_chip.dart index 69d2736..232bd74 100644 --- a/kell_creations_apps/packages/design_system/lib/src/widgets/kc_status_chip.dart +++ b/kell_creations_apps/packages/design_system/lib/src/widgets/kc_status_chip.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import '../theme/kc_colors.dart'; class KcStatusChip extends StatelessWidget { final String label; diff --git a/kell_creations_apps/packages/design_system/test/design_system_test.dart b/kell_creations_apps/packages/design_system/test/design_system_test.dart index 58145d3..e1e608e 100644 --- a/kell_creations_apps/packages/design_system/test/design_system_test.dart +++ b/kell_creations_apps/packages/design_system/test/design_system_test.dart @@ -1,12 +1,39 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:kell_web/main.dart'; + +import 'package:design_system/design_system.dart'; void main() { - testWidgets('app renders inventory page', (WidgetTester tester) async { - await tester.pumpWidget(const KellWebApp()); - await tester.pumpAndSettle(); + group('KcStatusChip', () { + testWidgets('renders label text', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: KcStatusChip(label: 'Active', background: Colors.green, foreground: Colors.white), + ), + ), + ); - expect(find.text('Kell Creations'), findsOneWidget); - expect(find.text('Inventory'), findsOneWidget); + expect(find.text('Active'), findsOneWidget); + }); + }); + + group('KcCard', () { + testWidgets('renders child widget', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: KcCard(child: Text('Hello'))), + ), + ); + + expect(find.text('Hello'), findsOneWidget); + }); + }); + + group('buildKcTheme', () { + test('returns a ThemeData', () { + final theme = buildKcTheme(); + expect(theme, isA()); + }); }); } diff --git a/kell_creations_apps/packages/feature_inventory/lib/feature_inventory.dart b/kell_creations_apps/packages/feature_inventory/lib/feature_inventory.dart index d6dbd7b..a71cdbe 100644 --- a/kell_creations_apps/packages/feature_inventory/lib/feature_inventory.dart +++ b/kell_creations_apps/packages/feature_inventory/lib/feature_inventory.dart @@ -1,5 +1,7 @@ library; -export 'src/presentation/inventory_page.dart'; +export 'src/data/fake_inventory_repository.dart'; export 'src/domain/inventory_item.dart'; +export 'src/domain/inventory_repository.dart'; export 'src/domain/inventory_status.dart'; +export 'src/presentation/inventory_page.dart'; diff --git a/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart index f4a59d8..8f80b11 100644 --- a/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart +++ b/kell_creations_apps/packages/feature_inventory/lib/src/presentation/inventory_page.dart @@ -3,11 +3,13 @@ import 'package:flutter/material.dart'; import '../application/get_inventory_items.dart'; import '../application/inventory_controller.dart'; -import '../data/fake_inventory_repository.dart'; +import '../domain/inventory_repository.dart'; import 'widgets/inventory_item_card.dart'; class InventoryPage extends StatefulWidget { - const InventoryPage({super.key}); + final InventoryRepository repository; + + const InventoryPage({super.key, required this.repository}); @override State createState() => _InventoryPageState(); @@ -19,7 +21,7 @@ class _InventoryPageState extends State { @override void initState() { super.initState(); - controller = InventoryController(GetInventoryItems(FakeInventoryRepository())); + controller = InventoryController(GetInventoryItems(widget.repository)); controller.load(); } diff --git a/kell_creations_apps/packages/feature_inventory/pubspec.yaml b/kell_creations_apps/packages/feature_inventory/pubspec.yaml index a53c66c..4a9df9b 100644 --- a/kell_creations_apps/packages/feature_inventory/pubspec.yaml +++ b/kell_creations_apps/packages/feature_inventory/pubspec.yaml @@ -1,6 +1,7 @@ name: feature_inventory description: "A new Flutter package project." version: 0.0.1 +publish_to: "none" homepage: environment: diff --git a/kell_creations_apps/packages/feature_inventory/test/feature_inventory_test.dart b/kell_creations_apps/packages/feature_inventory/test/feature_inventory_test.dart index 81f9c03..6e8f167 100644 --- a/kell_creations_apps/packages/feature_inventory/test/feature_inventory_test.dart +++ b/kell_creations_apps/packages/feature_inventory/test/feature_inventory_test.dart @@ -3,10 +3,37 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:feature_inventory/feature_inventory.dart'; void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); + group('InventoryStatus', () { + test('has four values', () { + expect(InventoryStatus.values.length, 4); + }); + + test('contains expected statuses', () { + expect(InventoryStatus.values, contains(InventoryStatus.inStock)); + expect(InventoryStatus.values, contains(InventoryStatus.lowStock)); + expect(InventoryStatus.values, contains(InventoryStatus.outOfStock)); + expect(InventoryStatus.values, contains(InventoryStatus.draft)); + }); + }); + + group('FakeInventoryRepository', () { + late FakeInventoryRepository repository; + + setUp(() { + repository = FakeInventoryRepository(); + }); + + test('returns six sample items', () async { + final items = await repository.getInventoryItems(); + expect(items.length, 6); + }); + + test('returns items with expected names', () async { + final items = await repository.getInventoryItems(); + final names = items.map((i) => i.name).toList(); + expect(names, contains('Floral Bowl Cozy')); + expect(names, contains('Citrus Coaster Set')); + expect(names, contains('Ocean Nightlight')); + }); }); } diff --git a/kell_creations_apps/packages/feature_wordpress/lib/feature_wordpress.dart b/kell_creations_apps/packages/feature_wordpress/lib/feature_wordpress.dart index 18717c6..95b62b8 100644 --- a/kell_creations_apps/packages/feature_wordpress/lib/feature_wordpress.dart +++ b/kell_creations_apps/packages/feature_wordpress/lib/feature_wordpress.dart @@ -1,5 +1,7 @@ library; +export 'src/data/fake_product_publishing_repository.dart'; export 'src/domain/product_draft.dart'; +export 'src/domain/product_publishing_repository.dart'; export 'src/domain/publish_status.dart'; export 'src/presentation/product_publishing_page.dart'; diff --git a/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/product_publishing_page.dart b/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/product_publishing_page.dart index 793485c..66721c4 100644 --- a/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/product_publishing_page.dart +++ b/kell_creations_apps/packages/feature_wordpress/lib/src/presentation/product_publishing_page.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import '../application/get_product_drafts.dart'; import '../application/product_publishing_controller.dart'; import '../application/publish_product.dart'; -import '../data/fake_product_publishing_repository.dart'; +import '../domain/product_publishing_repository.dart'; import 'widgets/product_draft_card.dart'; import 'widgets/product_preview_panel.dart'; @@ -13,7 +13,9 @@ import 'widgets/product_preview_panel.dart'; /// Displays a list of product drafts on the left and a preview panel on the /// right. Users can select a draft to preview and publish it to the store. class ProductPublishingPage extends StatefulWidget { - const ProductPublishingPage({super.key}); + final ProductPublishingRepository repository; + + const ProductPublishingPage({super.key, required this.repository}); @override State createState() => _ProductPublishingPageState(); @@ -25,7 +27,7 @@ class _ProductPublishingPageState extends State { @override void initState() { super.initState(); - final repo = FakeProductPublishingRepository(); + final repo = widget.repository; controller = ProductPublishingController(GetProductDrafts(repo), PublishProduct(repo)); controller.load(); } diff --git a/kell_creations_apps/packages/feature_wordpress/test/fake_product_publishing_repository_test.dart b/kell_creations_apps/packages/feature_wordpress/test/fake_product_publishing_repository_test.dart index e505406..70a8ff3 100644 --- a/kell_creations_apps/packages/feature_wordpress/test/fake_product_publishing_repository_test.dart +++ b/kell_creations_apps/packages/feature_wordpress/test/fake_product_publishing_repository_test.dart @@ -1,7 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:feature_wordpress/feature_wordpress.dart'; -import 'package:feature_wordpress/src/data/fake_product_publishing_repository.dart'; void main() { late FakeProductPublishingRepository repository; diff --git a/kell_creations_apps/packages/feature_wordpress/test/product_publishing_controller_test.dart b/kell_creations_apps/packages/feature_wordpress/test/product_publishing_controller_test.dart index 346dbf4..8fa2620 100644 --- a/kell_creations_apps/packages/feature_wordpress/test/product_publishing_controller_test.dart +++ b/kell_creations_apps/packages/feature_wordpress/test/product_publishing_controller_test.dart @@ -4,7 +4,6 @@ import 'package:feature_wordpress/feature_wordpress.dart'; import 'package:feature_wordpress/src/application/get_product_drafts.dart'; import 'package:feature_wordpress/src/application/product_publishing_controller.dart'; import 'package:feature_wordpress/src/application/publish_product.dart'; -import 'package:feature_wordpress/src/data/fake_product_publishing_repository.dart'; void main() { late FakeProductPublishingRepository repository;