Files
TPSchedule/mobile/test/widgets/dock_motion_test.dart
T

303 lines
11 KiB
Dart

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('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 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;
furthest = furthest > rect.center.dx ? furthest : rect.center.dx;
expect(rect.width, lessThan(resting.width * 1.4));
expect(rect.center.dx, lessThan(target + 8));
}
expect(widest, greaterThan(resting.width * 1.06));
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(
'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<GlassRefraction>(glass.first).strength,
greaterThan(0),
);
});
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);
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);
});
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<bool>(
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<FadeTransition>(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),
),
),
),
),
),
);
}