feat: connect undergraduate manual timetable and exam batches
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
|
||||
import 'package:hepan_everyday/features/school/undergraduate/session.dart';
|
||||
import 'package:hepan_everyday/features/school/undergraduate/page_probe.dart';
|
||||
import 'package:hepan_everyday/domain/school_term.dart';
|
||||
import 'package:hepan_everyday/domain/school_identity.dart';
|
||||
import 'package:hepan_everyday/domain/undergraduate_exam_query.dart';
|
||||
|
||||
import '../support/fake_school_web.dart';
|
||||
import 'undergraduate_session_test.dart' show underEvidence;
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
late FakeSchoolPlatform platform;
|
||||
late UndergraduateSession session;
|
||||
final query = UndergraduateExamQuery(
|
||||
term: SchoolTerm(503, school: SupportedSchool.uestcUndergraduate),
|
||||
types: UndergraduateExamType.values.toSet(),
|
||||
);
|
||||
setUp(() {
|
||||
platform = FakeSchoolPlatform();
|
||||
WebViewPlatform.instance = platform;
|
||||
session = UndergraduateSession();
|
||||
});
|
||||
tearDown(() => session.dispose());
|
||||
String exam(int type) {
|
||||
final text = File('test/fixtures/school/undergraduate-exams.html')
|
||||
.readAsStringSync();
|
||||
return text
|
||||
.replaceAll('examType.id=1', 'examType.id=$type')
|
||||
.replaceAll(
|
||||
'name="examType.id" value="1"',
|
||||
'name="examType.id" value="$type"',
|
||||
)
|
||||
.replaceAll(
|
||||
'期末考试',
|
||||
type == 2
|
||||
? '期中考试'
|
||||
: type == 3
|
||||
? '补缓考'
|
||||
: '期末考试',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> finish() async {
|
||||
platform.delegate.started(platform.web.url!);
|
||||
platform.delegate.finished(platform.web.url!);
|
||||
for (var i = 0; i < 40; i++) {
|
||||
await Future<void>.value();
|
||||
}
|
||||
}
|
||||
|
||||
test(
|
||||
'all selected exam types complete before publishing one result',
|
||||
() async {
|
||||
await session.openExams(query);
|
||||
platform.web.javascript = (s) async {
|
||||
final id = int.tryParse(
|
||||
Uri.parse(platform.web.url!).queryParameters['examType.id'] ?? '',
|
||||
);
|
||||
return underEvidence(
|
||||
platform.web.url!,
|
||||
html: s == undergraduateCaptureScript && id != null ? exam(id) : null,
|
||||
kind: 'exams',
|
||||
);
|
||||
};
|
||||
await finish();
|
||||
expect(
|
||||
platform.web.loads.last,
|
||||
query.uriFor(UndergraduateExamType.finalExam).toString(),
|
||||
);
|
||||
await finish();
|
||||
expect(session.pendingExams, isNull);
|
||||
expect(
|
||||
platform.web.loads.last,
|
||||
query.uriFor(UndergraduateExamType.midterm).toString(),
|
||||
);
|
||||
await finish();
|
||||
expect(session.pendingExams, isNull);
|
||||
await finish();
|
||||
expect(session.pendingExams!.records, hasLength(12));
|
||||
expect(session.pendingExams!.requestedExamTypes, {1, 2, 3});
|
||||
},
|
||||
);
|
||||
test('last type failure never publishes partial results', () async {
|
||||
await session.openExams(query);
|
||||
platform.web.javascript = (s) async {
|
||||
final id = int.tryParse(
|
||||
Uri.parse(platform.web.url!).queryParameters['examType.id'] ?? '',
|
||||
);
|
||||
return underEvidence(
|
||||
platform.web.url!,
|
||||
html: s == undergraduateCaptureScript && id != null
|
||||
? (id == 3 ? exam(1) : exam(id))
|
||||
: null,
|
||||
kind: 'exams',
|
||||
);
|
||||
};
|
||||
await finish();
|
||||
await finish();
|
||||
await finish();
|
||||
await finish();
|
||||
expect(session.pendingExams, isNull);
|
||||
expect(session.message, contains('未完成'));
|
||||
});
|
||||
testWidgets('login waiting is excluded and active reads have timeouts', (
|
||||
tester,
|
||||
) async {
|
||||
await session.openExams(query);
|
||||
platform.web.javascript = (_) async =>
|
||||
underEvidence(platform.web.url!, authenticated: false);
|
||||
await finish();
|
||||
await tester.pump(const Duration(seconds: 121));
|
||||
expect(session.awaitingAuthentication, isTrue);
|
||||
expect(session.message, isNot(contains('超时')));
|
||||
platform.web.javascript = (_) async => underEvidence(platform.web.url!);
|
||||
await finish();
|
||||
await tester.pump(const Duration(seconds: 46));
|
||||
expect(session.message, contains('超时'));
|
||||
expect(session.pendingExams, isNull);
|
||||
});
|
||||
test('cancelled batch ignores late page callbacks', () async {
|
||||
await session.openExams(query);
|
||||
final generation = session.pageGeneration;
|
||||
session.setReadingActive(false);
|
||||
platform.web.javascript = (_) async =>
|
||||
underEvidence(platform.web.url!, html: exam(1), kind: 'exams');
|
||||
await finish();
|
||||
expect(session.pendingExams, isNull);
|
||||
expect(session.pageGeneration, greaterThan(generation));
|
||||
});
|
||||
test(
|
||||
'expired authentication discards earlier parts and restarts every type',
|
||||
() async {
|
||||
await session.openExams(query);
|
||||
var renewed = false;
|
||||
platform.web.javascript = (s) async {
|
||||
final uri = Uri.parse(platform.web.url!),
|
||||
id = int.tryParse(uri.queryParameters['examType.id'] ?? '');
|
||||
return underEvidence(
|
||||
uri.toString(),
|
||||
authenticated: uri.host == 'eams.uestc.edu.cn',
|
||||
html: s == undergraduateCaptureScript && id != null
|
||||
? exam(id).replaceAll('<td>E', '<td>${renewed ? 'NEW' : 'OLD'}E')
|
||||
: null,
|
||||
kind: 'exams',
|
||||
);
|
||||
};
|
||||
await finish();
|
||||
await finish();
|
||||
expect(
|
||||
platform.web.loads.last,
|
||||
query.uriFor(UndergraduateExamType.midterm).toString(),
|
||||
);
|
||||
platform.web.url = 'https://idas.uestc.edu.cn/authserver/login';
|
||||
await finish();
|
||||
expect(session.awaitingAuthentication, isTrue);
|
||||
renewed = true;
|
||||
platform.web.url = 'https://eams.uestc.edu.cn/eams/';
|
||||
await finish();
|
||||
expect(
|
||||
platform.web.loads.last,
|
||||
query.uriFor(UndergraduateExamType.finalExam).toString(),
|
||||
);
|
||||
await finish();
|
||||
await finish();
|
||||
await finish();
|
||||
expect(
|
||||
session.pendingExams!.records.every((r) => r.code.startsWith('NEW')),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
testWidgets('the overall batch timeout limits individually timely requests', (
|
||||
tester,
|
||||
) async {
|
||||
await session.openExams(query);
|
||||
platform.web.javascript = (s) async {
|
||||
final id = int.tryParse(
|
||||
Uri.parse(platform.web.url!).queryParameters['examType.id'] ?? '',
|
||||
);
|
||||
return underEvidence(
|
||||
platform.web.url!,
|
||||
html: s == undergraduateCaptureScript && id != null ? exam(id) : null,
|
||||
kind: 'exams',
|
||||
);
|
||||
};
|
||||
await finish();
|
||||
await tester.pump(const Duration(seconds: 40));
|
||||
await finish();
|
||||
await tester.pump(const Duration(seconds: 40));
|
||||
await finish();
|
||||
await tester.pump(const Duration(seconds: 41));
|
||||
expect(session.pendingExams, isNull);
|
||||
expect(session.message, contains('超时'));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
|
||||
import 'package:hepan_everyday/features/school/undergraduate/session.dart';
|
||||
import 'package:hepan_everyday/features/school/undergraduate/page_probe.dart';
|
||||
|
||||
import '../support/fake_school_web.dart';
|
||||
|
||||
String underEvidence(
|
||||
String url, {
|
||||
bool authenticated = true,
|
||||
String? html,
|
||||
String kind = 'timetable',
|
||||
}) => jsonEncode({
|
||||
'topUrl': url,
|
||||
'authenticated': authenticated,
|
||||
'pages': [
|
||||
if (html != null) {'kind': kind, 'url': url, 'html': html},
|
||||
],
|
||||
});
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
late FakeSchoolPlatform platform;
|
||||
late UndergraduateSession session;
|
||||
setUp(() {
|
||||
platform = FakeSchoolPlatform();
|
||||
WebViewPlatform.instance = platform;
|
||||
session = UndergraduateSession();
|
||||
});
|
||||
tearDown(() => session.dispose());
|
||||
test(
|
||||
'manual session is lazy and never captures until the student taps import',
|
||||
() async {
|
||||
expect(platform.controllerCreations, 0);
|
||||
await session.openTimetable();
|
||||
expect(platform.web.loads, ['https://eams.uestc.edu.cn/eams/']);
|
||||
final content = File('test/fixtures/school/undergraduate-timetable.html')
|
||||
.readAsStringSync();
|
||||
var captures = 0;
|
||||
platform.web.javascript = (s) async {
|
||||
if (s == undergraduateCaptureScript) {
|
||||
captures++;
|
||||
return underEvidence(platform.web.url!, html: content);
|
||||
}
|
||||
return underEvidence(platform.web.url!);
|
||||
};
|
||||
platform.delegate.finished(platform.web.url!);
|
||||
await flushSchoolEvents();
|
||||
expect(captures, 0);
|
||||
expect(session.pendingDraft, isNull);
|
||||
await session.readCurrentTimetable();
|
||||
expect(captures, 1);
|
||||
expect(session.pendingDraft!.courses, hasLength(18));
|
||||
},
|
||||
);
|
||||
test('closed routes reject old asynchronous capture and save', () async {
|
||||
await session.openTimetable();
|
||||
final gate = Completer<Object>();
|
||||
platform.web.javascript = (_) => gate.future;
|
||||
final read = session.readCurrentTimetable();
|
||||
await flushSchoolEvents();
|
||||
final generation = session.pageGeneration;
|
||||
session.setReadingActive(false);
|
||||
gate.complete(
|
||||
underEvidence(
|
||||
platform.web.url!,
|
||||
html: File('test/fixtures/school/undergraduate-timetable.html')
|
||||
.readAsStringSync(),
|
||||
),
|
||||
);
|
||||
await read;
|
||||
expect(session.pendingDraft, isNull);
|
||||
expect(session.saved(generation: generation), isFalse);
|
||||
});
|
||||
test(
|
||||
'unused logout stays lazy and failed cookie clearing can be retried',
|
||||
() async {
|
||||
await session.logout();
|
||||
expect(platform.controllerCreations, 0);
|
||||
await session.openTimetable();
|
||||
platform.cookies.fail = true;
|
||||
await session.logout();
|
||||
expect(session.canLogout, isTrue);
|
||||
platform.cookies.fail = false;
|
||||
await session.logout();
|
||||
expect(session.canLogout, isFalse);
|
||||
},
|
||||
);
|
||||
test('new login waits for an outstanding cookie clear', () async {
|
||||
await session.openTimetable();
|
||||
platform.cookies.clearing = Completer<bool>();
|
||||
final closing = session.logout();
|
||||
await flushSchoolEvents();
|
||||
final reopening = session.openTimetable();
|
||||
await flushSchoolEvents();
|
||||
platform.cookies.clearing!.complete(true);
|
||||
await closing;
|
||||
await reopening;
|
||||
expect(session.hasOpened, isTrue);
|
||||
expect(platform.web.loads.last, 'https://eams.uestc.edu.cn/eams/');
|
||||
});
|
||||
}
|
||||
@@ -4,13 +4,18 @@ import 'dart:convert';
|
||||
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
|
||||
|
||||
class FakeSchoolPlatform extends WebViewPlatform {
|
||||
int controllerCreations = 0;
|
||||
late FakeSchoolWeb web;
|
||||
late FakeSchoolDelegate delegate;
|
||||
final cookies = FakeSchoolCookies();
|
||||
@override
|
||||
PlatformWebViewController createPlatformWebViewController(
|
||||
PlatformWebViewControllerCreationParams params,
|
||||
) => web = FakeSchoolWeb(params);
|
||||
) {
|
||||
controllerCreations++;
|
||||
return web = FakeSchoolWeb(params);
|
||||
}
|
||||
|
||||
@override
|
||||
PlatformNavigationDelegate createPlatformNavigationDelegate(
|
||||
PlatformNavigationDelegateCreationParams params,
|
||||
|
||||
Reference in New Issue
Block a user