feat: select school terms and smooth refractive Dock motion

This commit is contained in:
Codex
2026-10-13 19:08:36 +08:00
parent 3e2d8864af
commit 50fcc70e8e
33 changed files with 1245 additions and 94 deletions
+88 -2
View File
@@ -2,12 +2,79 @@ 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 cursor travels monotonically with no size jitter', (
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 previous = resting.center.dx;
for (var frame = 0; frame < 50; frame++) {
await tester.pump(const Duration(milliseconds: 16));
final rect = tester.getRect(lens);
expect(rect.width, closeTo(resting.width, 0.05));
expect(rect.height, closeTo(resting.height, 0.05));
expect(rect.center.dx, greaterThanOrEqualTo(previous - 0.05));
expect(rect.center.dx, lessThanOrEqualTo(target + 0.05));
previous = rect.center.dx;
}
expect(previous, closeTo(target, 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, 68));
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('held cursor enlarges before dragging and returns on release', (
tester,
) async {
@@ -32,8 +99,8 @@ void main() {
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.12));
expect(held.height, greaterThan(resting.height * 1.12));
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));
@@ -76,3 +143,22 @@ void main() {
},
);
}
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),
),
),
),
),
),
);
}
+1 -1
View File
@@ -16,7 +16,7 @@ void main() {
await c.showDemo();
await tester.pumpWidget(HepanApp(controller: c));
await tester.pumpAndSettle();
expect(find.text('RSchedule'), findsOneWidget);
expect(find.text('课表'), findsOneWidget);
expect(tester.getRect(find.text('周日')).right, lessThanOrEqualTo(800));
await tester.drag(find.text('周三'), const Offset(-250, 0));
await tester.pumpAndSettle();
@@ -19,7 +19,7 @@ void main() {
await c.showDemo();
await tester.pumpWidget(HepanApp(controller: c));
await tester.pumpAndSettle();
expect(find.text('RSchedule'), findsOneWidget);
expect(find.text('课表'), findsOneWidget);
expect(find.bySemanticsLabel('考试'), findsOneWidget);
await tester.drag(find.text('周三'), const Offset(0, -200));
await tester.pumpAndSettle();
+8 -1
View File
@@ -7,6 +7,7 @@ import 'package:hepan_everyday/app/hepan_app.dart';
import 'package:hepan_everyday/domain/app_snapshot.dart';
import 'package:hepan_everyday/domain/app_settings.dart';
import 'package:hepan_everyday/domain/semester.dart';
import 'package:hepan_everyday/widgets/glass_dock.dart';
import '../support/fakes.dart';
import '../domain/schedule_engine_test.dart' show course;
@@ -38,7 +39,13 @@ void main() {
buildApplication(controller, notificationSelection: selection),
);
await tester.pumpAndSettle();
expect(find.bySemanticsLabel('课表'), findsOneWidget);
expect(
find.descendant(
of: find.byType(GlassDock),
matching: find.bySemanticsLabel('课表'),
),
findsOneWidget,
);
expect(find.bySemanticsLabel('个人设置'), findsOneWidget);
await tester.tap(find.bySemanticsLabel('个人设置'));
await tester.pumpAndSettle();
+79
View File
@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:timezone/data/latest.dart' as tzdata;
import 'package:hepan_everyday/app/hepan_app.dart';
import 'package:hepan_everyday/app/app_controller.dart';
import 'package:hepan_everyday/domain/app_snapshot.dart';
import 'package:hepan_everyday/domain/semester.dart';
import 'package:hepan_everyday/features/timetable/timetable_page.dart';
import '../support/fakes.dart';
import '../domain/schedule_engine_test.dart' show course;
void main() {
setUpAll(tzdata.initializeTimeZones);
testWidgets('imported courses without start date show an actionable prompt', (
tester,
) async {
final c = AppController(
MemoryStore(AppSnapshot(courses: [course()])),
FakeNotifications(),
FakeCalendar(),
() => DateTime(2026, 10, 13),
);
await c.initialize();
await tester.pumpWidget(HepanApp(controller: c));
await tester.pumpAndSettle();
expect(find.text('课表已导入,请设置学期开始日'), findsOneWidget);
expect(find.text('1 条课程安排已保存'), findsOneWidget);
await tester.tap(find.text('设置学期开始日'));
await tester.pumpAndSettle();
expect(find.text('课表设置'), findsOneWidget);
expect(find.text('学期开始日').hitTestable(), findsOneWidget);
await c.updateSettings(
(s) => s.copyWith(
semester: Semester(startDate: DateTime(2026, 10, 12), totalWeeks: 20),
),
);
Navigator.of(tester.element(find.text('课表设置'))).pop();
await tester.pumpAndSettle();
expect(find.text('课表已导入,请设置学期开始日'), findsNothing);
expect(find.text('周日'), findsOneWidget);
c.dispose();
});
testWidgets(
'timetable refresh calls school sync and page titles are specific',
(tester) async {
final c = AppController(
MemoryStore(),
FakeNotifications(),
FakeCalendar(),
() => DateTime(2026, 10, 13),
);
await c.initialize();
var calls = 0;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: TimetablePage(controller: c, onSync: () => calls++),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('刷新课表'));
expect(calls, 1);
expect(find.text('课表'), findsOneWidget);
await tester.pumpWidget(HepanApp(controller: c));
await tester.pumpAndSettle();
await tester.tap(find.bySemanticsLabel('考试'));
await tester.pumpAndSettle();
expect(find.text('考试'), findsOneWidget);
expect(find.text('考试安排'), findsOneWidget);
await tester.tap(find.bySemanticsLabel('个人设置'));
await tester.pumpAndSettle();
expect(find.text('更新学期'), findsOneWidget);
expect(find.text('RSchedule'), findsWidgets);
c.dispose();
},
);
}