diff --git a/client/test/golden/goldens/inventory_list_a.png b/client/test/golden/goldens/inventory_list_a.png new file mode 100644 index 0000000..f1c0ec1 Binary files /dev/null and b/client/test/golden/goldens/inventory_list_a.png differ diff --git a/client/test/golden/goldens/inventory_list_b.png b/client/test/golden/goldens/inventory_list_b.png new file mode 100644 index 0000000..841a781 Binary files /dev/null and b/client/test/golden/goldens/inventory_list_b.png differ diff --git a/client/test/golden/goldens/inventory_list_c.png b/client/test/golden/goldens/inventory_list_c.png new file mode 100644 index 0000000..71fd5ed Binary files /dev/null and b/client/test/golden/goldens/inventory_list_c.png differ diff --git a/client/test/golden/inventory_list_golden_test.dart b/client/test/golden/inventory_list_golden_test.dart new file mode 100644 index 0000000..29d5f49 --- /dev/null +++ b/client/test/golden/inventory_list_golden_test.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:jiu_client/core/models/page_result.dart'; +import 'package:jiu_client/core/auth/auth_state.dart'; +import 'package:jiu_client/models/inventory.dart'; +import 'package:jiu_client/models/product_option.dart'; +import 'package:jiu_client/providers/inventory_provider.dart'; +import 'package:jiu_client/providers/product_option_provider.dart'; +import 'package:jiu_client/screens/inventory/inventory_list_screen.dart'; + +import '../support/golden_harness.dart'; + +/// design-distill 阶段4:库存列表**整屏** golden × 三主题(回归闸)。 +/// 保真目检:另跑 shoot-prototype 截 screens/inventory.html,再用 montage.mjs +/// 与 goldens/inventory_list_.png 并排比对。 +/// 更新基准:flutter test --update-goldens test/golden/inventory_list_golden_test.dart + +const _items = [ + Inventory( + id: 1, productId: 1, quantity: 128, productCode: 'MT-FT-500', + productName: '茅台 飞天 53°', series: '飞天', spec: '500ml×6', unit: '件', + warehouseName: '主仓', unitPrice: 2680, productionDate: '2024-07-11', + batchNo: 'PZ240711', supplierName: '贵州茅台经销', minStock: 10, + createdAt: '2026-06-14', + ), + Inventory( + id: 2, productId: 2, quantity: 8, productCode: 'JNC-SJ-500', + productName: '剑南春 水晶剑', series: '水晶剑', spec: '500ml×6', unit: '件', + warehouseName: '主仓', unitPrice: 438, productionDate: '2023-11-15', + batchNo: 'PZ231115', supplierName: '绵竹剑南春', minStock: 10, + createdAt: '2026-06-12', + ), + Inventory( + id: 3, productId: 3, quantity: 0, productCode: 'FJ-QH20-500', + productName: '汾酒 青花 20', series: '青花20', spec: '500ml×6', unit: '件', + warehouseName: '主仓', unitPrice: 420, productionDate: '2024-01-12', + batchNo: 'PZ240112', supplierName: '山西杏花村', minStock: 10, + createdAt: '2026-06-03', + ), +]; + +class _FakeInvNotifier extends InventoryListNotifier { + @override + Future> build() async => + const PageResult(data: _items, total: 3, page: 1, pageSize: 20); + @override + void setPage(int page) {} + @override + void setPageSize(int pageSize) {} + @override + void setWarehouseId(int? id) {} + @override + void setKeyword(String keyword) {} + @override + void setSeries(List series) {} + @override + void setSpec(List spec) {} + @override + void reload() {} +} + +class _FakeSeriesNotifier extends ProductSeriesListNotifier { + @override + Future> build() async => const []; +} + +class _FakeSpecNotifier extends ProductSpecListNotifier { + @override + Future> build() async => const []; +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); + + goldenAcrossThemes( + 'inventory list 整屏', + goldenPrefix: 'inventory_list', + child: () => const Scaffold(body: InventoryListScreen()), + overrides: () => [ + inventoryListProvider.overrideWith(() => _FakeInvNotifier()), + productSeriesListProvider.overrideWith(() => _FakeSeriesNotifier()), + productSpecListProvider.overrideWith(() => _FakeSpecNotifier()), + isReadonlyProvider.overrideWithValue(false), + ], + logical: const Size(1280, 900), + ); +} diff --git a/client/test/support/golden_harness.dart b/client/test/support/golden_harness.dart new file mode 100644 index 0000000..5b7c748 --- /dev/null +++ b/client/test/support/golden_harness.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jiu_client/core/theme/themes.dart'; + +/// design-distill 阶段4(Flutter 端)可复用 golden 骨架。 +/// 在指定主题 / viewport 下渲染 [child](含 provider 覆盖),比对/更新 golden。 +/// +/// 验收分两类闸(见 ~/.claude/skills/design-distill/references/flutter-golden.md): +/// - 保真:原型基准 vs 本 golden 用 montage.mjs 人工目检(跨渲染器,不跑严格 diff)。 +/// - 回归:本 golden 多主题入库,CI `flutter test` 同渲染器自比抓串色/漏 token。 +/// +/// 更新基准:flutter test --update-goldens test/golden/_golden_test.dart +/// +/// 坑(详见 reference):viewport 逻辑尺寸 = physical ÷ dpr(已按 logical*dpr 设好); +/// 高度给足防 RenderFlex 溢出;测试无中文字体 → CJK 显示豆腐块属正常。 +Future pumpGolden( + WidgetTester tester, { + required Widget child, + required String theme, + required String goldenName, + List overrides = const [], + Size logical = const Size(1280, 900), + double dpr = 2.0, +}) async { + tester.view.physicalSize = Size(logical.width * dpr, logical.height * dpr); + tester.view.devicePixelRatio = dpr; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget(ProviderScope( + overrides: overrides, + child: MaterialApp( + debugShowCheckedModeBanner: false, + theme: buildTheme(theme), + home: child, + ), + )); + await tester.pumpAndSettle(); + + await expectLater( + find.byType(MaterialApp), + matchesGoldenFile('goldens/$goldenName.png'), + ); +} + +/// 跑「同一 child × A/B/C 三主题」的 golden 组(回归闸常用)。 +void goldenAcrossThemes( + String description, { + required Widget Function() child, + required String goldenPrefix, + List Function()? overrides, + Size logical = const Size(1280, 900), + double dpr = 2.0, +}) { + for (final theme in const ['a', 'b', 'c']) { + testWidgets('$description · theme $theme', (tester) async { + await pumpGolden( + tester, + child: child(), + theme: theme, + goldenName: '${goldenPrefix}_$theme', + overrides: overrides?.call() ?? const [], + logical: logical, + dpr: dpr, + ); + }); + } +}