import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:patbond_flutter/core/theme/app_theme.dart'; import 'package:patbond_flutter/core/widgets/like_button.dart'; /// 宿主:模拟持有方(controller)的状态翻转——点按经 [onTap] 乐观翻转, /// 外部(回滚/对账)经 [setActive] 直接改。 class _Host extends StatefulWidget { const _Host({required this.initialActive, required this.initialCount}); final bool initialActive; final int initialCount; @override State<_Host> createState() => _HostState(); } class _HostState extends State<_Host> { late bool active = widget.initialActive; late int count = widget.initialCount; void setExternal({required bool active, required int count}) { setState(() { this.active = active; this.count = count; }); } @override Widget build(BuildContext context) { return MaterialApp( theme: buildAppTheme(), home: Scaffold( body: Center( child: LikeButton( variant: LikeButtonVariant.like, active: active, count: count, onPressed: () => setState(() { active = !active; count += active ? 1 : -1; }), semanticLabel: '点赞', ), ), ), ); } } void main() { double scaleOf(WidgetTester tester) => tester .widget( find.descendant( of: find.byType(LikeButton), matching: find.byType(ScaleTransition), ), ) .scale .value; testWidgets('点按激活:240ms 弹性缩放动画(中途 >1,播完归位)', (tester) async { await tester.pumpWidget(const _Host(initialActive: false, initialCount: 6)); await tester.tap(find.byIcon(Icons.favorite_border)); await tester.pump(); await tester.pump(const Duration(milliseconds: 90)); expect(scaleOf(tester), greaterThan(1.0)); expect(find.byIcon(Icons.favorite), findsOneWidget); expect(find.text('7'), findsOneWidget); await tester.pumpAndSettle(); expect(scaleOf(tester), 1.0); }); testWidgets('点按取消:仅颜色切换,无缩放动画', (tester) async { await tester.pumpWidget(const _Host(initialActive: true, initialCount: 7)); await tester.tap(find.byIcon(Icons.favorite)); await tester.pump(); await tester.pump(const Duration(milliseconds: 90)); expect(scaleOf(tester), 1.0); expect(find.byIcon(Icons.favorite_border), findsOneWidget); expect(find.text('6'), findsOneWidget); await tester.pumpAndSettle(); }); testWidgets('外部回滚:零动画直接跳变,计数与状态成对恢复', (tester) async { await tester.pumpWidget(const _Host(initialActive: false, initialCount: 6)); // 点按激活并播完动画(在途请求随后失败的场景)。 await tester.tap(find.byIcon(Icons.favorite_border)); await tester.pumpAndSettle(); expect(find.byIcon(Icons.favorite), findsOneWidget); // 外部(非点按)回滚:下一帧即恢复,无过渡动画。 tester .state<_HostState>(find.byType(_Host)) .setExternal(active: false, count: 6); await tester.pump(); expect(find.byIcon(Icons.favorite_border), findsOneWidget); expect(find.text('6'), findsOneWidget); expect(scaleOf(tester), 1.0); }); testWidgets('激活动画未播完时回滚:等播完再成对跳变(§4.3a 抖动抑制)', (tester) async { await tester.pumpWidget(const _Host(initialActive: false, initialCount: 6)); await tester.tap(find.byIcon(Icons.favorite_border)); await tester.pump(); await tester.pump(const Duration(milliseconds: 60)); expect(find.byIcon(Icons.favorite), findsOneWidget); // 动画中途回滚:本帧仍显示激活视觉(等待播完),计数同持。 tester .state<_HostState>(find.byType(_Host)) .setExternal(active: false, count: 6); await tester.pump(); expect(find.byIcon(Icons.favorite), findsOneWidget); expect(find.text('7'), findsOneWidget); // 播完后成对跳回。 await tester.pumpAndSettle(); expect(find.byIcon(Icons.favorite_border), findsOneWidget); expect(find.text('6'), findsOneWidget); }); testWidgets('对账静默:状态不变的计数变化直接替换,无动画', (tester) async { await tester.pumpWidget(const _Host(initialActive: true, initialCount: 7)); tester .state<_HostState>(find.byType(_Host)) .setExternal(active: true, count: 9); await tester.pump(); expect(find.text('9'), findsOneWidget); expect(scaleOf(tester), 1.0); }); testWidgets('onPressed 为 null 时禁用但照常渲染激活态', (tester) async { await tester.pumpWidget( MaterialApp( theme: buildAppTheme(), home: const Scaffold( body: LikeButton( variant: LikeButtonVariant.bookmark, active: true, count: 2, ), ), ), ); expect(find.byIcon(Icons.bookmark), findsOneWidget); expect(find.text('2'), findsOneWidget); }); }