fix: harden school imports and add optional feedback diagnostics
This commit is contained in:
@@ -109,4 +109,24 @@ void main() {
|
||||
'{"schemaVersion":1,"courses":[${const JsonEncoder().convert(old)}]}';
|
||||
expect(() => JsonScheduleImport.parse(json), throwsFormatException);
|
||||
});
|
||||
test('graduate timetable changes preserve independently saved undergraduate exams', () async {
|
||||
await controller.initialize();
|
||||
await controller.applyExams(exams());
|
||||
await controller.applyImport(
|
||||
ImportDraft(
|
||||
courses: [course()],
|
||||
explicitlyEmpty: false,
|
||||
source: course().source,
|
||||
parserVersion: 'test',
|
||||
schoolTerm: SchoolTerm(72),
|
||||
),
|
||||
confirmedEmpty: false,
|
||||
);
|
||||
expect(controller.lastError, isNull);
|
||||
expect(
|
||||
controller.snapshot.examSchedule?.school,
|
||||
SupportedSchool.uestcUndergraduate,
|
||||
);
|
||||
expect(controller.snapshot.examSchedule?.termId, '503');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,17 @@ import 'package:hepan_everyday/domain/app_snapshot.dart';
|
||||
import 'package:hepan_everyday/domain/course.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'optional exam types reject invalid values even in graduate snapshots',
|
||||
() {
|
||||
final saved = ExamSchedule(
|
||||
semesterLabel: '2026秋',
|
||||
termId: '73',
|
||||
records: [const ExamRecord(code: 'A', name: '课程', examTypeId: 99)],
|
||||
).toJson();
|
||||
expect(() => ExamSchedule.fromJson(saved), throwsFormatException);
|
||||
},
|
||||
);
|
||||
test(
|
||||
'managed undergraduate identifiers cannot lose their school or term',
|
||||
() {
|
||||
|
||||
@@ -33,6 +33,27 @@ class FakeFeedbackHttpTransport implements FeedbackHttpTransport {
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('diagnostics use a separate UTF8 attachment field', () async {
|
||||
final transport = FakeFeedbackHttpTransport();
|
||||
await HttpFeedbackSender(
|
||||
endpoint: 'https://example.com/v1/feedback',
|
||||
transport: transport,
|
||||
).send(
|
||||
FeedbackSubmission.problem(
|
||||
id: 'same-id',
|
||||
message: '问题',
|
||||
diagnosticLog: Uint8List.fromList(
|
||||
utf8.encode('TPSchedule diagnostic log\nlogin_opened'),
|
||||
),
|
||||
),
|
||||
);
|
||||
final body = utf8.decode(transport.body!);
|
||||
expect(
|
||||
body,
|
||||
contains('name="diagnostics"; filename="TPSchedule-diagnostics.txt"'),
|
||||
);
|
||||
expect(body, contains('login_opened'));
|
||||
});
|
||||
test(
|
||||
'matching uncertain delivery is distinct and never exposes server detail',
|
||||
() async {
|
||||
|
||||
@@ -31,10 +31,29 @@ Future<void> reveal(
|
||||
backwards ? -250 : 250,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('logs are opt in and included only when checked', (tester) async {
|
||||
final sender = RecordingSender();
|
||||
await tester.pumpWidget(MaterialApp(home: FeedbackPage(sender: sender)));
|
||||
await tester.enterText(find.byKey(const Key('feedback-message')), '无法登录');
|
||||
await reveal(tester, find.byKey(const Key('feedback-logs')));
|
||||
expect(
|
||||
tester
|
||||
.widget<CheckboxListTile>(find.byKey(const Key('feedback-logs')))
|
||||
.value,
|
||||
isFalse,
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('feedback-logs')));
|
||||
await tester.pump();
|
||||
await reveal(tester, find.byKey(const Key('feedback-submit')));
|
||||
await tester.tap(find.byKey(const Key('feedback-submit')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(sender.submissions.single.diagnosticLog, isNotNull);
|
||||
expect(sender.submissions.single.attachments, isEmpty);
|
||||
});
|
||||
testWidgets('uncertain delivery retains original payload for status check', (
|
||||
tester,
|
||||
) async {
|
||||
@@ -191,7 +210,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
await tester.enterText(find.byKey(const Key('feedback-message')), '旧问题');
|
||||
await tester.ensureVisible(find.byKey(const Key('feedback-add-media')));
|
||||
await reveal(tester, find.byKey(const Key('feedback-add-media')));
|
||||
await tester.tap(find.byKey(const Key('feedback-add-media')));
|
||||
await tester.pumpAndSettle();
|
||||
await reveal(tester, find.text('我要参与开发'), backwards: true);
|
||||
@@ -303,7 +322,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
await tester.enterText(find.byKey(const Key('feedback-message')), '问题');
|
||||
await tester.ensureVisible(find.byKey(const Key('feedback-add-media')));
|
||||
await reveal(tester, find.byKey(const Key('feedback-add-media')));
|
||||
await tester.tap(find.byKey(const Key('feedback-add-media')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('screen.png'), findsOneWidget);
|
||||
@@ -312,7 +331,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
expect(sender.submissions.single.attachments, hasLength(1));
|
||||
expect(find.text('screen.png'), findsOneWidget);
|
||||
await tester.ensureVisible(find.byTooltip('移除 screen.png'));
|
||||
await reveal(tester, find.byTooltip('移除 screen.png'), backwards: true);
|
||||
await tester.tap(find.byTooltip('移除 screen.png'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('screen.png'), findsNothing);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hepan_everyday/services/diagnostic_log.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
|
||||
@@ -26,6 +29,30 @@ void main() {
|
||||
platform.delegate.finished(url);
|
||||
}
|
||||
|
||||
test('login diagnostic breadcrumbs distinguish probe and network failures without raw details', () async {
|
||||
await session.openTimetable(term: SchoolTerm(73));
|
||||
session.setReadingActive(true);
|
||||
platform.web.javascript = (_) async => throw StateError('PRIVATE_TICKET');
|
||||
navigate('https://yjsjy.uestc.edu.cn/pyxx/home/index');
|
||||
await flushSchoolEvents();
|
||||
var log = utf8.decode(await DiagnosticLog.instance.export());
|
||||
expect(log, contains('school_probe_failed'));
|
||||
expect(log, isNot(contains('PRIVATE_TICKET')));
|
||||
platform.delegate.resourceError(
|
||||
WebResourceError(
|
||||
errorCode: -2,
|
||||
description: 'PRIVATE_NETWORK_DETAIL',
|
||||
isForMainFrame: true,
|
||||
url: platform.web.url,
|
||||
),
|
||||
);
|
||||
await flushSchoolEvents();
|
||||
log = utf8.decode(await DiagnosticLog.instance.export());
|
||||
expect(log, contains('web_resource_error'));
|
||||
expect(log, contains('"status":-2'));
|
||||
expect(log, isNot(contains('PRIVATE_NETWORK_DETAIL')));
|
||||
});
|
||||
|
||||
test('graduate logout function automatically opens timetable without an exit anchor', () async {
|
||||
await session.openTimetable(term: SchoolTerm(73));
|
||||
platform.web.javascript = (script) async =>
|
||||
|
||||
@@ -82,6 +82,10 @@ void main() {
|
||||
await finish();
|
||||
expect(session.pendingExams!.records, hasLength(12));
|
||||
expect(session.pendingExams!.requestedExamTypes, {1, 2, 3});
|
||||
final completedGeneration = session.pageGeneration;
|
||||
platform.delegate.started('https://eams.uestc.edu.cn/eams/');
|
||||
expect(session.pendingExams, isNull);
|
||||
expect(session.pageGeneration, greaterThan(completedGeneration));
|
||||
},
|
||||
);
|
||||
test('last type failure never publishes partial results', () async {
|
||||
|
||||
@@ -9,6 +9,21 @@ import 'package:hepan_everyday/data/schedule_file_import.dart';
|
||||
import 'package:hepan_everyday/domain/school_identity.dart';
|
||||
|
||||
void main() {
|
||||
test('unrecognized activity writes and incomplete declarations reject the entire timetable', () {
|
||||
const valid =
|
||||
'activity = new TaskActivity("T", "教师", "C", "课程(C.01)", "R", "教室", "010"); '
|
||||
'index = 0*unitCount+0; table0.activities[index][table0.activities[index].length]=activity;';
|
||||
for (final extra in [
|
||||
'table0.activities[11][table0.activities[11].length]=activity;',
|
||||
'activity = new TaskActivity("T", "教师", "C", "另一课程(C.02)", "R", "教室", "010")\n'
|
||||
'index = 0*unitCount+1; table0.activities[index][table0.activities[index].length]=activity;',
|
||||
]) {
|
||||
expect(
|
||||
() => parseActivityData('$valid $extra', unitCount: 12),
|
||||
throwsFormatException,
|
||||
);
|
||||
}
|
||||
});
|
||||
final uri = Uri.parse(
|
||||
'https://eams.uestc.edu.cn/eams/courseTableForStd.action',
|
||||
);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hepan_everyday/services/diagnostic_log.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'logs retain only ten minutes across reopening and omit login secrets',
|
||||
() async {
|
||||
final directory = await Directory.systemTemp.createTemp('tp-log-test');
|
||||
addTearDown(() => directory.delete(recursive: true));
|
||||
var now = DateTime.utc(2026, 10, 15, 10);
|
||||
final log = DiagnosticLog(clock: () => now);
|
||||
addTearDown(log.dispose);
|
||||
await log.attach(directory);
|
||||
log.add('old_event');
|
||||
now = now.add(const Duration(minutes: 11));
|
||||
log.navigation(
|
||||
'graduate',
|
||||
'finished',
|
||||
'https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/73?ticket=SECRET&username=PERSON',
|
||||
);
|
||||
log.add(
|
||||
'read_failed',
|
||||
fields: {'code': 'no_timetable', 'password': 'SECRET'},
|
||||
);
|
||||
await log.flush();
|
||||
final restored = DiagnosticLog(clock: () => now);
|
||||
addTearDown(restored.dispose);
|
||||
await restored.attach(directory);
|
||||
final text = utf8.decode(await restored.export());
|
||||
expect(text, contains('navigation_finished'));
|
||||
expect(text, contains('timetable'));
|
||||
expect(text, contains('no_timetable'));
|
||||
for (final value in [
|
||||
'old_event',
|
||||
'SECRET',
|
||||
'PERSON',
|
||||
'ticket',
|
||||
'"password"',
|
||||
'https://',
|
||||
]) {
|
||||
expect(text, isNot(contains(value)));
|
||||
}
|
||||
now = now.add(const Duration(minutes: 11));
|
||||
expect(
|
||||
utf8.decode(await restored.export()),
|
||||
isNot(contains('no_timetable')),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hepan_everyday/app/app_controller.dart';
|
||||
import 'package:hepan_everyday/app/app_theme.dart';
|
||||
import 'package:hepan_everyday/domain/app_snapshot.dart';
|
||||
import 'package:hepan_everyday/domain/exam_schedule.dart';
|
||||
import 'package:hepan_everyday/domain/school_identity.dart';
|
||||
import 'package:hepan_everyday/features/exams/exams_page.dart';
|
||||
|
||||
import '../support/fakes.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('query description has breathing room above exam cards', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = AppController(
|
||||
MemoryStore(),
|
||||
FakeNotifications(),
|
||||
FakeCalendar(),
|
||||
DateTime.now,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
controller.snapshot = AppSnapshot(
|
||||
examSchedule: ExamSchedule(
|
||||
semesterLabel: '2025-2026第二学期',
|
||||
termId: '503',
|
||||
school: SupportedSchool.uestcUndergraduate,
|
||||
requestedExamTypes: {1},
|
||||
records: [const ExamRecord(code: 'C1', name: '测试考试', examTypeId: 1)],
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ExamsPage(controller: controller, onSync: () {}),
|
||||
),
|
||||
),
|
||||
);
|
||||
final description = tester.getRect(find.text('查询类型:期末考试'));
|
||||
final card = tester.getRect(find.byType(SurfaceCard).first);
|
||||
expect(card.top - description.bottom, greaterThanOrEqualTo(12));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user