Make compact Dock persistent and support week swipes
This commit is contained in:
@@ -54,7 +54,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
final glass = find.byType(GlassRefraction);
|
||||
expect(glass, findsNWidgets(2));
|
||||
expect(tester.getSize(glass.first), const Size(264, 68));
|
||||
expect(tester.getSize(glass.first), const Size(264, 58));
|
||||
expect(
|
||||
tester.widget<GlassRefraction>(glass.first).strength,
|
||||
greaterThan(0),
|
||||
@@ -101,42 +101,41 @@ void main() {
|
||||
expect(tester.getSize(lens).width, closeTo(resting.width, 0.5));
|
||||
expect(tester.getSize(lens).height, closeTo(resting.height, 0.5));
|
||||
});
|
||||
testWidgets(
|
||||
'Dock stays visible while sliding down instead of abruptly fading',
|
||||
(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).top, greaterThan(before.top));
|
||||
expect(tester.getRect(dock).top, lessThan(844));
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(dock.hitTestable(), findsNothing);
|
||||
c.dispose();
|
||||
},
|
||||
);
|
||||
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}) {
|
||||
|
||||
@@ -9,46 +9,46 @@ import 'navigation_test.dart' show uiController;
|
||||
|
||||
void main() {
|
||||
setUpAll(tzdata.initializeTimeZones);
|
||||
testWidgets(
|
||||
'glass cursor drags in both directions, taps work, body swipes do not navigate',
|
||||
(tester) async {
|
||||
final c = uiController();
|
||||
await c.initialize();
|
||||
await c.showDemo();
|
||||
await tester.pumpWidget(HepanApp(controller: c));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(TimetablePage),
|
||||
matching: find.text('课表'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(tester.getRect(find.text('周日')).right, lessThanOrEqualTo(800));
|
||||
await tester.drag(find.text('周三'), const Offset(-250, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('登录学校账户'), findsNothing);
|
||||
await tester.drag(
|
||||
find.byKey(const Key('dock-cursor')),
|
||||
const Offset(185, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('登录学校账户'), findsOneWidget);
|
||||
await tester.drag(find.text('连接学校课表'), const Offset(250, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('登录学校账户'), findsOneWidget);
|
||||
await tester.drag(
|
||||
find.byKey(const Key('dock-cursor')),
|
||||
const Offset(-185, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('示例课表'), findsOneWidget);
|
||||
await tester.tap(find.text('我的'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('登录学校账户'), findsOneWidget);
|
||||
c.dispose();
|
||||
},
|
||||
);
|
||||
testWidgets('dock drags switch tabs while body swipes only change week', (
|
||||
tester,
|
||||
) async {
|
||||
final c = uiController();
|
||||
await c.initialize();
|
||||
await c.showDemo();
|
||||
await tester.pumpWidget(HepanApp(controller: c));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(TimetablePage),
|
||||
matching: find.text('课表'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(tester.getRect(find.text('周日')).right, lessThanOrEqualTo(800));
|
||||
await tester.drag(find.text('周三'), const Offset(-250, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('第 2 周'), findsOneWidget);
|
||||
expect(find.text('登录学校账户'), findsNothing);
|
||||
await tester.drag(
|
||||
find.byKey(const Key('dock-cursor')),
|
||||
const Offset(185, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('登录学校账户'), findsOneWidget);
|
||||
await tester.drag(find.text('连接学校课表'), const Offset(250, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('登录学校账户'), findsOneWidget);
|
||||
await tester.drag(
|
||||
find.byKey(const Key('dock-cursor')),
|
||||
const Offset(-185, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('示例课表'), findsOneWidget);
|
||||
await tester.tap(find.text('我的'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('登录学校账户'), findsOneWidget);
|
||||
c.dispose();
|
||||
});
|
||||
testWidgets('cancelled cursor drag keeps selected tab', (tester) async {
|
||||
int selected = 0;
|
||||
await tester.pumpWidget(
|
||||
|
||||
@@ -19,10 +19,10 @@ void main() {
|
||||
preview: true,
|
||||
);
|
||||
// Sample beside the visible label, away from icon glow and glass rims.
|
||||
final selected = _channel(resting, 22, 52, 0);
|
||||
final base = _channel(resting, 160, 52, 0);
|
||||
final selected = _channel(resting, 16, 43, 0);
|
||||
final base = _channel(resting, 160, 43, 0);
|
||||
expect(base - selected, greaterThan(20));
|
||||
expect((_channel(resting, 22, 52, 2) - selected).abs(), lessThan(12));
|
||||
expect((_channel(resting, 16, 43, 2) - selected).abs(), lessThan(12));
|
||||
final held = await _render(
|
||||
tester,
|
||||
GlassDock(selectedIndex: 0, onSelected: (_) {}),
|
||||
@@ -30,7 +30,7 @@ void main() {
|
||||
held: true,
|
||||
preview: true,
|
||||
);
|
||||
expect(_channel(held, 22, 52, 0) - selected, greaterThan(18));
|
||||
expect(_channel(held, 16, 43, 0) - selected, greaterThan(18));
|
||||
});
|
||||
testWidgets(
|
||||
'glass softens fine background detail without whitening its colour',
|
||||
@@ -61,12 +61,14 @@ void main() {
|
||||
);
|
||||
// Below the symbol and away from the rim, the selected lens should retain
|
||||
// this saturated cyan backdrop while applying the new resting grey tint.
|
||||
final red = _channel(pixels, 22, 52, 0);
|
||||
final green = _channel(pixels, 22, 52, 1);
|
||||
final blue = _channel(pixels, 22, 52, 2);
|
||||
final red = _channel(pixels, 16, 40, 0);
|
||||
final green = _channel(pixels, 16, 40, 1);
|
||||
final blue = _channel(pixels, 16, 40, 2);
|
||||
expect(red, lessThan(85));
|
||||
expect(green - red, greaterThan(95));
|
||||
expect(blue - red, greaterThan(140));
|
||||
// The strongest resting tint (34%) and base veil (5%) retain about 63%
|
||||
// of the source's 210-level blue/red contrast, including the shorter rim.
|
||||
expect(blue - red, greaterThan(130));
|
||||
},
|
||||
);
|
||||
|
||||
@@ -81,7 +83,7 @@ void main() {
|
||||
);
|
||||
for (final start in [32, 160]) {
|
||||
final reds = [
|
||||
for (var x = start; x < start + 20; x++) _channel(pixels, x, 60, 0),
|
||||
for (var x = start; x < start + 20; x++) _channel(pixels, x, 50, 0),
|
||||
];
|
||||
final contrast =
|
||||
reds.reduce((a, b) => a > b ? a : b) -
|
||||
@@ -104,7 +106,7 @@ void main() {
|
||||
);
|
||||
for (final start in [24, 160]) {
|
||||
final reds = [
|
||||
for (var x = start; x < start + 40; x++) _channel(pixels, x, 60, 0),
|
||||
for (var x = start; x < start + 40; x++) _channel(pixels, x, 50, 0),
|
||||
];
|
||||
final contrast =
|
||||
reds.reduce((a, b) => a > b ? a : b) -
|
||||
@@ -173,6 +175,7 @@ Future<ByteData> _render(
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final key = GlobalKey(), glassKey = GlobalKey();
|
||||
final height = glass is GlassDock ? 58 : 68;
|
||||
await tester.pumpWidget(
|
||||
RepaintBoundary(
|
||||
key: key,
|
||||
@@ -183,7 +186,7 @@ Future<ByteData> _render(
|
||||
child: SizedBox(
|
||||
key: glassKey,
|
||||
width: 264,
|
||||
height: 68,
|
||||
height: height.toDouble(),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
@@ -235,11 +238,11 @@ Future<ByteData> _render(
|
||||
ui.Canvas(recorder).drawImageRect(
|
||||
image,
|
||||
region,
|
||||
const Rect.fromLTWH(0, 0, 792, 204),
|
||||
Rect.fromLTWH(0, 0, 792, height * 3.0),
|
||||
Paint()..filterQuality = FilterQuality.high,
|
||||
);
|
||||
final picture = recorder.endRecording();
|
||||
final previewImage = await picture.toImage(792, 204);
|
||||
final previewImage = await picture.toImage(792, height * 3);
|
||||
final png = (await previewImage.toByteData(
|
||||
format: ui.ImageByteFormat.png,
|
||||
))!;
|
||||
@@ -254,8 +257,8 @@ Future<ByteData> _render(
|
||||
previewImage.dispose();
|
||||
picture.dispose();
|
||||
}
|
||||
final cropped = ByteData(264 * 68 * 4);
|
||||
for (var y = 0; y < 68; y++) {
|
||||
final cropped = ByteData(264 * height * 4);
|
||||
for (var y = 0; y < height; y++) {
|
||||
for (var x = 0; x < 264; x++) {
|
||||
final source =
|
||||
((region.top.round() + y) * image.width + region.left.round() + x) *
|
||||
|
||||
@@ -8,9 +8,7 @@ import 'navigation_test.dart' show uiController;
|
||||
|
||||
void main() {
|
||||
setUpAll(tzdata.initializeTimeZones);
|
||||
testWidgets('three icon tabs and vertical scroll hide and restore Dock', (
|
||||
tester,
|
||||
) async {
|
||||
testWidgets('three tabs remain reachable while pages scroll', (tester) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
@@ -30,7 +28,7 @@ void main() {
|
||||
expect(find.bySemanticsLabel('考试'), findsOneWidget);
|
||||
await tester.drag(find.text('周三'), const Offset(0, -200));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.bySemanticsLabel('考试').hitTestable(), findsNothing);
|
||||
expect(find.bySemanticsLabel('考试').hitTestable(), findsOneWidget);
|
||||
await tester.drag(find.byType(ListView).first, const Offset(0, 100));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.bySemanticsLabel('考试').hitTestable(), findsOneWidget);
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
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/app_controller.dart';
|
||||
import 'package:hepan_everyday/domain/app_snapshot.dart';
|
||||
import 'package:hepan_everyday/domain/app_settings.dart';
|
||||
import 'package:hepan_everyday/domain/schedule_engine.dart';
|
||||
import 'package:hepan_everyday/features/timetable/timetable_page.dart';
|
||||
|
||||
import '../support/fakes.dart';
|
||||
|
||||
AppController _controller({bool hasSemester = true}) {
|
||||
Course lesson(int week) => Course(
|
||||
id: 'week-$week',
|
||||
name: '第$week周课程',
|
||||
teacher: '测试教师',
|
||||
room: 'A101',
|
||||
weekday: 3,
|
||||
startPeriod: 1,
|
||||
endPeriod: 2,
|
||||
weeks: {week},
|
||||
source: CourseSource.file,
|
||||
);
|
||||
return AppController(
|
||||
MemoryStore(
|
||||
AppSnapshot(
|
||||
courses: [lesson(1), lesson(2), lesson(3)],
|
||||
settings: AppSettings(
|
||||
semester: hasSemester
|
||||
? Semester(startDate: DateTime(2026, 10, 12), totalWeeks: 3)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
FakeNotifications(),
|
||||
FakeCalendar(),
|
||||
() => DateTime.utc(2026, 10, 14),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _open(WidgetTester tester, AppController controller) async {
|
||||
await controller.initialize();
|
||||
addTearDown(controller.dispose);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(body: TimetablePage(controller: controller)),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
setUpAll(tzdata.initializeTimeZones);
|
||||
testWidgets(
|
||||
'horizontal course swipe changes week and course taps still open',
|
||||
(tester) async {
|
||||
await _open(tester, _controller());
|
||||
expect(find.text('第 1 周'), findsOneWidget);
|
||||
expect(find.text('第1周课程'), findsOneWidget);
|
||||
await tester.drag(find.text('第1周课程'), const Offset(-180, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('第 2 周'), findsOneWidget);
|
||||
expect(find.text('第1周课程'), findsNothing);
|
||||
expect(find.text('第2周课程'), findsOneWidget);
|
||||
await tester.tap(find.text('第2周课程'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('知道了'), findsOneWidget);
|
||||
await tester.tap(find.text('知道了'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.drag(find.text('周三'), const Offset(180, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('第 1 周'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
testWidgets('one swipe advances one week and stops at semester boundaries', (
|
||||
tester,
|
||||
) async {
|
||||
await _open(tester, _controller());
|
||||
await tester.drag(find.text('周三'), const Offset(300, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('第 1 周'), findsOneWidget);
|
||||
for (final week in [2, 3, 3]) {
|
||||
await tester.drag(find.text('周三'), const Offset(-300, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('第 $week 周'), findsOneWidget);
|
||||
}
|
||||
await tester.tap(find.text('本周'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('第 1 周'), findsOneWidget);
|
||||
});
|
||||
testWidgets('swiping an imported timetable without a semester keeps prompt', (
|
||||
tester,
|
||||
) async {
|
||||
await _open(tester, _controller(hasSemester: false));
|
||||
await tester.drag(find.byType(TimetablePage), const Offset(-180, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('课表已导入,请设置学期开始日'), findsOneWidget);
|
||||
expect(find.text('第 2 周'), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user