feat: add two-tab timetable UI and glass dock
This commit is contained in:
@@ -35,6 +35,32 @@ void main() {
|
||||
);
|
||||
});
|
||||
tearDown(() => controller.dispose());
|
||||
test('a new successful demo action clears a stale save error', () async {
|
||||
await controller.initialize();
|
||||
store.failSave = true;
|
||||
await controller.updateSettings(
|
||||
controller.snapshot.settings.copyWith(reminderMinutes: 30),
|
||||
);
|
||||
expect(controller.lastError, isNotNull);
|
||||
await controller.showDemo();
|
||||
expect(controller.lastError, isNull);
|
||||
});
|
||||
test(
|
||||
'refresh after corrupt storage never reconciles an empty timetable',
|
||||
() async {
|
||||
final damaged = AppController(
|
||||
DamagedStore(),
|
||||
notifications,
|
||||
calendar,
|
||||
() => DateTime.utc(2026, 9, 8),
|
||||
);
|
||||
await damaged.initialize();
|
||||
await damaged.refreshServices();
|
||||
expect(notifications.reconciliations, 0);
|
||||
expect(damaged.storageReady, isFalse);
|
||||
damaged.dispose();
|
||||
},
|
||||
);
|
||||
test('failed saving does not change currently visible settings', () async {
|
||||
await controller.initialize();
|
||||
store.failSave = true;
|
||||
@@ -90,3 +116,8 @@ void main() {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class DamagedStore extends MemoryStore {
|
||||
@override
|
||||
Future<AppSnapshot?> load() async => throw const FormatException('damaged');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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 '../support/fakes.dart';
|
||||
|
||||
AppController uiController() => AppController(
|
||||
MemoryStore(),
|
||||
FakeNotifications(),
|
||||
FakeCalendar(),
|
||||
() => DateTime.utc(2026, 10, 12),
|
||||
);
|
||||
void main() {
|
||||
setUpAll(tzdata.initializeTimeZones);
|
||||
testWidgets('two icon tabs navigate to profile and timetable', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = uiController();
|
||||
await controller.initialize();
|
||||
await tester.pumpWidget(HepanApp(controller: controller));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.bySemanticsLabel('我的'), findsOneWidget);
|
||||
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);
|
||||
controller.dispose();
|
||||
});
|
||||
testWidgets('demo reaches Sunday and the last lesson on a small display', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(360, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final controller = uiController();
|
||||
await controller.initialize();
|
||||
await controller.showDemo();
|
||||
await tester.pumpWidget(HepanApp(controller: controller));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('示例课表'), findsOneWidget);
|
||||
expect(find.text('11'), findsOneWidget);
|
||||
await tester.ensureVisible(find.text('周一'));
|
||||
await tester.drag(find.text('周一'), const Offset(-650, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.text('周日'), findsOneWidget);
|
||||
expect(tester.getCenter(find.text('周日')).dx, lessThan(360));
|
||||
controller.dispose();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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 'navigation_test.dart' show uiController;
|
||||
|
||||
void main() {
|
||||
setUpAll(tzdata.initializeTimeZones);
|
||||
testWidgets(
|
||||
'invalid semester weeks are rejected without changing saved data',
|
||||
(tester) async {
|
||||
final controller = uiController();
|
||||
await controller.initialize();
|
||||
await tester.pumpWidget(HepanApp(controller: controller));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.bySemanticsLabel('我的'), findsOneWidget);
|
||||
await tester.tap(find.bySemanticsLabel('我的'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('课表设置'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('课表设置'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(
|
||||
find.byKey(const Key('semester-weeks')),
|
||||
300,
|
||||
scrollable: find.byType(Scrollable).last,
|
||||
);
|
||||
await tester.enterText(find.byKey(const Key('semester-weeks')), '31');
|
||||
await tester.ensureVisible(find.text('保存学期设置'));
|
||||
await tester.tap(find.text('保存学期设置'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('周数应为 1–30'), findsOneWidget);
|
||||
expect(controller.snapshot.settings.semester, isNull);
|
||||
controller.dispose();
|
||||
},
|
||||
);
|
||||
testWidgets('two hundred percent font demo has no overflow', (tester) async {
|
||||
tester.view.physicalSize = const Size(360, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
tester.platformDispatcher.textScaleFactorTestValue = 2;
|
||||
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
|
||||
final controller = uiController();
|
||||
await controller.initialize();
|
||||
await controller.showDemo();
|
||||
await tester.pumpWidget(HepanApp(controller: controller));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('示例课表'),
|
||||
250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
expect(find.text('示例课表'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
controller.dispose();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:timezone/data/latest.dart' as tzdata;
|
||||
import 'package:hepan_everyday/features/timetable/week_grid.dart';
|
||||
import 'package:hepan_everyday/domain/schedule_engine.dart';
|
||||
import 'package:hepan_everyday/domain/app_settings.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(tzdata.initializeTimeZones);
|
||||
testWidgets('transitive lesson conflicts never cover another course card', (
|
||||
tester,
|
||||
) async {
|
||||
final term = Semester(startDate: DateTime(2026, 9, 7), totalWeeks: 1);
|
||||
Course c(String name, int first, int last) => Course(
|
||||
id: name,
|
||||
name: name,
|
||||
teacher: 'T',
|
||||
room: 'R',
|
||||
weekday: 1,
|
||||
startPeriod: first,
|
||||
endPeriod: last,
|
||||
weeks: {1},
|
||||
source: CourseSource.file,
|
||||
);
|
||||
final events = ScheduleEngine.occurrences([
|
||||
c('A', 1, 2),
|
||||
c('B', 2, 3),
|
||||
c('C', 3, 4),
|
||||
], term);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: WeekGrid(
|
||||
events: events,
|
||||
monday: term.firstMonday,
|
||||
today: term.startDate,
|
||||
settings: const AppSettings(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final a = tester.getRect(find.bySemanticsLabel('A,T,R'));
|
||||
final b = tester.getRect(find.bySemanticsLabel('B,T,R'));
|
||||
final cRect = tester.getRect(find.bySemanticsLabel('C,T,R'));
|
||||
expect(a.right, lessThanOrEqualTo(b.left));
|
||||
expect(b.right, lessThanOrEqualTo(cRect.left));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user