import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:timezone/data/latest.dart' as tzdata; import 'package:hepan_everyday/widgets/glass_dock.dart'; import 'package:hepan_everyday/widgets/glass_refraction.dart'; import 'package:hepan_everyday/app/hepan_app.dart'; import 'navigation_test.dart' show uiController; void main() { setUpAll(tzdata.initializeTimeZones); testWidgets( 'moving lens refracts symbols above the icon row and releases cleanly', (tester) async { await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); final optical = find.byKey(const ValueKey('dock-symbol-refraction')); expect(optical, findsNothing); final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('dock-cursor'))), ); await gesture.moveBy(const Offset(24, 0)); await tester.pump(); await gesture.moveBy(const Offset(54, 0)); await tester.pump(const Duration(milliseconds: 120)); expect(optical, findsOneWidget); final filter = tester.widget( find.descendant(of: optical, matching: find.byType(GlassRefraction)), ); expect(filter.strength, greaterThan(0.35)); expect(filter.blurSigma, 0); // Keep the transmitted symbols legible. final stack = tester.widget( find.ancestor(of: optical, matching: find.byType(Stack)).first, ); expect(stack.children.last.key, const ValueKey('dock-symbol-refraction')); await gesture.up(); await tester.pumpAndSettle(); expect(optical, findsNothing); expect(tester.takeException(), isNull); }, ); testWidgets('edge overflow respects the actual dock parent and safe insets', ( tester, ) async { tester.view.physicalSize = const Size(320, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); var selected = 0; await tester.pumpWidget( MaterialApp( home: MediaQuery( data: const MediaQueryData(size: Size(320, 800)), child: StatefulBuilder( builder: (context, setState) => Scaffold( body: Padding( padding: const EdgeInsets.only(left: 30), child: Center( child: GlassDock( selectedIndex: selected, onSelected: (i) => setState(() => selected = i), ), ), ), ), ), ), ), ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.person_rounded)); await tester.pump(); for (var i = 0; i < 70; i++) { await tester.pump(const Duration(milliseconds: 16)); expect( tester.getRect(find.byType(GlassRefraction).last).right, lessThanOrEqualTo(312.5), ); } await tester.pumpAndSettle(); }); testWidgets( 'tap pulls the dock along and enlarged edge lens can overflow its body', (tester) async { tester.view.physicalSize = const Size(320, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); final dock = find.byType(GlassDock), lens = find.byType(GlassRefraction).last; final icon = find.byIcon(Icons.calendar_month_rounded); final x = tester.getCenter(icon).dx; var pulled = x, overflow = 0.0; await tester.tap(find.byIcon(Icons.person_rounded)); await tester.pump(); for (var i = 0; i < 70; i++) { await tester.pump(const Duration(milliseconds: 16)); pulled = math.max(pulled, tester.getCenter(icon).dx); final r = tester.getRect(lens); overflow = math.max(overflow, r.right - tester.getRect(dock).right); expect(r.left, greaterThanOrEqualTo(7.5)); expect(r.right, lessThanOrEqualTo(312.5)); } expect(pulled, greaterThan(x + 3)); expect(overflow, greaterThan(5)); await tester.pumpAndSettle(); expect(tester.getCenter(icon).dx, closeTo(x, 0.1)); expect(tester.getRect(lens).right, lessThan(tester.getRect(dock).right)); }, ); testWidgets('drag tugs the leading edge and lens follows with elastic lag', ( tester, ) async { await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.assignment_rounded)); await tester.pumpAndSettle(); final lens = find.byType(GlassRefraction).last; final body = find.byType(GlassRefraction).first; final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('dock-cursor'))), ); await tester.pump(const Duration(milliseconds: 110)); await tester.pump(const Duration(milliseconds: 700)); await gesture.moveBy(const Offset(24, 0)); await tester.pump(); final initialX = tester.getCenter(lens).dx; final initialBody = tester.getRect(body); await gesture.moveBy(const Offset(60, 0)); await tester.pump(); expect(tester.getCenter(lens).dx - initialX, lessThan(45)); await tester.pump(const Duration(milliseconds: 180)); expect(tester.getCenter(lens).dx, greaterThan(initialX + 35)); final draggedBody = tester.getRect(body); final leading = draggedBody.right - initialBody.right; final trailing = draggedBody.left - initialBody.left; expect(leading - trailing, greaterThan(7)); await gesture.cancel(); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); }); testWidgets('inactive icon press leaves the cursor at its resting size', ( tester, ) async { await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); final lens = find.byType(GlassRefraction).last; final resting = tester.getSize(lens); final gesture = await tester.startGesture( tester.getCenter(find.byIcon(Icons.assignment_rounded)), ); await tester.pump(const Duration(milliseconds: 110)); await tester.pump(const Duration(milliseconds: 200)); expect(tester.getSize(lens), resting); await gesture.cancel(); await tester.pumpAndSettle(); }); testWidgets('tap has bounded elastic stretch and settles on the new icon', ( tester, ) async { await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); final lens = find.byType(GlassRefraction).last; final resting = tester.getRect(lens); final target = tester.getCenter(find.byIcon(Icons.assignment_rounded)).dx; await tester.tap(find.byIcon(Icons.assignment_rounded)); await tester.pump(); var widest = resting.width; var narrowest = resting.width; var tallest = resting.height; var furthest = resting.center.dx; for (var frame = 0; frame < 70; frame++) { await tester.pump(const Duration(milliseconds: 16)); final rect = tester.getRect(lens); widest = widest > rect.width ? widest : rect.width; narrowest = narrowest < rect.width ? narrowest : rect.width; tallest = tallest > rect.height ? tallest : rect.height; furthest = furthest > rect.center.dx ? furthest : rect.center.dx; expect(rect.width, lessThan(resting.width * 1.7)); expect(rect.center.dx, lessThan(target + 22)); } expect(widest, greaterThan(resting.width * 1.40)); expect(narrowest, lessThan(resting.width * 0.97)); expect(tallest, greaterThan(resting.height * 1.25)); expect(furthest, greaterThan(target + 0.1)); expect(tester.getCenter(lens).dx, closeTo(target, 0.1)); expect(tester.getSize(lens).width, closeTo(resting.width, 0.1)); }); testWidgets('drag deforms the dock body while labels remain readable', ( tester, ) async { await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); final body = find.byType(GlassRefraction).first; final resting = tester.getRect(body); final labelSize = tester.getSize(find.text('考试')); final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('dock-cursor'))), ); await tester.pump(const Duration(milliseconds: 180)); await gesture.moveBy(const Offset(75, 0)); await tester.pump(); await tester.pump(const Duration(milliseconds: 150)); expect(tester.getSize(body).width, greaterThan(resting.width + 5)); expect(tester.getSize(body).height, greaterThan(resting.height + 2)); expect(tester.getSize(find.text('考试')), labelSize); await gesture.up(); await tester.pumpAndSettle(); expect(tester.getSize(body), resting.size); }); testWidgets( 'lens keeps softly stretching and squeezing during an even drag', (tester) async { await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); final lens = find.byType(GlassRefraction).last; final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('dock-cursor'))), ); await tester.pump(const Duration(milliseconds: 110)); await tester.pump(const Duration(milliseconds: 700)); await gesture.moveBy(const Offset(24, 0)); await tester.pump(); final widths = [], heights = [], centres = []; for (var i = 0; i < 22; i++) { await gesture.moveBy(const Offset(5, 0)); await tester.pump(const Duration(milliseconds: 20)); if (i > 3) { final size = tester.getSize(lens); widths.add(size.width); heights.add(size.height); centres.add(tester.getCenter(lens).dx); } } expect(widths.reduce(math.max) - widths.reduce(math.min), greaterThan(6)); expect( heights.reduce(math.max) - heights.reduce(math.min), greaterThan(2), ); expect(centres.last - centres.first, greaterThan(50)); await gesture.up(); await tester.pumpAndSettle(); expect(tester.getSize(lens).width, closeTo(86, 0.1)); expect(tester.getSize(lens).height, closeTo(52, 0.1)); }, ); testWidgets( 'rapid reverse taps keep position continuous and finish on last tab', (tester) async { await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); final lens = find.byType(GlassRefraction).last; await tester.tap(find.byIcon(Icons.person_rounded)); await tester.pump(); await tester.pump(const Duration(milliseconds: 90)); final before = tester.getCenter(lens); final beforeSize = tester.getSize(lens); await tester.tap(find.byIcon(Icons.calendar_month_rounded)); await tester.pump(); expect((tester.getCenter(lens) - before).distance, lessThan(1)); expect(tester.getSize(lens).width, closeTo(beforeSize.width, 0.1)); await tester.pumpAndSettle(); expect( tester.getCenter(lens).dx, closeTo( tester.getCenter(find.byIcon(Icons.calendar_month_rounded)).dx, 0.1, ), ); }, ); testWidgets('reversing a tap at the edge preserves glass size and position', ( tester, ) async { await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); final lens = find.byType(GlassRefraction).last; final body = find.byType(GlassRefraction).first; await tester.tap(find.byIcon(Icons.person_rounded)); await tester.pump(); await tester.pump(const Duration(milliseconds: 230)); final before = tester.getRect(lens); final bodyBefore = tester.getRect(body); await tester.tap(find.byIcon(Icons.calendar_month_rounded)); await tester.pump(); final after = tester.getRect(lens); expect(after.width, closeTo(before.width, 0.1)); expect(after.height, closeTo(before.height, 0.1)); expect(after.center.dx, closeTo(before.center.dx, 0.1)); expect(tester.getSize(body).width, closeTo(bodyBefore.width, 0.1)); await tester.pumpAndSettle(); expect( tester.getCenter(lens).dx, closeTo( tester.getCenter(find.byIcon(Icons.calendar_month_rounded)).dx, 0.1, ), ); }); testWidgets('dock body follows a moving drag and springs back on cancel', ( tester, ) async { await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); final body = find.byType(GlassRefraction).first; final resting = tester.getRect(body); final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('dock-cursor'))), ); await tester.pump(const Duration(milliseconds: 110)); await tester.pump(const Duration(milliseconds: 350)); await gesture.moveBy(const Offset(24, 0)); await tester.pump(); await tester.pump(const Duration(milliseconds: 350)); final held = tester.getRect(body); await gesture.moveBy(const Offset(60, 0)); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(tester.getSize(body).width, greaterThan(held.width + 0.5)); await gesture.cancel(); await tester.pumpAndSettle(); final settled = tester.getRect(body); expect(settled.left, closeTo(resting.left, 0.05)); expect(settled.top, closeTo(resting.top, 0.05)); expect(settled.width, closeTo(resting.width, 0.05)); expect(settled.height, closeTo(resting.height, 0.05)); }); testWidgets('base and selected cursor both refract the live backdrop', ( tester, ) async { await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); final glass = find.byType(GlassRefraction); expect(glass, findsNWidgets(2)); expect(tester.getSize(glass.first), const Size(264, 58)); expect( tester.widget(glass.first).strength, greaterThan(0), ); }); testWidgets( 'drag moves the whole dock in its direction and returns on release', (tester) async { await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.assignment_rounded)); await tester.pumpAndSettle(); final icon = find.byIcon(Icons.calendar_month_rounded); final glass = find.byType(GlassRefraction).first; final x = tester.getCenter(icon).dx; final glassX = tester.getCenter(glass).dx; final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('dock-cursor'))), ); await gesture.moveBy(const Offset(24, 0)); await tester.pump(); await gesture.moveBy(const Offset(45, 0)); await tester.pump(); await tester.pump(const Duration(milliseconds: 140)); expect(tester.getCenter(icon).dx, greaterThan(x + 4)); expect(tester.getCenter(glass).dx, greaterThan(glassX + 4)); await gesture.moveBy(const Offset(-70, 0)); await tester.pump(); await tester.pump(const Duration(milliseconds: 220)); expect(tester.getCenter(icon).dx, lessThan(x - 4)); await gesture.up(); await tester.pumpAndSettle(); expect(tester.getCenter(icon).dx, closeTo(x, 0.05)); }, ); testWidgets('icon bounces once when the glass arrives and then rests', ( tester, ) async { await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); final icon = find.byIcon(Icons.assignment_rounded); final rest = tester.getRect(icon); await tester.tap(icon); await tester.pump(); var highest = rest.top; var widest = rest.width; for (var i = 0; i < 65; i++) { await tester.pump(const Duration(milliseconds: 16)); final r = tester.getRect(icon); highest = math.min(highest, r.top); widest = math.max(widest, r.width); } expect(highest, lessThan(rest.top - 1)); expect(widest, greaterThan(rest.width * 1.1)); await tester.pumpAndSettle(); expect(tester.getRect(icon).top, closeTo(rest.top, 0.05)); expect(tester.getSize(icon).width, closeTo(rest.width, 0.05)); }); testWidgets('reduced motion snaps the cursor without oscillation', ( tester, ) async { await tester.pumpWidget(_dock(reduceMotion: true)); await tester.pumpAndSettle(); final lens = find.byType(GlassRefraction).last; final resting = tester.getSize(lens); final iconBefore = tester.getRect(find.byIcon(Icons.person_rounded)); await tester.tap(find.byIcon(Icons.person_rounded)); await tester.pump(); final target = tester.getCenter(find.byIcon(Icons.person_rounded)).dx; expect(tester.getCenter(lens).dx, closeTo(target, 0.05)); await tester.pump(const Duration(milliseconds: 200)); expect(tester.getCenter(lens).dx, closeTo(target, 0.05)); expect(tester.getSize(lens), resting); expect(tester.getRect(find.byIcon(Icons.person_rounded)), iconBefore); }); testWidgets('hovering a dragged lens animates its icon before releasing', ( tester, ) async { await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); final icon = find.byIcon(Icons.assignment_rounded); final y = tester.getCenter(icon).dy; final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('dock-cursor'))), ); await gesture.moveBy(const Offset(24, 0)); await tester.pump(); await gesture.moveBy(const Offset(86, 0)); await tester.pump(); for (var frame = 0; frame < 12; frame++) { await tester.pump(const Duration(milliseconds: 16)); } expect(tester.getCenter(icon).dy, lessThan(y - 1)); await tester.pump(const Duration(milliseconds: 600)); expect(tester.getCenter(icon).dy, closeTo(y, 0.1)); await gesture.cancel(); await tester.pumpAndSettle(); }); testWidgets('releasing over an animating icon preserves its bounce', ( tester, ) async { await tester.pumpWidget(_dock()); await tester.pumpAndSettle(); final icon = find.byIcon(Icons.assignment_rounded); final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('dock-cursor'))), ); await gesture.moveBy(const Offset(24, 0)); await tester.pump(); await gesture.moveBy(const Offset(86, 0)); await tester.pump(); for (var frame = 0; frame < 12; frame++) { await tester.pump(const Duration(milliseconds: 16)); } final before = tester.getRect(icon); await gesture.up(); await tester.pump(); final after = tester.getRect(icon); expect(after.top, closeTo(before.top, 0.1)); expect(after.width, closeTo(before.width, 0.1)); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); }); testWidgets( 'changing reduced motion during a tap does not leave frozen deformation', (tester) async { final reduced = ValueNotifier(false); var selected = 0; await tester.pumpWidget( MaterialApp( home: StatefulBuilder( builder: (context, setState) => ValueListenableBuilder( valueListenable: reduced, builder: (context, value, _) => MediaQuery( data: MediaQueryData(disableAnimations: value), child: Center( child: GlassDock( selectedIndex: selected, onSelected: (index) => setState(() => selected = index), ), ), ), ), ), ), ); await tester.pumpAndSettle(); final lens = find.byType(GlassRefraction).last; final size = tester.getSize(lens); await tester.tap(find.byIcon(Icons.assignment_rounded)); await tester.pump(); await tester.pump(const Duration(milliseconds: 120)); reduced.value = true; await tester.pump(); reduced.value = false; await tester.pumpAndSettle(); expect(tester.getSize(lens).width, closeTo(size.width, 0.05)); expect(tester.getSize(lens).height, closeTo(size.height, 0.05)); reduced.dispose(); }, ); testWidgets('held cursor enlarges before dragging and returns on release', ( tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center(child: GlassDock(selectedIndex: 0, onSelected: (_) {})), ), ), ); await tester.pumpAndSettle(); final lens = find.byType(GlassRefraction).last; final resting = tester.getSize(lens); final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('dock-cursor'))), ); await tester.pump(const Duration(milliseconds: 110)); await tester.pump(const Duration(milliseconds: 200)); final held = tester.getSize(lens); expect(held.width, greaterThan(resting.width * 1.25)); expect(held.height, greaterThan(resting.height * 1.25)); await gesture.cancel(); await tester.pumpAndSettle(); expect(tester.getSize(lens).width, closeTo(resting.width, 0.5)); expect(tester.getSize(lens).height, closeTo(resting.height, 0.5)); }); testWidgets('Dock stays stationary and tappable while scrolling timetable', ( tester, ) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final c = uiController(); await c.initialize(); await c.showDemo(); await tester.pumpWidget(HepanApp(controller: c)); await tester.pumpAndSettle(); final dock = find.byType(GlassDock); final before = tester.getRect(dock); final gesture = await tester.startGesture( tester.getCenter(find.text('周三')), ); await gesture.moveBy(const Offset(0, -80)); await tester.pump(); await tester.pump(const Duration(milliseconds: 120)); final fading = find.ancestor( of: dock, matching: find.byType(FadeTransition), ); final opacity = fading.evaluate().isEmpty ? 1.0 : tester.widget(fading.first).opacity.value; expect(opacity, greaterThan(0.95)); expect(tester.getRect(dock), before); expect(tester.getRect(dock).top, lessThan(844)); await gesture.up(); await tester.pumpAndSettle(); expect(dock.hitTestable(), findsOneWidget); c.dispose(); }); } Widget _dock({bool reduceMotion = false}) { var selected = 0; return MaterialApp( home: MediaQuery( data: MediaQueryData(disableAnimations: reduceMotion), child: StatefulBuilder( builder: (context, setState) => Scaffold( body: Center( child: GlassDock( selectedIndex: selected, onSelected: (index) => setState(() => selected = index), ), ), ), ), ), ); }