refactor(kell-web): add app-level dependency composition
Validate Docs / validate-docs (push) Successful in 1m9s
Details
Validate Docs / validate-docs (push) Successful in 1m9s
Details
This commit is contained in:
parent
226b21d22d
commit
e06c2d8f94
|
|
@ -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<AppScope>();
|
||||||
|
assert(scope != null, 'No AppScope found in the widget tree');
|
||||||
|
return scope!.services;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(AppScope oldWidget) => services != oldWidget.services;
|
||||||
|
}
|
||||||
|
|
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
|
import 'composition/app_scope.dart';
|
||||||
|
import 'composition/app_services.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const KellWebApp());
|
final services = AppServices.fake();
|
||||||
|
runApp(AppScope(services: services, child: const KellWebApp()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:feature_inventory/feature_inventory.dart';
|
||||||
import 'package:feature_wordpress/feature_wordpress.dart';
|
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../composition/app_scope.dart';
|
||||||
import '../pages/dashboard_page.dart';
|
import '../pages/dashboard_page.dart';
|
||||||
import '../pages/finance_placeholder_page.dart';
|
import '../pages/finance_placeholder_page.dart';
|
||||||
import '../pages/integrations_placeholder_page.dart';
|
import '../pages/integrations_placeholder_page.dart';
|
||||||
|
|
@ -23,41 +24,60 @@ abstract final class AppRoutes {
|
||||||
case dashboard:
|
case dashboard:
|
||||||
return _buildRoute(
|
return _buildRoute(
|
||||||
settings,
|
settings,
|
||||||
|
(context) =>
|
||||||
const AppShell(selectedRoute: dashboard, title: 'Dashboard', child: DashboardPage()),
|
const AppShell(selectedRoute: dashboard, title: 'Dashboard', child: DashboardPage()),
|
||||||
);
|
);
|
||||||
case inventory:
|
case inventory:
|
||||||
return _buildRoute(
|
return _buildRoute(
|
||||||
settings,
|
settings,
|
||||||
const AppShell(selectedRoute: inventory, title: 'Inventory', child: InventoryPage()),
|
(context) => AppShell(
|
||||||
|
selectedRoute: inventory,
|
||||||
|
title: 'Inventory',
|
||||||
|
child: InventoryPage(repository: AppScope.of(context).inventoryRepository),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
case products:
|
case products:
|
||||||
return _buildRoute(
|
return _buildRoute(
|
||||||
settings,
|
settings,
|
||||||
const AppShell(
|
(context) => AppShell(
|
||||||
selectedRoute: products,
|
selectedRoute: products,
|
||||||
title: 'Products',
|
title: 'Products',
|
||||||
child: ProductPublishingPage(),
|
child: ProductPublishingPage(
|
||||||
|
repository: AppScope.of(context).productPublishingRepository,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
case orders:
|
case orders:
|
||||||
return _buildRoute(
|
return _buildRoute(
|
||||||
settings,
|
settings,
|
||||||
const AppShell(selectedRoute: orders, title: 'Orders', child: OrdersPlaceholderPage()),
|
(context) => const AppShell(
|
||||||
|
selectedRoute: orders,
|
||||||
|
title: 'Orders',
|
||||||
|
child: OrdersPlaceholderPage(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
case finance:
|
case finance:
|
||||||
return _buildRoute(
|
return _buildRoute(
|
||||||
settings,
|
settings,
|
||||||
const AppShell(selectedRoute: finance, title: 'Finance', child: FinancePlaceholderPage()),
|
(context) => const AppShell(
|
||||||
|
selectedRoute: finance,
|
||||||
|
title: 'Finance',
|
||||||
|
child: FinancePlaceholderPage(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
case policy:
|
case policy:
|
||||||
return _buildRoute(
|
return _buildRoute(
|
||||||
settings,
|
settings,
|
||||||
const AppShell(selectedRoute: policy, title: 'Policy', child: PolicyPlaceholderPage()),
|
(context) => const AppShell(
|
||||||
|
selectedRoute: policy,
|
||||||
|
title: 'Policy',
|
||||||
|
child: PolicyPlaceholderPage(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
case integrations:
|
case integrations:
|
||||||
return _buildRoute(
|
return _buildRoute(
|
||||||
settings,
|
settings,
|
||||||
const AppShell(
|
(context) => const AppShell(
|
||||||
selectedRoute: integrations,
|
selectedRoute: integrations,
|
||||||
title: 'Integrations',
|
title: 'Integrations',
|
||||||
child: IntegrationsPlaceholderPage(),
|
child: IntegrationsPlaceholderPage(),
|
||||||
|
|
@ -66,12 +86,19 @@ abstract final class AppRoutes {
|
||||||
default:
|
default:
|
||||||
return _buildRoute(
|
return _buildRoute(
|
||||||
settings,
|
settings,
|
||||||
const AppShell(selectedRoute: inventory, title: 'Inventory', child: InventoryPage()),
|
(context) => AppShell(
|
||||||
|
selectedRoute: inventory,
|
||||||
|
title: 'Inventory',
|
||||||
|
child: InventoryPage(repository: AppScope.of(context).inventoryRepository),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static MaterialPageRoute<dynamic> _buildRoute(RouteSettings settings, Widget page) {
|
static MaterialPageRoute<dynamic> _buildRoute(
|
||||||
return MaterialPageRoute<dynamic>(settings: settings, builder: (_) => page);
|
RouteSettings settings,
|
||||||
|
Widget Function(BuildContext context) pageBuilder,
|
||||||
|
) {
|
||||||
|
return MaterialPageRoute<dynamic>(settings: settings, builder: pageBuilder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:kell_web/app.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() {
|
void main() {
|
||||||
testWidgets('app shell loads dashboard route', (WidgetTester tester) async {
|
testWidgets('app shell loads dashboard route', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const KellWebApp());
|
await tester.pumpWidget(_buildTestApp());
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('Kell Creations'), findsOneWidget);
|
expect(find.text('Kell Creations'), findsOneWidget);
|
||||||
|
|
@ -12,7 +18,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('dashboard shows summary cards', (WidgetTester tester) async {
|
testWidgets('dashboard shows summary cards', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const KellWebApp());
|
await tester.pumpWidget(_buildTestApp());
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('Overview'), findsOneWidget);
|
expect(find.text('Overview'), findsOneWidget);
|
||||||
|
|
@ -23,7 +29,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('dashboard shows quick actions section', (WidgetTester tester) async {
|
testWidgets('dashboard shows quick actions section', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const KellWebApp());
|
await tester.pumpWidget(_buildTestApp());
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Scroll down to reveal the quick actions section.
|
// Scroll down to reveal the quick actions section.
|
||||||
|
|
@ -41,7 +47,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('dashboard shows recent activity empty state', (WidgetTester tester) async {
|
testWidgets('dashboard shows recent activity empty state', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const KellWebApp());
|
await tester.pumpWidget(_buildTestApp());
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Scroll down to reveal the recent activity section.
|
// Scroll down to reveal the recent activity section.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../theme/kc_colors.dart';
|
|
||||||
|
|
||||||
class KcStatusChip extends StatelessWidget {
|
class KcStatusChip extends StatelessWidget {
|
||||||
final String label;
|
final String label;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,39 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:kell_web/main.dart';
|
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('app renders inventory page', (WidgetTester tester) async {
|
group('KcStatusChip', () {
|
||||||
await tester.pumpWidget(const KellWebApp());
|
testWidgets('renders label text', (WidgetTester tester) async {
|
||||||
await tester.pumpAndSettle();
|
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('Active'), findsOneWidget);
|
||||||
expect(find.text('Inventory'), 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<ThemeData>());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
library;
|
library;
|
||||||
|
|
||||||
export 'src/presentation/inventory_page.dart';
|
export 'src/data/fake_inventory_repository.dart';
|
||||||
export 'src/domain/inventory_item.dart';
|
export 'src/domain/inventory_item.dart';
|
||||||
|
export 'src/domain/inventory_repository.dart';
|
||||||
export 'src/domain/inventory_status.dart';
|
export 'src/domain/inventory_status.dart';
|
||||||
|
export 'src/presentation/inventory_page.dart';
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../application/get_inventory_items.dart';
|
import '../application/get_inventory_items.dart';
|
||||||
import '../application/inventory_controller.dart';
|
import '../application/inventory_controller.dart';
|
||||||
import '../data/fake_inventory_repository.dart';
|
import '../domain/inventory_repository.dart';
|
||||||
import 'widgets/inventory_item_card.dart';
|
import 'widgets/inventory_item_card.dart';
|
||||||
|
|
||||||
class InventoryPage extends StatefulWidget {
|
class InventoryPage extends StatefulWidget {
|
||||||
const InventoryPage({super.key});
|
final InventoryRepository repository;
|
||||||
|
|
||||||
|
const InventoryPage({super.key, required this.repository});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<InventoryPage> createState() => _InventoryPageState();
|
State<InventoryPage> createState() => _InventoryPageState();
|
||||||
|
|
@ -19,7 +21,7 @@ class _InventoryPageState extends State<InventoryPage> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
controller = InventoryController(GetInventoryItems(FakeInventoryRepository()));
|
controller = InventoryController(GetInventoryItems(widget.repository));
|
||||||
controller.load();
|
controller.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
name: feature_inventory
|
name: feature_inventory
|
||||||
description: "A new Flutter package project."
|
description: "A new Flutter package project."
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
|
publish_to: "none"
|
||||||
homepage:
|
homepage:
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,37 @@ import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:feature_inventory/feature_inventory.dart';
|
import 'package:feature_inventory/feature_inventory.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('adds one to input values', () {
|
group('InventoryStatus', () {
|
||||||
final calculator = Calculator();
|
test('has four values', () {
|
||||||
expect(calculator.addOne(2), 3);
|
expect(InventoryStatus.values.length, 4);
|
||||||
expect(calculator.addOne(-7), -6);
|
});
|
||||||
expect(calculator.addOne(0), 1);
|
|
||||||
|
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'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
export 'src/data/fake_product_publishing_repository.dart';
|
||||||
export 'src/domain/product_draft.dart';
|
export 'src/domain/product_draft.dart';
|
||||||
|
export 'src/domain/product_publishing_repository.dart';
|
||||||
export 'src/domain/publish_status.dart';
|
export 'src/domain/publish_status.dart';
|
||||||
export 'src/presentation/product_publishing_page.dart';
|
export 'src/presentation/product_publishing_page.dart';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||||
import '../application/get_product_drafts.dart';
|
import '../application/get_product_drafts.dart';
|
||||||
import '../application/product_publishing_controller.dart';
|
import '../application/product_publishing_controller.dart';
|
||||||
import '../application/publish_product.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_draft_card.dart';
|
||||||
import 'widgets/product_preview_panel.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
|
/// 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.
|
/// right. Users can select a draft to preview and publish it to the store.
|
||||||
class ProductPublishingPage extends StatefulWidget {
|
class ProductPublishingPage extends StatefulWidget {
|
||||||
const ProductPublishingPage({super.key});
|
final ProductPublishingRepository repository;
|
||||||
|
|
||||||
|
const ProductPublishingPage({super.key, required this.repository});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ProductPublishingPage> createState() => _ProductPublishingPageState();
|
State<ProductPublishingPage> createState() => _ProductPublishingPageState();
|
||||||
|
|
@ -25,7 +27,7 @@ class _ProductPublishingPageState extends State<ProductPublishingPage> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final repo = FakeProductPublishingRepository();
|
final repo = widget.repository;
|
||||||
controller = ProductPublishingController(GetProductDrafts(repo), PublishProduct(repo));
|
controller = ProductPublishingController(GetProductDrafts(repo), PublishProduct(repo));
|
||||||
controller.load();
|
controller.load();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'package:feature_wordpress/feature_wordpress.dart';
|
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||||
import 'package:feature_wordpress/src/data/fake_product_publishing_repository.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late FakeProductPublishingRepository repository;
|
late FakeProductPublishingRepository repository;
|
||||||
|
|
|
||||||
|
|
@ -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/get_product_drafts.dart';
|
||||||
import 'package:feature_wordpress/src/application/product_publishing_controller.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/application/publish_product.dart';
|
||||||
import 'package:feature_wordpress/src/data/fake_product_publishing_repository.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late FakeProductPublishingRepository repository;
|
late FakeProductPublishingRepository repository;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue