From 9e6f4c9303869a18da5cf94cb0dcb7b3a0939534 Mon Sep 17 00:00:00 2001 From: code3743 Date: Mon, 14 Sep 2026 08:53:50 -0500 Subject: [PATCH 1/2] fix: enhance regex patterns and parsing logic in SiraScheduleRemoteDataSource --- .../sira_schedule_remote_datasource.dart | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/lib/features/schedule/data/datasources/sira_schedule_remote_datasource.dart b/lib/features/schedule/data/datasources/sira_schedule_remote_datasource.dart index 9f2a345..2801d50 100644 --- a/lib/features/schedule/data/datasources/sira_schedule_remote_datasource.dart +++ b/lib/features/schedule/data/datasources/sira_schedule_remote_datasource.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:html/dom.dart'; -import 'package:html/parser.dart' show parse; +import 'package:html/parser.dart' show parse, parseFragment; import '../../../../core/constants/schedule_constants.dart'; import '../../../../core/error/exceptions.dart'; @@ -17,10 +17,12 @@ class SiraScheduleRemoteDataSource { const SiraScheduleRemoteDataSource(this._dio); // Matches a session's day/time/building line when a room is assigned, - // e.g. "JUE: 14:00-17:00 , Edf. B13". Hours may be one or two digits - // ("8:00" as well as "14:00"). + // e.g. "JUE: 14:00-17:00 , Edf. B13 (B13) -> SALA 1 -- MG -- MELENDEZ". + // Hours may be one or two digits ("8:00" as well as "14:00"). The + // location detail after the building code is optional and, when + // present, captured whole in group 5 for `_locationLine` to parse. static final _sessionWithBuilding = RegExp( - r'^(LUN|MAR|MI[EÉ]|JUE|VIE|SAB|DOM):\s*(\d{1,2}:\d{2})\s*-\s*(\d{1,2}:\d{2})\s*,\s*Edf\.\s*(.+)$', + r'^(LUN|MAR|MI[EÉ]|JUE|VIE|SAB|DOM):\s*(\d{1,2}:\d{2})\s*-\s*(\d{1,2}:\d{2})\s*,\s*Edf\.\s*(\S+)\s*(\(.*)?$', caseSensitive: false, ); @@ -32,8 +34,9 @@ class SiraScheduleRemoteDataSource { caseSensitive: false, ); - // Matches the location line that follows a `_sessionWithBuilding` line, - // e.g. "(B13) -> SALA 1 -- MG -- MELENDEZ". + // Matches the location detail, e.g. "(B13) -> SALA 1 -- MG -- MELENDEZ" — + // usually on the same line as `_sessionWithBuilding` right after the + // building code, occasionally (defensively handled) on the next line. static final _locationLine = RegExp( r'^\([^)]*\)\s*->\s*(.+?)\s*--\s*(.+?)\s*--\s*(.+)$', ); @@ -76,11 +79,11 @@ class SiraScheduleRemoteDataSource { if (cells.length != 9) return const []; if (cells[2].text.trim() != subject.group) return const []; - final scheduleText = cells[4].text.trim(); - if (scheduleText.isEmpty) return const []; + final scheduleLines = _splitByBreaks(cells[4]); + if (scheduleLines.isEmpty) return const []; final teacher = _parseTeacher(cells[5].text.trim()); - return _parseSessions(scheduleText) + return _parseSessions(scheduleLines) .map( (session) => ScheduleClassModel( subjectCode: subject.code, @@ -99,18 +102,22 @@ class SiraScheduleRemoteDataSource { .toList(); } - // The schedule cell packs one or more sessions. When a room is assigned - // it's an alternating pair of lines: "JUE: 14:00-17:00 , Edf. B13" then - // "(B13) -> SALA 1 -- MG -- MELENDEZ" (the middle "--" segment is an - // internal room-type code that isn't meaningful to show the student). - // When it isn't, it's a single line: "JUE: 8:00-12:00 ,SIN ESPACIO -- MG". - List<_ParsedSession> _parseSessions(String rawText) { - final lines = rawText - .split('\n') - .map((line) => line.trim()) - .where((line) => line.isNotEmpty) - .toList(); - + // The schedule cell packs one or more sessions separated by
tags + // rather than actual newlines in the source HTML, so lines must be split + // on the markup itself — splitting the extracted text on '\n' leaves + // every session glued into one string with no separator to find. + List _splitByBreaks(Element cell) => cell.innerHtml + .split(RegExp(r'', caseSensitive: false)) + .map((fragment) => parseFragment(fragment).text?.trim() ?? '') + .where((line) => line.isNotEmpty) + .toList(); + + // The schedule cell packs one or more sessions, each its own
- + // separated line: "JUE: 14:00-17:00 , Edf. B13 (B13) -> SALA 1 -- MG -- + // MELENDEZ" (the middle "--" segment is an internal room-type code that + // isn't meaningful to show the student). When no room is assigned yet, + // it's just "JUE: 8:00-12:00 ,SIN ESPACIO -- MG" with no location detail. + List<_ParsedSession> _parseSessions(List lines) { final sessions = <_ParsedSession>[]; var i = 0; while (i < lines.length) { @@ -118,7 +125,14 @@ class SiraScheduleRemoteDataSource { if (withBuilding != null) { var room = ''; var campus = ''; - if (i + 1 < lines.length) { + final sameLineLocation = withBuilding.group(5); + if (sameLineLocation != null) { + final locationMatch = _locationLine.firstMatch(sameLineLocation); + if (locationMatch != null) { + room = locationMatch.group(1)!.trim(); + campus = locationMatch.group(3)!.trim(); + } + } else if (i + 1 < lines.length) { final locationMatch = _locationLine.firstMatch(lines[i + 1]); if (locationMatch != null) { room = locationMatch.group(1)!.trim(); From 325c1d5d20d12161c9f10718879c534e4760df0c Mon Sep 17 00:00:00 2001 From: code3743 Date: Mon, 14 Sep 2026 09:41:38 -0500 Subject: [PATCH 2/2] test: add HTML template for teacher rating page --- test/core/error/exceptions_test.dart | 36 + test/core/error/failures_test.dart | 7 + .../shared_preferences_service_test.dart | 100 + .../sira_auth_remote_datasource_test.dart | 275 +++ .../library_remote_datasource_test.dart | 178 ++ .../sira_profile_remote_datasource_test.dart | 137 ++ ...ira_resolution_remote_datasource_test.dart | 110 + .../sira_schedule_remote_datasource_test.dart | 220 ++ .../sira_grades_remote_datasource_test.dart | 170 ++ .../sira_tabulate_remote_datasource_test.dart | 101 + ...rse_evaluation_remote_datasource_test.dart | 431 ++++ test/fixtures/auth/login_error.html | 116 + test/fixtures/auth/reset_password_form.html | 88 + .../fixtures/auth/reset_password_success.html | 48 + test/fixtures/library/opac_account.html | 176 ++ test/fixtures/library/opac_session.html | 252 ++ test/fixtures/profile/contact_info.html | 675 ++++++ test/fixtures/resolution/curriculum.html | 826 +++++++ test/fixtures/schedule/subject_schedule.html | 243 ++ .../academic_record_report.html | 2028 +++++++++++++++++ .../student_tabulate/tabulate_report.html | 180 ++ .../teacher_review_not_configured.html | 325 +++ .../teacher_review_questions.html | 973 ++++++++ .../teaching_rating/teachers_to_rate.html | 192 ++ 24 files changed, 7887 insertions(+) create mode 100644 test/core/error/exceptions_test.dart create mode 100644 test/core/storage/shared_preferences_service_test.dart create mode 100644 test/features/auth/data/datasources/sira_auth_remote_datasource_test.dart create mode 100644 test/features/library/data/datasources/library_remote_datasource_test.dart create mode 100644 test/features/profile/data/datasources/sira_profile_remote_datasource_test.dart create mode 100644 test/features/resolution/data/datasources/sira_resolution_remote_datasource_test.dart create mode 100644 test/features/schedule/data/datasources/sira_schedule_remote_datasource_test.dart create mode 100644 test/features/student_grades/data/datasources/sira_grades_remote_datasource_test.dart create mode 100644 test/features/student_tabulate/data/datasources/sira_tabulate_remote_datasource_test.dart create mode 100644 test/features/teaching_rating/data/datasources/course_evaluation_remote_datasource_test.dart create mode 100644 test/fixtures/auth/login_error.html create mode 100644 test/fixtures/auth/reset_password_form.html create mode 100644 test/fixtures/auth/reset_password_success.html create mode 100644 test/fixtures/library/opac_account.html create mode 100644 test/fixtures/library/opac_session.html create mode 100644 test/fixtures/profile/contact_info.html create mode 100644 test/fixtures/resolution/curriculum.html create mode 100644 test/fixtures/schedule/subject_schedule.html create mode 100644 test/fixtures/student_grades/academic_record_report.html create mode 100644 test/fixtures/student_tabulate/tabulate_report.html create mode 100644 test/fixtures/teaching_rating/teacher_review_not_configured.html create mode 100644 test/fixtures/teaching_rating/teacher_review_questions.html create mode 100644 test/fixtures/teaching_rating/teachers_to_rate.html diff --git a/test/core/error/exceptions_test.dart b/test/core/error/exceptions_test.dart new file mode 100644 index 0000000..ff498a3 --- /dev/null +++ b/test/core/error/exceptions_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:univalle_app/core/error/exceptions.dart'; + +void main() { + test('ServerException carries an optional status code', () { + final exception = ServerException(message: 'boom', statusCode: 500); + expect(exception.message, 'boom'); + expect(exception.statusCode, 500); + }); + + test('NetworkException carries its message', () { + expect(NetworkException(message: 'boom').message, 'boom'); + }); + + test('CacheException carries its message', () { + expect(CacheException(message: 'boom').message, 'boom'); + }); + + test('AuthException carries its message', () { + expect(AuthException(message: 'boom').message, 'boom'); + }); + + test('BusinessException defaults to retryable', () { + final exception = BusinessException(message: 'boom'); + expect(exception.retryable, isTrue); + }); + + test('BusinessException can be marked as non-retryable', () { + final exception = BusinessException(message: 'boom', retryable: false); + expect(exception.retryable, isFalse); + }); + + test('toString returns the runtime type', () { + expect(NetworkException(message: 'boom').toString(), 'NetworkException'); + }); +} diff --git a/test/core/error/failures_test.dart b/test/core/error/failures_test.dart index 749315b..1b53019 100644 --- a/test/core/error/failures_test.dart +++ b/test/core/error/failures_test.dart @@ -77,4 +77,11 @@ void main() { final failure = BusinessFailure(message: 'x', retryable: false); expect(failure.retryable, isFalse); }); + + test('toString includes the internal message', () { + expect( + NetworkFailure(message: 'boom').toString(), + 'Failure(message: boom)', + ); + }); } diff --git a/test/core/storage/shared_preferences_service_test.dart b/test/core/storage/shared_preferences_service_test.dart new file mode 100644 index 0000000..179d175 --- /dev/null +++ b/test/core/storage/shared_preferences_service_test.dart @@ -0,0 +1,100 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:univalle_app/core/error/exceptions.dart'; +import 'package:univalle_app/core/storage/shared_preferences_service.dart'; + +class MockSharedPreferences extends Mock implements SharedPreferences {} + +void main() { + late MockSharedPreferences preferences; + late SharedPreferencesService service; + + setUp(() { + preferences = MockSharedPreferences(); + service = SharedPreferencesService(preferences); + }); + + test('getString reads through to SharedPreferences', () async { + when(() => preferences.getString('k')).thenReturn('v'); + expect(await service.getString('k'), 'v'); + }); + + test('setString writes through to SharedPreferences', () async { + when(() => preferences.setString('k', 'v')).thenAnswer((_) async => true); + await service.setString('k', 'v'); + verify(() => preferences.setString('k', 'v')).called(1); + }); + + test('getBool reads through to SharedPreferences', () async { + when(() => preferences.getBool('k')).thenReturn(true); + expect(await service.getBool('k'), isTrue); + }); + + test('setBool writes through to SharedPreferences', () async { + when(() => preferences.setBool('k', true)).thenAnswer((_) async => true); + await service.setBool('k', true); + verify(() => preferences.setBool('k', true)).called(1); + }); + + test('getInt reads through to SharedPreferences', () async { + when(() => preferences.getInt('k')).thenReturn(42); + expect(await service.getInt('k'), 42); + }); + + test('setInt writes through to SharedPreferences', () async { + when(() => preferences.setInt('k', 42)).thenAnswer((_) async => true); + await service.setInt('k', 42); + verify(() => preferences.setInt('k', 42)).called(1); + }); + + test('getDouble reads through to SharedPreferences', () async { + when(() => preferences.getDouble('k')).thenReturn(4.2); + expect(await service.getDouble('k'), 4.2); + }); + + test('setDouble writes through to SharedPreferences', () async { + when(() => preferences.setDouble('k', 4.2)).thenAnswer((_) async => true); + await service.setDouble('k', 4.2); + verify(() => preferences.setDouble('k', 4.2)).called(1); + }); + + test('getStringList reads through to SharedPreferences', () async { + when(() => preferences.getStringList('k')).thenReturn(['a', 'b']); + expect(await service.getStringList('k'), ['a', 'b']); + }); + + test('setStringList writes through to SharedPreferences', () async { + when(() => preferences.setStringList('k', ['a', 'b'])) + .thenAnswer((_) async => true); + await service.setStringList('k', ['a', 'b']); + verify(() => preferences.setStringList('k', ['a', 'b'])).called(1); + }); + + test('remove delegates to SharedPreferences', () async { + when(() => preferences.remove('k')).thenAnswer((_) async => true); + await service.remove('k'); + verify(() => preferences.remove('k')).called(1); + }); + + test('clear delegates to SharedPreferences', () async { + when(() => preferences.clear()).thenAnswer((_) async => true); + await service.clear(); + verify(() => preferences.clear()).called(1); + }); + + test('wraps any thrown error as a CacheException', () async { + when(() => preferences.getString('k')).thenThrow(Exception('disk full')); + + await expectLater( + () => service.getString('k'), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('disk full'), + ), + ), + ); + }); +} diff --git a/test/features/auth/data/datasources/sira_auth_remote_datasource_test.dart b/test/features/auth/data/datasources/sira_auth_remote_datasource_test.dart new file mode 100644 index 0000000..c25da17 --- /dev/null +++ b/test/features/auth/data/datasources/sira_auth_remote_datasource_test.dart @@ -0,0 +1,275 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:univalle_app/core/constants/sira_constants.dart'; +import 'package:univalle_app/core/error/exceptions.dart'; +import 'package:univalle_app/features/auth/auth_strings.dart'; +import 'package:univalle_app/features/auth/data/datasources/sira_auth_remote_datasource.dart'; + +class MockDio extends Mock implements Dio {} + +Response> _bytesResponse(String html, {int statusCode = 200}) => + Response( + requestOptions: RequestOptions(path: ''), + statusCode: statusCode, + data: latin1.encode(html), + ); + +Response _plainResponse(String text, {int statusCode = 200}) => + Response( + requestOptions: RequestOptions(path: SiraConstants.resetPasswordUrl), + statusCode: statusCode, + data: text, + ); + +void main() { + late MockDio dio; + late SiraAuthRemoteDataSource dataSource; + + setUpAll(() { + registerFallbackValue({}); + registerFallbackValue(Options()); + }); + + setUp(() { + dio = MockDio(); + dataSource = SiraAuthRemoteDataSource(dio); + }); + + group('login', () { + test( + 'throws AuthException without a request for empty credentials', + () async { + await expectLater( + () => dataSource.login(username: '', password: ''), + throwsA(isA()), + ); + verifyNever(() => dio.post(any(), data: any(named: 'data'))); + }, + ); + + test('succeeds silently when SIRA redirects (valid credentials)', () async { + when( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer((_) async => _bytesResponse('', statusCode: 302)); + + await expectLater( + dataSource.login(username: 'jperez-3743', password: 'secret'), + completes, + ); + }); + + test('parses the failure message from a real login error page', () async { + final html = File('test/fixtures/auth/login_error.html') + .readAsStringSync(); + when( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer((_) async => _bytesResponse(html, statusCode: 200)); + + await expectLater( + dataSource.login(username: 'jperez-3743', password: 'wrong'), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Verifique su LOGIN y CONTRASEÑA.', + ), + ), + ); + }); + + test( + 'accepts redirects and rejects hard failures as valid status', + () async { + when( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer((_) async => _bytesResponse('', statusCode: 302)); + + await dataSource.login(username: 'jperez-3743', password: 'secret'); + + final options = + verify( + () => dio.post( + any(), + data: any(named: 'data'), + options: captureAny(named: 'options'), + ), + ).captured.single + as Options; + expect(options.validateStatus!(200), isTrue); + expect(options.validateStatus!(302), isTrue); + expect(options.validateStatus!(399), isTrue); + expect(options.validateStatus!(400), isFalse); + expect(options.validateStatus!(500), isFalse); + expect(options.validateStatus!(null), isFalse); + }, + ); + }); + + group('logout', () { + test('requests the SIRA logout endpoint', () async { + when(() => dio.get(any())).thenAnswer((_) async => _bytesResponse('')); + + await dataSource.logout(); + + verify(() => dio.get(SiraConstants.logoutPath)).called(1); + }); + + test('maps a DioException to an AppException', () async { + when(() => dio.get(any())).thenThrow( + DioException( + requestOptions: RequestOptions(path: SiraConstants.logoutPath), + type: DioExceptionType.connectionTimeout, + ), + ); + + expect(() => dataSource.logout(), throwsA(isA())); + }); + }); + + group('resetPassword', () { + test( + 'throws AuthException without a request for an empty username', + () async { + await expectLater( + () => dataSource.resetPassword(username: ''), + throwsA(isA()), + ); + verifyNever(() => dio.get(any(), options: any(named: 'options'))); + }, + ); + + test( + 'fetches the csrf token and returns the masked confirmation email', + () async { + final formHtml = File('test/fixtures/auth/reset_password_form.html') + .readAsStringSync(); + final successHtml = File( + 'test/fixtures/auth/reset_password_success.html', + ).readAsStringSync(); + + when(() => dio.get(any(), options: any(named: 'options'))) + .thenAnswer((_) async => _plainResponse(formHtml)); + when( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer((_) async => _plainResponse(successHtml)); + + final email = await dataSource.resetPassword(username: 'jperez-3743'); + + expect(email, 'usu***ario@correounivalle.edu.co'); + final captured = + verify( + () => dio.post( + any(), + data: captureAny(named: 'data'), + options: any(named: 'options'), + ), + ).captured.single + as Map; + expect( + captured['parametros[_csrf_token]'], + 'b81f83ee4b3ea839b0a0d2f822f60b56', + ); + expect(captured['parametros[usuario]'], 'jperez-3743'); + }, + ); + + test('throws when the reset form request does not return 200', () async { + when(() => dio.get(any(), options: any(named: 'options'))) + .thenAnswer((_) async => _plainResponse('', statusCode: 500)); + + expect( + () => dataSource.resetPassword(username: 'jperez-3743'), + throwsA( + isA().having( + (e) => e.message, + 'message', + AuthStrings.serverNotResponding, + ), + ), + ); + }); + + test('throws when the reset form has no csrf token', () async { + when( + () => dio.get(any(), options: any(named: 'options')), + ).thenAnswer((_) async => _plainResponse('')); + + expect( + () => dataSource.resetPassword(username: 'jperez-3743'), + throwsA( + isA().having( + (e) => e.message, + 'message', + AuthStrings.resetServiceUnavailable, + ), + ), + ); + }); + + test( + 'throws AuthException when the reset result page has an error', + () async { + final formHtml = File('test/fixtures/auth/reset_password_form.html') + .readAsStringSync(); + when(() => dio.get(any(), options: any(named: 'options'))) + .thenAnswer((_) async => _plainResponse(formHtml)); + when( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer( + (_) async => _plainResponse( + '
Usuario no encontrado
', + ), + ); + + expect( + () => dataSource.resetPassword(username: 'jperez-3743'), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Usuario no encontrado', + ), + ), + ); + }, + ); + + test('maps a DioException to an AppException', () async { + when(() => dio.get(any(), options: any(named: 'options'))).thenThrow( + DioException( + requestOptions: RequestOptions(path: SiraConstants.resetPasswordUrl), + type: DioExceptionType.connectionTimeout, + ), + ); + + expect( + () => dataSource.resetPassword(username: 'jperez-3743'), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/features/library/data/datasources/library_remote_datasource_test.dart b/test/features/library/data/datasources/library_remote_datasource_test.dart new file mode 100644 index 0000000..420438c --- /dev/null +++ b/test/features/library/data/datasources/library_remote_datasource_test.dart @@ -0,0 +1,178 @@ +import 'dart:io'; + +import 'package:cookie_jar/cookie_jar.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:univalle_app/core/constants/library_constants.dart'; +import 'package:univalle_app/core/error/exceptions.dart'; +import 'package:univalle_app/features/library/data/datasources/library_remote_datasource.dart'; +import 'package:univalle_app/features/library/library_strings.dart'; + +class MockDio extends Mock implements Dio {} + +Response _stringResponse(String html, {int statusCode = 200}) => + Response( + requestOptions: RequestOptions(path: ''), + statusCode: statusCode, + data: html, + ); + +void main() { + late MockDio dio; + late CookieJar cookieJar; + late LibraryRemoteDataSource dataSource; + late String sessionHtml; + late String accountHtml; + + setUpAll(() { + registerFallbackValue({}); + registerFallbackValue(Options()); + sessionHtml = File('test/fixtures/library/opac_session.html') + .readAsStringSync(); + accountHtml = File('test/fixtures/library/opac_account.html') + .readAsStringSync(); + }); + + setUp(() { + dio = MockDio(); + cookieJar = CookieJar(); + dataSource = LibraryRemoteDataSource(dio, cookieJar); + }); + + void stubAuthAndAccount({ + String? sessionResponse, + String? loginResponse, + String? accountResponse, + }) { + // A single stub routes by path: the home page GET (`_dio.get('')`) + // passes no `options` while the account page GET does, and mocktail's + // `any(named: 'options')` matches both shapes — registering them as two + // separate stubs makes whichever is registered last silently win for + // every call, home page included. + when(() => dio.get(any(), options: any(named: 'options'))) + .thenAnswer((invocation) async { + final path = invocation.positionalArguments[0] as String; + if (path == LibraryConstants.accountPath) { + return _stringResponse(accountResponse ?? accountHtml); + } + return _stringResponse(sessionResponse ?? sessionHtml); + }); + when( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer( + (_) async => + _stringResponse(loginResponse ?? ''), + ); + } + + group('fetchAccount', () { + test('parses the fine and loan history from a real OPAC account', () async { + stubAuthAndAccount(); + + final account = await dataSource.fetchAccount(username: '0000000-3743'); + + expect(account.currentFine, '0.00'); + expect(account.currentLoans, isEmpty); + expect(account.history, hasLength(4)); + + final first = account.history.first; + expect(first.code, '0579289'); + // OPAC's own title data has this stray space before the accented + // vowel ("Ingenier ía" instead of "Ingeniería") — captured verbatim. + expect(first.title, 'Ingenier ía de software'); + expect(first.location, 'Biblioteca Sede Tulua'); + expect(first.date, '20-Mar-2026 16:53'); + }); + + test( + 'accepts redirects and rejects hard failures as valid status', + () async { + stubAuthAndAccount(); + + await dataSource.fetchAccount(username: '0000000-3743'); + + final options = + verify( + () => dio.post( + any(), + data: any(named: 'data'), + options: captureAny(named: 'options'), + ), + ).captured.single + as Options; + expect(options.validateStatus!(200), isTrue); + expect(options.validateStatus!(399), isTrue); + expect(options.validateStatus!(400), isFalse); + expect(options.validateStatus!(null), isFalse); + }, + ); + + test('throws when the home page has no SessionID script', () async { + stubAuthAndAccount(sessionResponse: ''); + + expect( + () => dataSource.fetchAccount(username: '0000000-3743'), + throwsA( + isA().having( + (e) => e.message, + 'message', + LibraryStrings.noSessionId, + ), + ), + ); + }); + + test('throws AuthException when the login response has an error', () async { + stubAuthAndAccount( + loginResponse: '
Credenciales inválidas
', + ); + + expect( + () => dataSource.fetchAccount(username: '0000000-3743'), + throwsA(isA()), + ); + }); + + test('throws when the account page has no fine field', () async { + stubAuthAndAccount(accountResponse: ''); + + expect( + () => dataSource.fetchAccount(username: '0000000-3743'), + throwsA(isA()), + ); + }); + + test('clears cookies and rethrows when a step fails', () async { + stubAuthAndAccount(accountResponse: ''); + + await expectLater( + () => dataSource.fetchAccount(username: '0000000-3743'), + throwsA(isA()), + ); + + final cookies = await cookieJar.loadForRequest( + Uri.parse(LibraryConstants.baseUrl), + ); + expect(cookies, isEmpty); + }); + + test('maps a DioException to an AppException', () async { + when(() => dio.get(any(), options: any(named: 'options'))).thenThrow( + DioException( + requestOptions: RequestOptions(path: ''), + type: DioExceptionType.connectionTimeout, + ), + ); + + expect( + () => dataSource.fetchAccount(username: '0000000-3743'), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/features/profile/data/datasources/sira_profile_remote_datasource_test.dart b/test/features/profile/data/datasources/sira_profile_remote_datasource_test.dart new file mode 100644 index 0000000..46a8fbc --- /dev/null +++ b/test/features/profile/data/datasources/sira_profile_remote_datasource_test.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:univalle_app/core/constants/sira_constants.dart'; +import 'package:univalle_app/core/error/exceptions.dart'; +import 'package:univalle_app/features/profile/data/datasources/sira_profile_remote_datasource.dart'; + +class MockDio extends Mock implements Dio {} + +// The academic-record half of fetchStudent hits the same "Generar Carpeta" +// report as student_grades, so that already-captured fixture is reused here +// instead of recapturing the same page. +const _academicRecordFixture = + 'test/fixtures/student_grades/academic_record_report.html'; +const _contactInfoFixture = 'test/fixtures/profile/contact_info.html'; + +const _academicRecordFormHtml = ''' + +
+ +
+ +'''; + +Response> _htmlResponse(String html, {int statusCode = 200}) => + Response( + requestOptions: RequestOptions(path: SiraConstants.academicRecordPath), + statusCode: statusCode, + data: latin1.encode(html), + ); + +void main() { + late MockDio dio; + late SiraProfileRemoteDataSource dataSource; + late String contactInfoHtml; + late String academicRecordHtml; + + setUpAll(() { + registerFallbackValue({}); + contactInfoHtml = File(_contactInfoFixture).readAsStringSync(); + academicRecordHtml = File(_academicRecordFixture).readAsStringSync(); + }); + + setUp(() { + dio = MockDio(); + dataSource = SiraProfileRemoteDataSource(dio); + }); + + void stubFetchStudent({int? contactInfoStatusCode, int? recordStatusCode}) { + when(() => dio.post(any(), data: any(named: 'data'))).thenAnswer(( + invocation, + ) async { + final data = invocation.namedArguments[#data]; + if (data is Map && data['accion'] == 'desplegarFmInformacionDeContacto') { + return _htmlResponse( + contactInfoHtml, + statusCode: contactInfoStatusCode ?? 200, + ); + } + if (data is Map && data['accion'] == 'Generar Carpeta') { + return _htmlResponse( + academicRecordHtml, + statusCode: recordStatusCode ?? 200, + ); + } + return _htmlResponse(_academicRecordFormHtml); + }); + } + + group('fetchStudent', () { + test( + 'combines the contact info and academic record pages into a student', + () async { + stubFetchStudent(); + + final student = await dataSource.fetchStudent(username: '0000000-3743'); + + expect(student.documentId, '000000000'); + expect(student.firstName, 'PRUEBA'); + expect(student.lastName, 'ESTUDIANTE'); + expect(student.email, 'estudiante.prueba@correounivalle.edu.co'); + expect(student.programName, 'INGENIERÍA DE SISTEMAS'); + expect(student.campus, 'Tuluá'); + expect(student.average, 4.38); + expect(student.accumulatedCredits, 157); + }, + ); + + test('throws when the contact info request does not return 200', () async { + stubFetchStudent(contactInfoStatusCode: 500); + + expect( + () => dataSource.fetchStudent(username: '0000000-3743'), + throwsA(isA()), + ); + }); + + test( + 'throws when the academic record request does not return 200', + () async { + stubFetchStudent(recordStatusCode: 500); + + expect( + () => dataSource.fetchStudent(username: '0000000-3743'), + throwsA(isA()), + ); + }, + ); + + test('throws an AuthException when the contact info page has no name (expired session)', () async { + when(() => dio.post(any(), data: any(named: 'data'))) + .thenAnswer((_) async => _htmlResponse('')); + + expect( + () => dataSource.fetchStudent(username: '0000000-3743'), + throwsA(isA()), + ); + }); + + test('maps a DioException to an AppException', () async { + when(() => dio.post(any(), data: any(named: 'data'))).thenThrow( + DioException( + requestOptions: RequestOptions(path: SiraConstants.studentPath), + type: DioExceptionType.connectionTimeout, + ), + ); + + expect( + () => dataSource.fetchStudent(username: '0000000-3743'), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/features/resolution/data/datasources/sira_resolution_remote_datasource_test.dart b/test/features/resolution/data/datasources/sira_resolution_remote_datasource_test.dart new file mode 100644 index 0000000..fd9d365 --- /dev/null +++ b/test/features/resolution/data/datasources/sira_resolution_remote_datasource_test.dart @@ -0,0 +1,110 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:univalle_app/core/constants/sira_constants.dart'; +import 'package:univalle_app/core/error/exceptions.dart'; +import 'package:univalle_app/features/resolution/data/datasources/sira_resolution_remote_datasource.dart'; + +class MockDio extends Mock implements Dio {} + +Response> _htmlResponse(String html) => Response( + requestOptions: RequestOptions(path: SiraConstants.resolutionPath), + statusCode: 200, + data: latin1.encode(html), +); + +void main() { + late MockDio dio; + late SiraResolutionRemoteDataSource dataSource; + + setUpAll(() { + registerFallbackValue({}); + }); + + setUp(() { + dio = MockDio(); + dataSource = SiraResolutionRemoteDataSource(dio); + }); + + group('fetchCurriculum', () { + test('parses every subject in a real curriculum resolution', () async { + final html = File('test/fixtures/resolution/curriculum.html') + .readAsStringSync(); + when(() => dio.post(any(), data: any(named: 'data'))) + .thenAnswer((_) async => _htmlResponse(html)); + + final subjects = await dataSource.fetchCurriculum( + username: '0000000-3743', + ); + + expect(subjects, hasLength(44)); + }); + + test('parses a subject with no prerequisites', () async { + final html = File('test/fixtures/resolution/curriculum.html') + .readAsStringSync(); + when(() => dio.post(any(), data: any(named: 'data'))) + .thenAnswer((_) async => _htmlResponse(html)); + + final subjects = await dataSource.fetchCurriculum( + username: '0000000-3743', + ); + + final subject = subjects.firstWhere((s) => s.code == '111023C'); + expect(subject.semester, 1); + expect(subject.name, 'MATEMÁTICAS BÁSICAS'); + expect(subject.subjectType, 'AB'); + expect(subject.credits, 3); + expect(subject.prerequisiteCodes, isEmpty); + }); + + test( + 'merges a subject repeated once per prerequisite into one entry', + () async { + final html = File('test/fixtures/resolution/curriculum.html') + .readAsStringSync(); + when(() => dio.post(any(), data: any(named: 'data'))) + .thenAnswer((_) async => _htmlResponse(html)); + + final subjects = await dataSource.fetchCurriculum( + username: '0000000-3743', + ); + + final subject = subjects.firstWhere((s) => s.code == '750016C'); + expect(subject.semester, 5); + expect(subject.name, 'SIMULACIÓN Y COMPUTACIÓN NUMÉRICA'); + expect( + subject.prerequisiteCodes, + unorderedEquals(['106012C', '111026C', '111048M', '761001C']), + ); + }, + ); + + test('throws when the resolution table is empty', () async { + when(() => dio.post(any(), data: any(named: 'data'))) + .thenAnswer((_) async => _htmlResponse('')); + + expect( + () => dataSource.fetchCurriculum(username: '0000000-3743'), + throwsA(isA()), + ); + }); + + test('maps a DioException to an AppException', () async { + when(() => dio.post(any(), data: any(named: 'data'))).thenThrow( + DioException( + requestOptions: RequestOptions(path: SiraConstants.resolutionPath), + type: DioExceptionType.connectionTimeout, + ), + ); + + expect( + () => dataSource.fetchCurriculum(username: '0000000-3743'), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/features/schedule/data/datasources/sira_schedule_remote_datasource_test.dart b/test/features/schedule/data/datasources/sira_schedule_remote_datasource_test.dart new file mode 100644 index 0000000..f18c966 --- /dev/null +++ b/test/features/schedule/data/datasources/sira_schedule_remote_datasource_test.dart @@ -0,0 +1,220 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:univalle_app/core/constants/schedule_constants.dart'; +import 'package:univalle_app/core/error/exceptions.dart'; +import 'package:univalle_app/features/schedule/data/datasources/sira_schedule_remote_datasource.dart'; +import 'package:univalle_app/features/schedule/domain/entities/schedule_subject.dart'; +import 'package:univalle_app/features/schedule/domain/entities/weekday.dart'; + +class MockDio extends Mock implements Dio {} + +Response> _htmlResponse(String html, {int statusCode = 200}) => + Response( + requestOptions: RequestOptions(path: ScheduleConstants.path), + statusCode: statusCode, + data: latin1.encode(html), + ); + +const _subjectWithSchedule = ScheduleSubject( + code: '806010M', + group: '01', + campusId: '02', + name: 'INGENIERÍA DE SOFTWARE', +); + +const _subjectOtherGroup = ScheduleSubject( + code: '806010M', + group: '02', + campusId: '02', + name: 'INGENIERÍA DE SOFTWARE', +); + +const _subjectUnknownGroup = ScheduleSubject( + code: '806010M', + group: '99', + campusId: '02', + name: 'INGENIERÍA DE SOFTWARE', +); + +void main() { + late MockDio dio; + late SiraScheduleRemoteDataSource dataSource; + late String scheduleHtml; + + setUpAll(() { + registerFallbackValue({}); + scheduleHtml = File('test/fixtures/schedule/subject_schedule.html') + .readAsStringSync(); + }); + + setUp(() { + dio = MockDio(); + dataSource = SiraScheduleRemoteDataSource(dio); + }); + + group('fetchSchedule', () { + test( + 'parses every weekly session for a group with two class days', + () async { + when(() => dio.post(any(), data: any(named: 'data'))) + .thenAnswer((_) async => _htmlResponse(scheduleHtml)); + + final classes = await dataSource.fetchSchedule( + subjects: [_subjectWithSchedule], + ); + + expect(classes, hasLength(2)); + + final wednesday = classes.firstWhere((c) => c.day == Weekday.wednesday); + expect(wednesday.startTime, '14:00'); + expect(wednesday.endTime, '16:00'); + expect(wednesday.building, 'E23'); + expect(wednesday.room, '1020'); + expect(wednesday.campus, 'MELENDEZ'); + expect(wednesday.teacher, 'DOCENTE PRUEBA UNO'); + expect(wednesday.teacherEmail, 'docente.uno@correounivalle.edu.co'); + expect(wednesday.subjectCode, _subjectWithSchedule.code); + expect(wednesday.subjectName, _subjectWithSchedule.name); + expect(wednesday.group, _subjectWithSchedule.group); + + final monday = classes.firstWhere((c) => c.day == Weekday.monday); + expect(monday.startTime, '14:00'); + expect(monday.endTime, '16:00'); + expect(monday.building, 'E26'); + expect(monday.room, '1017'); + expect(monday.campus, 'MELENDEZ'); + }, + ); + + test( + 'parses a different group\'s own two sessions independently', + () async { + when(() => dio.post(any(), data: any(named: 'data'))) + .thenAnswer((_) async => _htmlResponse(scheduleHtml)); + + final classes = await dataSource.fetchSchedule( + subjects: [_subjectOtherGroup], + ); + + expect(classes, hasLength(2)); + final monday = classes.firstWhere((c) => c.day == Weekday.monday); + expect(monday.building, 'E23'); + expect(monday.room, '1011'); + final wednesday = classes.firstWhere((c) => c.day == Weekday.wednesday); + expect(wednesday.building, 'E20'); + expect(wednesday.room, 'SC2'); + }, + ); + + test( + 'returns no sessions for a group that has no row in the table', + () async { + when(() => dio.post(any(), data: any(named: 'data'))) + .thenAnswer((_) async => _htmlResponse(scheduleHtml)); + + final classes = await dataSource.fetchSchedule( + subjects: [_subjectUnknownGroup], + ); + + expect(classes, isEmpty); + }, + ); + + test('throws when the response does not return 200', () async { + when( + () => dio.post(any(), data: any(named: 'data')), + ).thenAnswer((_) async => _htmlResponse(scheduleHtml, statusCode: 500)); + + expect( + () => dataSource.fetchSchedule(subjects: [_subjectWithSchedule]), + throwsA(isA()), + ); + }); + + test('maps a DioException to an AppException', () async { + when(() => dio.post(any(), data: any(named: 'data'))).thenThrow( + DioException( + requestOptions: RequestOptions(path: ScheduleConstants.path), + type: DioExceptionType.connectionTimeout, + ), + ); + + expect( + () => dataSource.fetchSchedule(subjects: [_subjectWithSchedule]), + throwsA(isA()), + ); + }); + + // Neither format below appears in the real captured fixture, so these + // use small synthetic rows matching the same table shape to exercise + // the defensive/legacy parsing branches instead. + test( + 'falls back to a next-line location when none is on the same line', + () async { + const subject = ScheduleSubject( + code: '806010M', + group: 'X1', + campusId: '02', + name: 'INGENIERÍA DE SOFTWARE', + ); + when(() => dio.post(any(), data: any(named: 'data'))).thenAnswer( + (_) async => _htmlResponse(''' + + + + + + + +
1PERIODOX110 JUE: 14:00-17:00 , Edf. B13
(B13) -> SALA 1 -- MG -- MELENDEZ
PROFESOR SIN CORREO Programa
+'''), + ); + + final classes = await dataSource.fetchSchedule(subjects: [subject]); + + expect(classes, hasLength(1)); + expect(classes.single.building, 'B13'); + expect(classes.single.room, 'SALA 1'); + expect(classes.single.campus, 'MELENDEZ'); + expect(classes.single.teacher, 'PROFESOR SIN CORREO'); + expect(classes.single.teacherEmail, isNull); + }, + ); + + test('parses a session with no room assigned yet', () async { + const subject = ScheduleSubject( + code: '806010M', + group: 'X2', + campusId: '02', + name: 'INGENIERÍA DE SOFTWARE', + ); + when(() => dio.post(any(), data: any(named: 'data'))).thenAnswer( + (_) async => _htmlResponse(''' + + + + + + + +
1PERIODOX210 JUE: 8:00-12:00 ,SIN ESPACIO -- MG
DOCENTE PRUEBA UNO docente.uno@correounivalle.edu.coPrograma
+'''), + ); + + final classes = await dataSource.fetchSchedule(subjects: [subject]); + + expect(classes, hasLength(1)); + final session = classes.single; + expect(session.day, Weekday.thursday); + expect(session.startTime, '08:00'); + expect(session.endTime, '12:00'); + expect(session.building, ''); + expect(session.room, ''); + expect(session.campus, ''); + }); + }); +} diff --git a/test/features/student_grades/data/datasources/sira_grades_remote_datasource_test.dart b/test/features/student_grades/data/datasources/sira_grades_remote_datasource_test.dart new file mode 100644 index 0000000..5867d23 --- /dev/null +++ b/test/features/student_grades/data/datasources/sira_grades_remote_datasource_test.dart @@ -0,0 +1,170 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:univalle_app/core/error/exceptions.dart'; +import 'package:univalle_app/core/constants/sira_constants.dart'; +import 'package:univalle_app/features/student_grades/data/datasources/sira_grades_remote_datasource.dart'; +import 'package:univalle_app/features/student_grades/student_grades_strings.dart'; + +class MockDio extends Mock implements Dio {} + +// The academic-record flow is a two-step SIRA form dance: a first POST +// returns a page whose hidden
inputs must be echoed back verbatim in +// a second POST to actually generate the report. Only the report's content +// is parsing-relevant, so the intermediate form page is a small synthetic +// stub rather than a captured fixture. +const _formPageHtml = ''' + + + +
+ +'''; + +Response> _htmlResponse(String html, {int statusCode = 200}) => + Response( + requestOptions: RequestOptions(path: SiraConstants.academicRecordPath), + statusCode: statusCode, + data: latin1.encode(html), + ); + +void main() { + late MockDio dio; + late SiraGradesRemoteDataSource dataSource; + late String reportHtml; + + setUpAll(() { + registerFallbackValue({}); + reportHtml = File( + 'test/fixtures/student_grades/academic_record_report.html', + ).readAsStringSync(); + }); + + setUp(() { + dio = MockDio(); + dataSource = SiraGradesRemoteDataSource(dio); + }); + + void stubReport({int statusCode = 200}) { + when(() => dio.post(any(), data: any(named: 'data'))) + .thenAnswer((invocation) async { + final data = invocation.namedArguments[#data]; + if (data is Map && data['accion'] == 'Generar Carpeta') { + return _htmlResponse(reportHtml, statusCode: statusCode); + } + return _htmlResponse(_formPageHtml); + }); + } + + group('fetchGrades', () { + test('parses every period from a real SIRA academic record', () async { + stubReport(); + + final grades = await dataSource.fetchGrades(username: '0000000-3743'); + + expect(grades, hasLength(10)); + }); + + test('parses a completed period\'s summary and subjects', () async { + stubReport(); + + final grades = await dataSource.fetchGrades(username: '0000000-3743'); + + final first = grades.first; + expect(first.period, 'FEBRERO/2022 - JUNIO/2022'); + expect(first.average, 4.5); + expect(first.credits, 18); + expect(first.approvedPercentage, '100%'); + expect(first.hasAcademicMerit, isFalse); + + final subject = first.subjects.firstWhere((s) => s.code == '111007M'); + expect(subject.group, '50'); + expect(subject.campusId, '06'); + expect(subject.name, 'MATEMÁTICA FUNDAMENTAL'); + expect(subject.credits, 3); + expect(subject.grade, '4.9'); + expect(subject.isCanceled, isFalse); + }); + + test('flags a period with an academic merit distinction', () async { + stubReport(); + + final grades = await dataSource.fetchGrades(username: '0000000-3743'); + + expect(grades[1].period, 'AGOSTO/2022 - DICIEMBRE/2022'); + expect(grades[1].hasAcademicMerit, isTrue); + }); + + test('marks a subject canceled mid-semester with a blank grade', () async { + stubReport(); + + final grades = await dataSource.fetchGrades(username: '0000000-3743'); + + final canceledPeriod = grades[5]; + expect(canceledPeriod.period, 'AGOSTO/2024 - DICIEMBRE/2024'); + + final canceled = canceledPeriod.subjects.firstWhere( + (s) => s.code == '750023C' && s.group == '51', + ); + expect(canceled.grade, ''); + expect(canceled.isCanceled, isTrue); + }); + + test( + 'parses the in-progress current period with no final average', + () async { + stubReport(); + + final grades = await dataSource.fetchGrades(username: '0000000-3743'); + + final current = grades.last; + expect(current.period, 'AGOSTO/2026 - DICIEMBRE/2026'); + expect(current.average, 0); + // SIRA renders this cell without a trailing '%' for the still-open + // in-progress period, unlike completed periods (e.g. "100%"). + expect(current.approvedPercentage, '0'); + + final inProgress = current.subjects.firstWhere( + (s) => s.code == '750039C', + ); + expect(inProgress.name, 'TRABAJO DE GRADO II'); + expect(inProgress.grade, ''); + expect(inProgress.isCanceled, isFalse); + }, + ); + + test('throws when the report request does not return 200', () async { + stubReport(statusCode: 500); + + expect( + () => dataSource.fetchGrades(username: '0000000-3743'), + throwsA( + isA().having( + (e) => e.message, + 'message', + StudentGradesStrings.gradesUnavailable, + ), + ), + ); + }); + + test('maps a DioException to an AppException', () async { + when(() => dio.post(any(), data: any(named: 'data'))).thenThrow( + DioException( + requestOptions: RequestOptions( + path: SiraConstants.academicRecordPath, + ), + type: DioExceptionType.connectionTimeout, + ), + ); + + expect( + () => dataSource.fetchGrades(username: '0000000-3743'), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/features/student_tabulate/data/datasources/sira_tabulate_remote_datasource_test.dart b/test/features/student_tabulate/data/datasources/sira_tabulate_remote_datasource_test.dart new file mode 100644 index 0000000..f4f3876 --- /dev/null +++ b/test/features/student_tabulate/data/datasources/sira_tabulate_remote_datasource_test.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:univalle_app/core/constants/sira_constants.dart'; +import 'package:univalle_app/core/error/exceptions.dart'; +import 'package:univalle_app/features/student_tabulate/data/datasources/sira_tabulate_remote_datasource.dart'; + +class MockDio extends Mock implements Dio {} + +Response> _htmlResponse(String html) => Response( + requestOptions: RequestOptions(path: SiraConstants.tabulatedPath), + statusCode: 200, + data: latin1.encode(html), +); + +void main() { + late MockDio dio; + late SiraTabulateRemoteDataSource dataSource; + late String reportHtml; + + setUpAll(() { + registerFallbackValue({}); + reportHtml = File('test/fixtures/student_tabulate/tabulate_report.html') + .readAsStringSync(); + }); + + setUp(() { + dio = MockDio(); + dataSource = SiraTabulateRemoteDataSource(dio); + }); + + group('fetchTabulate', () { + test( + 'returns a mobile-ready document scaled from a real tabulado', + () async { + when(() => dio.post(any(), data: any(named: 'data'))) + .thenAnswer((_) async => _htmlResponse(reportHtml)); + + final result = await dataSource.fetchTabulate(username: '0000000-3743'); + + expect(result, contains('id="mobileWrapper"')); + expect(result, contains('name="viewport"')); + // The real fixture's frame table is 765px wide. + expect(result, contains('calc(100vw / 765px)')); + }, + ); + + test('strips dead interactive elements and HTML comments', () async { + when(() => dio.post(any(), data: any(named: 'data'))) + .thenAnswer((_) async => _htmlResponse(reportHtml)); + + final result = await dataSource.fetchTabulate(username: '0000000-3743'); + + expect(result, isNot(contains(' dio.post(any(), data: any(named: 'data'))) + .thenAnswer((_) async => _htmlResponse(reportHtml)); + + final result = await dataSource.fetchTabulate(username: '0000000-3743'); + + expect(result, isNot(contains('oncontextmenu'))); + expect(result, isNot(contains('leftmargin'))); + }); + + test( + 'throws when the response has no univalle logo (unavailable)', + () async { + when(() => dio.post(any(), data: any(named: 'data'))).thenAnswer( + (_) async => _htmlResponse('No tabulado'), + ); + + expect( + () => dataSource.fetchTabulate(username: '0000000-3743'), + throwsA(isA()), + ); + }, + ); + + test('maps a DioException to an AppException', () async { + when(() => dio.post(any(), data: any(named: 'data'))).thenThrow( + DioException( + requestOptions: RequestOptions(path: SiraConstants.tabulatedPath), + type: DioExceptionType.connectionTimeout, + ), + ); + + expect( + () => dataSource.fetchTabulate(username: '0000000-3743'), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/features/teaching_rating/data/datasources/course_evaluation_remote_datasource_test.dart b/test/features/teaching_rating/data/datasources/course_evaluation_remote_datasource_test.dart new file mode 100644 index 0000000..7620ef3 --- /dev/null +++ b/test/features/teaching_rating/data/datasources/course_evaluation_remote_datasource_test.dart @@ -0,0 +1,431 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cookie_jar/cookie_jar.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:univalle_app/core/constants/course_evaluation_constants.dart'; +import 'package:univalle_app/core/error/exceptions.dart'; +import 'package:univalle_app/features/teaching_rating/data/datasources/course_evaluation_remote_datasource.dart'; +import 'package:univalle_app/features/teaching_rating/domain/entities/rating_option.dart'; +import 'package:univalle_app/features/teaching_rating/domain/entities/review_question.dart'; +import 'package:univalle_app/features/teaching_rating/domain/entities/teacher_review.dart'; +import 'package:univalle_app/features/teaching_rating/domain/entities/teacher_to_rate.dart'; +import 'package:univalle_app/features/teaching_rating/teaching_rating_strings.dart'; + +class MockDio extends Mock implements Dio {} + +Response> _htmlResponse(String html, {int statusCode = 200}) => + Response( + requestOptions: RequestOptions(path: CourseEvaluationConstants.homePath), + statusCode: statusCode, + data: latin1.encode(html), + ); + +const _teacher = TeacherToRate( + id: '202608041', + teacherName: 'DOCENTE PRUEBA DOS', + subjectName: 'COMPRENSIÓN Y PRODUCCIÓN DE TEXTOS ACADÉMICOS GENERALES', + subjectCode: '204133C', + group: '51', + campusId: '06', + teacherId: '1000000002', + teacherDocument: '0000000001', + programId: '1000000004', + programName: 'TECNOLOGÍA EN DESARROLLO DE SOFTWARE', + programCode: '2724', + isQualified: false, +); + +void main() { + late MockDio dio; + late CookieJar cookieJar; + late CourseEvaluationRemoteDataSource dataSource; + late String teachersHtml; + late String questionsHtml; + late String notConfiguredHtml; + + setUpAll(() { + registerFallbackValue({}); + registerFallbackValue(Options()); + teachersHtml = File('test/fixtures/teaching_rating/teachers_to_rate.html') + .readAsStringSync(); + questionsHtml = File( + 'test/fixtures/teaching_rating/teacher_review_questions.html', + ).readAsStringSync(); + notConfiguredHtml = File( + 'test/fixtures/teaching_rating/teacher_review_not_configured.html', + ).readAsStringSync(); + }); + + setUp(() { + dio = MockDio(); + cookieJar = CookieJar(); + dataSource = CourseEvaluationRemoteDataSource(dio, cookieJar); + }); + + void stubLogin({int statusCode = 302}) { + when( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer((_) async => _htmlResponse('', statusCode: statusCode)); + } + + group('fetchTeachersToRate', () { + test('parses the pending teacher from a real assignment list', () async { + stubLogin(); + when(() => dio.get(any())) + .thenAnswer((_) async => _htmlResponse(teachersHtml)); + + final teachers = await dataSource.fetchTeachersToRate( + username: 'jperez-3743', + password: 'secret', + ); + + expect(teachers, hasLength(1)); + final teacher = teachers.single; + expect(teacher.teacherName, 'DOCENTE PRUEBA UNO'); + expect(teacher.subjectName, 'TRABAJO DE GRADO II'); + expect(teacher.subjectCode, '750039C'); + expect(teacher.group, '54'); + expect(teacher.campusId, '06'); + expect(teacher.teacherDocument, '0000000000'); + expect(teacher.programCode, '3743'); + expect(teacher.isQualified, isFalse); + expect(teacher.novelty, isNull); + }); + + test( + 'flags an already-qualified teacher and carries a novelty reason', + () async { + stubLogin(); + when(() => dio.get(any())).thenAnswer( + (_) async => _htmlResponse(''' + +
+ DOCENTE CALIFICADO + + + + + +
+
+ DOCENTE CON NOVEDAD + + + + + +
+ +'''), + ); + + final teachers = await dataSource.fetchTeachersToRate( + username: 'jperez-3743', + password: 'secret', + ); + + expect(teachers, hasLength(2)); + final qualified = teachers.firstWhere((t) => t.id == '111'); + expect(qualified.isQualified, isTrue); + expect(qualified.novelty, isNull); + + final withNovelty = teachers.firstWhere((t) => t.id == '222'); + expect(withNovelty.isQualified, isFalse); + expect(withNovelty.novelty, 'Periodo de evaluación cerrado'); + }, + ); + + test( + 'accepts redirects and rejects hard failures as valid status', + () async { + stubLogin(); + when(() => dio.get(any())) + .thenAnswer((_) async => _htmlResponse(teachersHtml)); + + await dataSource.fetchTeachersToRate( + username: 'jperez-3743', + password: 'secret', + ); + + final options = + verify( + () => dio.post( + any(), + data: any(named: 'data'), + options: captureAny(named: 'options'), + ), + ).captured.single + as Options; + expect(options.validateStatus!(200), isTrue); + expect(options.validateStatus!(399), isTrue); + expect(options.validateStatus!(400), isFalse); + expect(options.validateStatus!(null), isFalse); + }, + ); + + test('parses the real alert() message from a rejected login', () async { + stubLogin(statusCode: 200); + when( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer( + (_) async => _htmlResponse( + "", + ), + ); + + expect( + () => dataSource.fetchTeachersToRate( + username: 'jperez-3743', + password: 'wrong', + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Usuario o clave incorrectos', + ), + ), + ); + }); + + test('maps a DioException to an AppException', () async { + when( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenThrow( + DioException( + requestOptions: RequestOptions( + path: CourseEvaluationConstants.homePath, + ), + type: DioExceptionType.connectionTimeout, + ), + ); + + expect( + () => dataSource.fetchTeachersToRate( + username: 'jperez-3743', + password: 'secret', + ), + throwsA(isA()), + ); + }); + + test( + 'throws when the assignment list request does not return 200', + () async { + stubLogin(); + when( + () => dio.get(any()), + ).thenAnswer((_) async => _htmlResponse(teachersHtml, statusCode: 500)); + + expect( + () => dataSource.fetchTeachersToRate( + username: 'jperez-3743', + password: 'secret', + ), + throwsA(isA()), + ); + }, + ); + + test('throws AuthException when SIRA rejects the credentials', () async { + stubLogin(statusCode: 200); + when(() => dio.get(any())).thenAnswer((_) async => _htmlResponse('')); + + // No alert() script in the (empty) response body, so the fallback + // message is used. + expect( + () => dataSource.fetchTeachersToRate( + username: 'jperez-3743', + password: 'wrong', + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + TeachingRatingStrings.unavailable, + ), + ), + ); + }); + }); + + group('fetchTeacherReview', () { + test( + 'parses all 24 questions and the 21 replay fields from a real survey', + () async { + when( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer((_) async => _htmlResponse(questionsHtml)); + + final review = await dataSource.fetchTeacherReview(teacher: _teacher); + + expect(review.questions, hasLength(24)); + expect(review.formFields, hasLength(21)); + expect( + review.questions + .where((q) => q.category == QuestionCategory.subject) + .length, + 4, + ); + expect( + review.questions + .where((q) => q.category == QuestionCategory.teacher) + .length, + 11, + ); + expect( + review.questions + .where((q) => q.category == QuestionCategory.student) + .length, + 9, + ); + + final first = review.questions.first; + expect(first.id, '264'); + expect(first.category, QuestionCategory.subject); + expect( + first.question, + contains('se presentaron los objetivos o aprendizajes esperados'), + ); + }, + ); + + test( + 'throws a non-retryable BusinessException when no survey is configured', + () async { + when( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer((_) async => _htmlResponse(notConfiguredHtml)); + + expect( + () => dataSource.fetchTeacherReview(teacher: _teacher), + throwsA( + isA() + .having( + (e) => e.message, + 'message', + 'Tipo encuesta: NO DEFINIDO (99)', + ) + .having((e) => e.retryable, 'retryable', isFalse), + ), + ); + }, + ); + + test('throws when the review request does not return 200', () async { + when( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer((_) async => _htmlResponse(questionsHtml, statusCode: 500)); + + expect( + () => dataSource.fetchTeacherReview(teacher: _teacher), + throwsA(isA()), + ); + }); + }); + + group('submitTeacherReview', () { + final review = TeacherReview( + formFields: const {'ase_maa_per_codigo': '1000000004'}, + questions: const [ + ReviewQuestion( + id: '264', + category: QuestionCategory.subject, + question: '¿...?', + ), + ], + teacherName: _teacher.teacherName, + subjectName: _teacher.subjectName, + ); + + test( + 'completes when every question is answered and SIRA accepts it', + () async { + when( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer((_) async => _htmlResponse('', statusCode: 200)); + + await expectLater( + dataSource.submitTeacherReview( + review: review, + answers: {'264': RatingOption.agree}, + ), + completes, + ); + }, + ); + + test( + 'throws BusinessException without a request for a missing answer', + () async { + await expectLater( + () => + dataSource.submitTeacherReview(review: review, answers: const {}), + throwsA( + isA().having( + (e) => e.message, + 'message', + TeachingRatingStrings.unansweredQuestion(1), + ), + ), + ); + verifyNever( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ); + }, + ); + + test('throws when the submit request does not return 200', () async { + when( + () => dio.post( + any(), + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer((_) async => _htmlResponse('', statusCode: 500)); + + expect( + () => dataSource.submitTeacherReview( + review: review, + answers: {'264': RatingOption.agree}, + ), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/fixtures/auth/login_error.html b/test/fixtures/auth/login_error.html new file mode 100644 index 0000000..b8dc2a9 --- /dev/null +++ b/test/fixtures/auth/login_error.html @@ -0,0 +1,116 @@ + + + + + + +
+ + + + + + + +
+ + + + + + +
+ + + Ingresar al sistema + + +       + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+   AVISO 301 Autenticación fallada: Verifique su LOGIN y CONTRASEÑA. +
+ + + +
+ + +
+ +
+

+Si usted es ESTUDIANTE, recuerde imprimir su tabulado cada vez que realice procesos de adición/cancelación de asignaturas. Este documento podría ser de utilidad para cualquier reclamación. +

+
+
+
+
Login :
Contraseña :
+ +
+
+ +    + +

+
+ + + + + +
+ + +
Ver Guía
+
+
+ Cambiar Contraseña       + Olvidé mi contraseña +
+
+
+

+
+ + + diff --git a/test/fixtures/auth/reset_password_form.html b/test/fixtures/auth/reset_password_form.html new file mode 100644 index 0000000..7566de0 --- /dev/null +++ b/test/fixtures/auth/reset_password_form.html @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + +
+
+
+
+
+
+ +
+
+
+
+

Solicitud de nueva contraseña para acceso a Sistemas de Información Institucionales

+
+ +
    +
  • Sistema de Registro Académico y Admisiones - SIRA (matrículas, calificaciones, etc.)
  • +
  • Parcial de Proficiencia
  • +
  • Solo Cali: Sistema de Matricula Financiera - MatFin
  • +
+
+
+
+
+
+
+ +
+ + +
+ +
+
+
+

+ ¿Cuál es mi login?
+
+ Estudiante:
+ Código de estudiante - Código de programa académico
+ Ejemplo: 1144601-3550 +

+ Docente:
+ Si es docente, digite su número de identificación. +

+ SICOP:
+ Si requiere ingresar al sistema SICOP, digite su usuario. +



+ +
+
+
+
+
+ +

Si es estudiante y no se reconoce la información registrada, contactese con la oficina de Registro Académico

+
+
+
+
+ +
Universidad del Valle
Cali, Colombia
+ +
+ + diff --git a/test/fixtures/auth/reset_password_success.html b/test/fixtures/auth/reset_password_success.html new file mode 100644 index 0000000..91e2353 --- /dev/null +++ b/test/fixtures/auth/reset_password_success.html @@ -0,0 +1,48 @@ + + + + + + + + + + + +
+
+
+
+
+
+ +
+
+
+ +

+ Contraseñas_Univalle le ha enviado un mensaje con el asunto “Enlace para asignación de contraseña” a sus cuentas de correo electrónico
+ usu***ario@correounivalle.edu.co
+
+
+Por favor ingrese a su correo, abra el mensaje y siga las indicaciones que lo llevarán al siguiente paso. +

+
+
+ Si después de hacer la solicitud no le llega inmediatamente el mensaje: +
    +
  1. Espere entre 5 y 15 minutos. +
  2. Verifique si llegó a alguna otra carpeta de sus filtros.
  3. +
  4. Repita el procedimiento de solicitud de contraseña
  5. +
+
+
+ +
+
+ +
Universidad del Valle
Cali, Colombia
+ +
+ + diff --git a/test/fixtures/library/opac_account.html b/test/fixtures/library/opac_account.html new file mode 100644 index 0000000..5fc25ff --- /dev/null +++ b/test/fixtures/library/opac_account.html @@ -0,0 +1,176 @@ + +
 
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Apellido(s): + ESTUDIANTE +
Nombre(s): + PRUEBA +
Código Barras: + 0000000 +
Fecha Expiración: + 10-Feb-2027 +
Localización: + + Biblioteca Sede Tulua +
+
+
Multa Actual($): + 0.00 +
Departamento: + + INGENIER ÍA DE SISTEMAS - P3743 +
+ TECNOLOG ÍA EN SISTEMAS DE INFORMACI ÓN - PLAN 2711 +
+
+
Curso: + +
+
+
+
+

 

+
+
+
+

Préstamos

+
+
+

+ Ejemplares que actuamente tiene en préstamo:- +

+
+
+ No tiene ejemplares en préstamo. +
+
+
+
+

Reservas

+
+
+

+ Las siguientes reservaciones han sido registradas:- +

+
+
Usted no ha registrado ninguna reserva.
+
+
+
+

Histórico Préstamos

+
+
+

+ Historia de sus transacciones. Nota: la más reciente se muestra al inicio:- +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Código de BarrasTítuloLocalizaciónTipo de TransacciónFecha de Transacción
0579289Ingenier ía de softwareBiblioteca Sede TuluaPréstamo del Libro al Usuario20-Mar-2026 16:53
0593307Prec álculoBiblioteca Sede TuluaPréstamo del Libro al Usuario06-Oct-2025 10:00
0370682Compiladores e int érpretesBiblioteca Sede TuluaPréstamo del Libro al Usuario08-Sep-2025 07:42
0403970Essentials of programming languagesBiblioteca Sede TuluaPréstamo del Libro al Usuario08-Sep-2025 07:41
+
+
+
+
+
+
    +
  • + Préstamos +
  • +
  • + Reservas +
  • +
  • + Histórico Préstamos +
  • +
+
+
 
+
+
+
+
+ + + diff --git a/test/fixtures/library/opac_session.html b/test/fixtures/library/opac_session.html new file mode 100644 index 0000000..bd4efb6 --- /dev/null +++ b/test/fixtures/library/opac_session.html @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ +
+ + + +
+ + +
+ +



+ +

+ + +
+ +
+
+
+
+ + +
+ + + + + + + + + + + + + + + + diff --git a/test/fixtures/profile/contact_info.html b/test/fixtures/profile/contact_info.html new file mode 100644 index 0000000..0ea867d --- /dev/null +++ b/test/fixtures/profile/contact_info.html @@ -0,0 +1,675 @@ + + + + + + + + + Sistema de Registro Académico y Admisiones >> 000000000-3743 - estudiante + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+    + Sistema de Registro Académico y Admisiones, Tulua + + +
+ +   000000000-3743 + + + + + +
+ + +
+ + + + + + + + +
+ + + + + + + + + + +
+   +
+ + + + + + + + + + + + + + + + + + + +
Panel de Control 
Panel de control
 
+ +
+ + +
+ +

      +   Manejo de Persona + + + + + +
+ + + + + +
+ + + + + + +
CONSULTAR PERSONA 
+
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + +
+ +
+ + + +
+ + + + + + + + + + +
 Copyright © 2003-2011
Universidad del Valle
 Desarrollado por
Net-Centric y OITEL
+ + + + + + + diff --git a/test/fixtures/resolution/curriculum.html b/test/fixtures/resolution/curriculum.html new file mode 100644 index 0000000..66dfdd1 --- /dev/null +++ b/test/fixtures/resolution/curriculum.html @@ -0,0 +1,826 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sistema de Registro Académico y Admisiones >> 000000000-3743 - estudiante + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+    + Sistema de Registro Académico y Admisiones, Tulua + + +
+ +   000000000-3743 + + + + + +
+ + +


      +   Manejo de Resoluciones de los Programas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ LISTADO DE ASIGNATURAS Y PRERREQUISITOS, RESOLUCIÓN:
+ + 047 -> 2020 -> 3743, INGENIERÍA DE SISTEMAS + +
+ + + +
+
# Semestre Código Nombre Asignatura Tipo de Asignatura Créditos Código Prerrequisito Nombre Prerrequisito Tipo de Prerrequisito
1 01 111023C MATEMÁTICAS BÁSICAS AB 3      
2 01 404002C DEPORTE Y SALUD AB 2      
3 01 701001C INSERCIÓN A LA VIDA UNIVERSITARIA AB 2      
4 01 701002C TALLER DE INGENIERÍA I AB 3      
5 01 750012C FUNDAMENTOS DE PROGRAMACIÓN IMPERATIVA AB 3      
6 01 750046C INTRODUCCIÓN A LA INGENIERÍA - SISTEMAS AB 2      
7 02 111021C CÁLCULO MONOVARIABLE AB 3 111023C MATEMÁTICAS BÁSICAS PA
8 02 111038C ÁLGEBRA LINEAL. AB 3 111023C MATEMÁTICAS BÁSICAS PA
9 02 204133C COMPRENSIÓN Y PRODUCCIÓN DE TEXTOS ACADÉMICOS GENERALES AB 2      
10 02 750004C MATEMÁTICAS DISCRETAS I AB 4 750012C FUNDAMENTOS DE PROGRAMACIÓN IMPERATIVA PA
11 02 750015C FUNDAMENTOS DE PROGRAMACIÓN ORIENTADA A OBJETOS AB 3 701002C TALLER DE INGENIERÍA I PA
  02 750015C FUNDAMENTOS DE PROGRAMACIÓN ORIENTADA A OBJETOS AB 3 750012C FUNDAMENTOS DE PROGRAMACIÓN IMPERATIVA PA
12 03 106012C FÍSICA I + LABORATORIO AB 4 111021C CÁLCULO MONOVARIABLE PA
13 03 111022C CÁLCULO MULTIVARIABLE AB 3 111021C CÁLCULO MONOVARIABLE PA
14 03 750005C MATEMÁTICAS DISCRETAS II AB 4 750004C MATEMÁTICAS DISCRETAS I PA
15 03 750014C FUNDAMENTOS DE PROGRAMACIÓN ORIENTADA A EVENTOS AP 3 750015C FUNDAMENTOS DE PROGRAMACIÓN ORIENTADA A OBJETOS PA
16 04 111026C ECUACIONES DIFERENCIALES AB 3 111022C CÁLCULO MULTIVARIABLE PA
17 04 750001C SISTEMAS OPERATIVOS AP 4 750015C FUNDAMENTOS DE PROGRAMACIÓN ORIENTADA A OBJETOS PA
18 04 750006C BASES DE DATOS AP 4 750015C FUNDAMENTOS DE PROGRAMACIÓN ORIENTADA A OBJETOS PA
19 04 750013C FUNDAMENTOS DE PROGRAMACIÓN FUNCIONAL Y CONCURRENTE AP 3 750014C FUNDAMENTOS DE PROGRAMACIÓN ORIENTADA A EVENTOS PA
20 04 761001C PROBABILIDAD Y ESTADÍSTICA AB 3 111021C CÁLCULO MONOVARIABLE PA
21 05 750008C ANÁLISIS Y DISEÑO DE ALGORITMOS I AB 3 750005C MATEMÁTICAS DISCRETAS II PA
  05 750008C ANÁLISIS Y DISEÑO DE ALGORITMOS I AB 3 750014C FUNDAMENTOS DE PROGRAMACIÓN ORIENTADA A EVENTOS PA
22 05 750009C DESARROLLO DE SOFTWARE I AB 3 750006C BASES DE DATOS PA
23 05 750010C FUNDAMENTOS DE REDES AB 3 750001C SISTEMAS OPERATIVOS PA
24 05 750016C SIMULACIÓN Y COMPUTACIÓN NUMÉRICA AB 3 106012C FÍSICA I + LABORATORIO PA
  05 750016C SIMULACIÓN Y COMPUTACIÓN NUMÉRICA AB 3 111026C ECUACIONES DIFERENCIALES PA
  05 750016C SIMULACIÓN Y COMPUTACIÓN NUMÉRICA AB 3 111048M ÁLGEBRA LINEAL PA
  05 750016C SIMULACIÓN Y COMPUTACIÓN NUMÉRICA AB 3 761001C PROBABILIDAD Y ESTADÍSTICA PA
25 05 750017C FUNDAMENTOS DE INTERPRETACIÓN Y COMPILACIÓN DE LENGUAJES DE PROGRAMACIÓN AB 3 750013C FUNDAMENTOS DE PROGRAMACIÓN FUNCIONAL Y CONCURRENTE PA
26 05 760016C INGENIERÍA ECONÓMICA AB 3 111022C CÁLCULO MULTIVARIABLE PA
27 06 750018C PROYECTO INTEGRADOR I AP 4 750009C DESARROLLO DE SOFTWARE I PA
28 06 750020C ANÁLISIS Y DISEÑO DE ALGORITMOS II AP 3 750008C ANÁLISIS Y DISEÑO DE ALGORITMOS I PA
  06 750020C ANÁLISIS Y DISEÑO DE ALGORITMOS II AP 3 750017C FUNDAMENTOS DE INTERPRETACIÓN Y COMPILACIÓN DE LENGUAJES DE PROGRAMACIÓN PA
29 06 750021C DESARROLLO DE SOFTWARE II AP 3 750009C DESARROLLO DE SOFTWARE I PA
30 06 750022C INTELIGENCIA ARTIFICIAL AP 4 750013C FUNDAMENTOS DE PROGRAMACIÓN FUNCIONAL Y CONCURRENTE PA
  06 750022C INTELIGENCIA ARTIFICIAL AP 4 761001C PROBABILIDAD Y ESTADÍSTICA PA
31 06 750023C INFRAESTRUCTURAS PARALELAS Y DISTRIBUIDAS AP 3 750010C FUNDAMENTOS DE REDES PA
32 07 730026C SEMINARIO EN CONSTITUCIÓN, LEGISLACIÓN Y ÉTICA DE LA PROFESIÓN AB 2      
33 07 750024C CIBERSEGURIDAD AP 3 750010C FUNDAMENTOS DE REDES PA
  07 750024C CIBERSEGURIDAD AP 3 750016C SIMULACIÓN Y COMPUTACIÓN NUMÉRICA PA
34 07 750025C INTRODUCCIÓN A LA CIENCIA DE LOS DATOS AP 3 750006C BASES DE DATOS PA
  07 750025C INTRODUCCIÓN A LA CIENCIA DE LOS DATOS AP 3 750022C INTELIGENCIA ARTIFICIAL PA
35 07 750026C DESARROLLO DE APLICACIONES PARA DISPOSITIVOS MÓVILES AP 3 750021C DESARROLLO DE SOFTWARE II PA
36 07 750027C DESARROLLO DE SOFTWARE III AP 3 750021C DESARROLLO DE SOFTWARE II PA
37 08 730078C IMPACTOS AMBIENTALES AP 3      
38 08 750031C INTRODUCCIÓN A LA GESTIÓN DE PROYECTOS DE SOFTWARE AB 3 750021C DESARROLLO DE SOFTWARE II PA
39 08 750036C SEMINARIO DE TRABAJO DE GRADO AP 2 750020C ANÁLISIS Y DISEÑO DE ALGORITMOS II PA
40 08 750037C PROYECTO INTEGRADOR II AP 4 750018C PROYECTO INTEGRADOR I PA
  08 750037C PROYECTO INTEGRADOR II AP 4 750025C INTRODUCCIÓN A LA CIENCIA DE LOS DATOS PA
  08 750037C PROYECTO INTEGRADOR II AP 4 750027C DESARROLLO DE SOFTWARE III PA
41 09 750032C INNOVACIÓN Y EMPRENDIMIENTO EMPRESARIAL AP 3 750031C INTRODUCCIÓN A LA GESTIÓN DE PROYECTOS DE SOFTWARE PA
42 09 750038C TRABAJO DE GRADO I AP 2 750036C SEMINARIO DE TRABAJO DE GRADO PA
43 09 750040C DISEÑO DE CONTENIDO PARA INTERFACES DE USUARIO AP 3      
44 10 750039C TRABAJO DE GRADO II AP 6 750038C TRABAJO DE GRADO I PA
+ +
ASIGNATURAS MARCADAS COMO TRABAJO DE GRADO:
750039C -> TRABAJO DE GRADO II
+ +
ASIGNATURAS PRÁCTICAS:

+ + + + + + + + +
 Copyright © 2003-2011
Universidad del Valle
 Desarrollado por
Net-Centric y OITEL
+ + +
+ + + diff --git a/test/fixtures/schedule/subject_schedule.html b/test/fixtures/schedule/subject_schedule.html new file mode 100644 index 0000000..f422ba5 --- /dev/null +++ b/test/fixtures/schedule/subject_schedule.html @@ -0,0 +1,243 @@ + + + Sistema de Registro Acadmico y Admisiones >> Programacin Acadmica + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +    + + 204355M -> INGLÉS CON FINES GENERALES Y ACADÉM. I + + + +    + + Crd. 3 + + + +    + + Sede: 00 + +
# Periodo Acadmico Grupo/Franja Cupo Otorgado Horario Asignatura Docente Responsable Programada para el Programa Acadmico: Convertir a Genrico Exclusivo para Repitentes
1 AGOSTO/2026 - DICIEMBRE/202601 13 MIE: 14:00-16:00 , Edf. E23 (E23) -> 1020 -- MG -- MELENDEZ
LUN: 14:00-16:00 , Edf. E26 (E26) -> 1017 -- MG -- MELENDEZ
DOCENTE PRUEBA UNO

docente.uno@correounivalle.edu.co
Cupo Genrico
  + AGOSTO/2026 - DICIEMBRE/202601 5

docente.uno@correounivalle.edu.co
3249-00-DIU SI
  + AGOSTO/2026 - DICIEMBRE/202601 7

docente.uno@correounivalle.edu.co
3464-00-DIU SI
  + AGOSTO/2026 - DICIEMBRE/202601 3

docente.uno@correounivalle.edu.co
3489-00-DIU SI
  + AGOSTO/2026 - DICIEMBRE/202601 3

docente.uno@correounivalle.edu.co
3560-00-DIU SI
  + AGOSTO/2026 - DICIEMBRE/202601 4

docente.uno@correounivalle.edu.co
3752-00-DIU SI
2 AGOSTO/2026 - DICIEMBRE/202602 35 LUN: 18:00-20:00 , Edf. E23 (E23) -> 1011 -- MG -- MELENDEZ
MIE: 18:00-20:00 , Edf. E20 (E20) -> SC2 -- MG -- MELENDEZ
DOCENTE PRUEBA DOS

docente.dos@correounivalle.edu.co
Cupo Genrico
+
+ + diff --git a/test/fixtures/student_grades/academic_record_report.html b/test/fixtures/student_grades/academic_record_report.html new file mode 100644 index 0000000..0a7c6c5 --- /dev/null +++ b/test/fixtures/student_grades/academic_record_report.html @@ -0,0 +1,2028 @@ + + + + + + + + + Sistema de Registro Académico y Admisiones >> 000000000-3743 - estudiante + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+    + Sistema de Registro Académico y Admisiones, Tulua + + +
+ +   000000000-3743 + + + + + +
+ + +
+ + + + + + + + + +
+ + + + + + + + + + +
+   +
+ + + + + + + + + + + + + + + + + + + +
Panel de Control 
Panel de control
 
+ +
+ + +
+

      +   Consultar Carpeta Académica + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + +   + + + + + +
+
   + + + + + + + + + + + + +
+
+
+ + + + + + + + + +    +
+
+ +
+ + + + + +
+ + + + + Estudiante : 000000000 -- ESTUDIANTE PRUEBA +
+ Documento: C.C. 000000000 +
+ + Programa Académico : 3743-06-DIU-INGENIERÍA DE SISTEMAS +
+ Email Institucional: estudiante.prueba@correounivalle.edu.co + + +
Tipo de Ingreso: ADMISION + + + +
+ + Resolución del estudiante: (698) 047 de 2020 +     
+ Tipo de Currículo: RF -> REFORMA CURRICULAR +      +
+
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + +
+
+ + NOTA:
(1) La impresión de este documento no tiene ninguna validez de tipo + legal y no se tendrá en cuenta en futuras reclamaciones.
(2) Los + certificados oficiales son elaborados por la Oficina de Registro + Académico y Admisiones.
(3) Los promedios se calculan únicamente con la información + registrada actualmente en el sistema. +
+ +
+ +
+ Promedio Total Acumulado del estudiante: 4.38 +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ PERIODO: FEBRERO/2022 - JUNIO/2022 +
+ Fecha Matricula: 2022-02-10 00:00:00

+
CÓDIGO GR SDE ASIGNATURA TM FUN COM CRD CAL HAB CANCELACIÓN/
REACTIVACIÓN
111007M 50 06 MATEMÁTICA FUNDAMENTAL N AB MAT 3 4.9
204355M 50 06 INGLÉS CON FINES GENERALES Y ACADÉM. I N AB HUM 3 3.6
404002C 51 06 DEPORTE Y SALUD N EC SIN 2 4.4
701001C 51 06 INSERCIÓN A LA VIDA UNIVERSITARIA N AB EVS 2 4.3
701002C 52 06 TALLER DE INGENIERÍA I N AB CTE 3 4.8
701003C 51 06 INTRODUCCIÓN A LA INGENIERÍA N AB LCO 2 4.7
750012C 50 06 FUNDAMENTOS DE PROGRAMACIÓN IMPERATIVA N AB CTE 3 4.8
+ + + + + + + + + + + + + + + + + + + + +
Resumen de Finalización por periodo
Créditos Matriculados: + 18 + + Núm. Créd. Aprobados : + + 18 + + % Créd. Aprobados : + + 100% +
Créditos Acumulados: + 18 + Num. Créd. Aprobados Acumulados: + 18 + Promedio Semestral : + 4.50 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ PERIODO: AGOSTO/2022 - DICIEMBRE/2022 +
+ Fecha Matricula: 2022-08-17 00:00:00

+
+
+ + + + + + + + + + + + + + + + +
+
+ ESTíMULOS ACADÉMICO, Puesto ocupado: 3 +
CÓDIGO GR SDE ASIGNATURA TM FUN COM CRD CAL HAB CANCELACIÓN/
REACTIVACIÓN
111021C 51 06 CÁLCULO MONOVARIABLE N AB SIN 3 4.1
111048M 52 06 ÁLGEBRA LINEAL N AB MAT 3 4.9
204402M 52 06 COMP. Y PROD. TEXTOS ACADÉMIC. GENERALES N EC HUM 2 4.0
405126M 50 06 HERRAMIENTAS PARA HABLAR EN PÚBLICO N EC ART 3 4.2
750004C 51 06 MATEMÁTICAS DISCRETAS I N AB SIN 4 4.1
750015C 52 06 FUND.PROGRAMACIÓN ORIENTADA A OBJETOS N AB LCO 3 4.6
+ + + + + + + + + + + + + + + + + + + + +
Resumen de Finalización por periodo
Créditos Matriculados: + 18 + + Núm. Créd. Aprobados : + + 18 + + % Créd. Aprobados : + + 100% +
Créditos Acumulados: + 36 + Num. Créd. Aprobados Acumulados: + 36 + Promedio Semestral : + 4.32 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ PERIODO: MARZO/2023 - JULIO/2023 +
+ Fecha Matricula: 2023-02-22 00:00:00

+
CÓDIGO GR SDE ASIGNATURA TM FUN COM CRD CAL HAB CANCELACIÓN/
REACTIVACIÓN
106012C 51 06 FÍSICA I + LABORATORIO N AB SIN 4 3.0
111022C 50 06 CÁLCULO MULTIVARIABLE N AB SIN 3 4.4
202009M 50 06 TALLER DE ESCRITURA E INVESTIGACIÓN N EC HUM 3 4.2
750005C 50 06 MATEMÁTICAS DISCRETAS II N AB SIN 4 4.3
750014C 50 06 FUNDAM. PROGRAMACIÓN ORIENTADA A EVENTOS N AP SIN 3 4.9
+ + + + + + + + + + + + + + + + + + + + +
Resumen de Finalización por periodo
Créditos Matriculados: + 17 + + Núm. Créd. Aprobados : + + 17 + + % Créd. Aprobados : + + 100% +
Créditos Acumulados: + 53 + Num. Créd. Aprobados Acumulados: + 53 + Promedio Semestral : + 4.1 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ PERIODO: AGOSTO/2023 - DICIEMBRE/2023 +
+ Fecha Matricula: 2023-08-16 00:00:00

+
CÓDIGO GR SDE ASIGNATURA TM FUN COM CRD CAL HAB CANCELACIÓN/
REACTIVACIÓN
111026C 50 06 ECUACIONES DIFERENCIALES N AB SIN 3 3.6
750001C 50 06 SISTEMAS OPERATIVOS N AP SIN 4 4.7
750006C 50 06 BASES DE DATOS N AP SIN 4 3.5
750013C 50 06 FUND. DE PROG. FUNCIONAL Y CONCURRENTE N AP SIN 3 4.7
761001C 50 06 PROBABILIDAD Y ESTADÍSTICA N AB SIN 3 4.6
+ + + + + + + + + + + + + + + + + + + + +
Resumen de Finalización por periodo
Créditos Matriculados: + 17 + + Núm. Créd. Aprobados : + + 17 + + % Créd. Aprobados : + + 100% +
Créditos Acumulados: + 70 + Num. Créd. Aprobados Acumulados: + 70 + Promedio Semestral : + 4.20 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ PERIODO: FEBRERO/2024 - JUNIO/2024 +
+ Fecha Matricula: 2024-02-07 00:00:00

+
CÓDIGO GR SDE ASIGNATURA TM FUN COM CRD CAL HAB CANCELACIÓN/
REACTIVACIÓN
204026C 54 06 INGLÉS FINES GENERALES Y ACADÉMICOS II N AB LCO 2 3.0
750008C 51 06 ANÁLISIS Y DISEÑO DE ALGORITMOS I N AP SIN 3 4.4
750009C 51 06 DESARROLLO DE SOFTWARE I N AP LCO 3 4.5
750010C 51 06 FUNDAMENTOS DE REDES N AP SIN 3 4.9
750016C 51 06 SIMULACIÓN Y COMPUTACIÓN NUMÉRICA N AB SIN 3 3.9
750017C 50 06 FUNDAM.INTERPRETACIÓN Y COMPILACIÓN LP N AP SIN 3 4.8
760016C 50 06 INGENIERÍA ECONÓMICA N AB SIN 3 3.8
+ + + + + + + + + + + + + + + + + + + + +
Resumen de Finalización por periodo
Créditos Matriculados: + 20 + + Núm. Créd. Aprobados : + + 20 + + % Créd. Aprobados : + + 100% +
Créditos Acumulados: + 90 + Num. Créd. Aprobados Acumulados: + 90 + Promedio Semestral : + 4.24 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ PERIODO: AGOSTO/2024 - DICIEMBRE/2024 +
+ Fecha Matricula: 2024-08-05 00:00:00

+
CÓDIGO GR SDE ASIGNATURA TM FUN COM CRD CAL HAB CANCELACIÓN/
REACTIVACIÓN
204027C 53 06 INGLÉS CON FINES GENERALES Y ACADÉM. III N AB LCO 2 4.0
750018C 50 06 PROYECTO INTEGRADOR I N AP CTE 4 4.6
750020C 50 06 ANÁLISIS Y DISEÑO DE ALGORITMOS II N AP SIN 3 4.3
750021C 50 06 DESARROLLO DE SOFTWARE II N AP LCO 3 4.4
750022C 50 06 INTELIGENCIA ARTIFICIAL N AP SIN 4 4.5
750023C 50 06 INFRAESTRUCTUR. PARALELAS Y DISTRIBUIDAS N AP SIN 3 4.7
750023C 51 06 INFRAESTRUCTUR. PARALELAS Y DISTRIBUIDAS N AP SIN 3 CANCELACIÓN:
2024-09-02 14:10:23
+ + + + + + + + + + + + + + + + + + + + +
Resumen de Finalización por periodo
Créditos Matriculados: + 19 + + Núm. Créd. Aprobados : + + 19 + + % Créd. Aprobados : + + 100% +
Créditos Acumulados: + 109 + Num. Créd. Aprobados Acumulados: + 109 + Promedio Semestral : + 4.45 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ PERIODO: FEBRERO/2025 - JUNIO/2025 +
+ Fecha Matricula: 2025-02-06 00:00:00

+
CÓDIGO GR SDE ASIGNATURA TM FUN COM CRD CAL HAB CANCELACIÓN/
REACTIVACIÓN
204028C 53 06 INGLÉS FINES GENERALES Y ACADÉMICOS IV N AB LCO 2 4.5
730026C 51 06 SEM.CONSTITUCIÓN, LEGISLACIÓN Y ÉTICA N AB FSC 2 4.9
750024C 51 06 CIBERSEGURIDAD N AP SIN 3 4.1
750025C 51 06 INTRODUCCIÓN A LA CIENCIA DE LOS DATOS N AP SIN 3 4.2
750026C 51 06 DESARR. APLICACIONES DISPOSIT. MÓVILES N AP SIN 3 4.7
750027C 51 06 DESARROLLO DE SOFTWARE III N AP LCO 3 4.6
+ + + + + + + + + + + + + + + + + + + + +
Resumen de Finalización por periodo
Créditos Matriculados: + 16 + + Núm. Créd. Aprobados : + + 16 + + % Créd. Aprobados : + + 100% +
Créditos Acumulados: + 125 + Num. Créd. Aprobados Acumulados: + 125 + Promedio Semestral : + 4.47 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ PERIODO: AGOSTO/2025 - DICIEMBRE/2025 +
+ Fecha Matricula: 2025-08-14 00:00:00

+
CÓDIGO GR SDE ASIGNATURA TM FUN COM CRD CAL HAB CANCELACIÓN/
REACTIVACIÓN
730078C 50 06 IMPACTOS AMBIENTALES N AP FSC 3 4.0
750031C 53 06 INTROD. A LA GESTIÓN PROYECTOS SOFTWARE N AB SIN 3 4.5
750035C 51 06 PREP. CERTIFIC. CIBERSEGURIDAD INDUSTRIA N EP SIN 3 CANCELACIÓN:
2025-09-08 08:44:17
750036C 50 06 SEMINARIO DE TRABAJO DE GRADO N AP LCO 2 A.P
750037C 51 06 PROYECTO INTEGRADOR II N AP CTE 4 4.7
750128M 50 06 BASES DE DATOS NOSQL N EP TEC 3 4.4
+ + + + + + + + + + + + + + + + + + + + +
Resumen de Finalización por periodo
Créditos Matriculados: + 15 + + Núm. Créd. Aprobados : + + 15 + + % Créd. Aprobados : + + 100% +
Créditos Acumulados: + 140 + Num. Créd. Aprobados Acumulados: + 140 + Promedio Semestral : + 4.42 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ PERIODO: FEBRERO/2026 - JUNIO/2026 +
+ Fecha Matricula: 2026-02-03 00:00:00

+
CÓDIGO GR SDE ASIGNATURA TM FUN COM CRD CAL HAB CANCELACIÓN/
REACTIVACIÓN
750030C 51 06 HERRAMIENTAS PARA GESTIÓN DE PROYECTOS N EP SIN 3 4.6
750032C 50 06 INNOVACIÓN Y EMPRENDIMIENTO EMPRESARIAL N AP SIN 3 4.4
750038C 53 06 TRABAJO DE GRADO I N AP LCO 2 5.0
750040C 51 06 DISEÑO CONTENIDO INTERFACES DE USUARIO N AP LCO 3 4.9
750057M 50 06 PRÁCTICA PROFESIONAL N AE TEC 6 5.0
750108M 50 06 INT. A PROCESAMIENTO DE LENGUAJE NATURAL N EP TEC 3 CANCELACIÓN:
2026-03-02 15:21:10
+ + + + + + + + + + + + + + + + + + + + +
Resumen de Finalización por periodo
Créditos Matriculados: + 17 + + Núm. Créd. Aprobados : + + 17 + + % Créd. Aprobados : + + 100% +
Créditos Acumulados: + 157 + Num. Créd. Aprobados Acumulados: + 157 + Promedio Semestral : + 4.80 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ PERIODO: AGOSTO/2026 - DICIEMBRE/2026 +
+ Fecha Matricula: 2026-08-14 00:00:00

+
CÓDIGO GR SDE ASIGNATURA TM FUN COM CRD CAL HAB CANCELACIÓN/
REACTIVACIÓN
750039C 54 06 TRABAJO DE GRADO II N AP LCO 6
+ + + + + + + + + + + + + + + + + + + + +
Resumen de Finalización por periodo
Créditos Matriculados: + 6 + + Núm. Créd. Aprobados : + + 0 + + % Créd. Aprobados : + + 0 +
Créditos Acumulados: + 157 + Num. Créd. Aprobados Acumulados: + 157 + Promedio Semestral : + +
+
+ + + + + + + + + + +   + + + + + +
+
   + + + + + + + + + + + + +
+
+
+ + + + + + + + + +    +
+
+ + +
+ \ No newline at end of file diff --git a/test/fixtures/student_tabulate/tabulate_report.html b/test/fixtures/student_tabulate/tabulate_report.html new file mode 100644 index 0000000..d1fce68 --- /dev/null +++ b/test/fixtures/student_tabulate/tabulate_report.html @@ -0,0 +1,180 @@ + + + + + + + + Sistema de Registro Académico y Admisiones >> - + + + + + + + + +
+ + + +
+
+ + + + + +
UNIVERSIDAD DEL VALLE - VICERRECTORIA ACADÉMICA

TABULADO PROVISIONAL DE MATRÍCULA ACADÉMICA
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Santiago de Cali, 14 de Septiembre de 2026 + + Imprimir -> + + +
 Estudiante  
+   Nombre: ESTUDIANTE PRUEBA + +   Código: 0000000 +
+   Documento: C.C. 000000000 + +   Resolución: 047 + + + + + + +
+ Listado de Prerrequisitos -> + +
+ + + + +
+ +
+
+   Programa: (3743) INGENIERÍA DE SISTEMAS + +   Sede: (06) TULUA +
+   Jornada: DIU + +   Periodo Académico: AGOSTO/2026 - DICIEMBRE/2026 +
+   Categoría Estudiante: REGULAR + +   Fecha de Matrícula: 2026-08-14 00:00:00 +
+   +
+   +
 Listado de Asignaturas  
   
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CódigoGrSdeAsignaturaSedeTMFunCrdCal.Hab.Cancelación/Reactivación
750039C5406TRABAJO DE GRADO II06NAP6
+ + 2ab9fae1c112a9c7de87
 
 
+
+
+ Total Asignaturas Matriculadas: 1
+ Total Asignaturas Canceladas: 0
+ Total Créditos Matriculados: 6
+
+ + +
OBSERVACION: Los movimientos a su matrícula deben ser realizados con la asesoría del Director de Programa.


VERIFIQUE QUE SU MATRÍCULA ACADÉMICA ESTÉ CORRECTA

+
+ +
+ + + diff --git a/test/fixtures/teaching_rating/teacher_review_not_configured.html b/test/fixtures/teaching_rating/teacher_review_not_configured.html new file mode 100644 index 0000000..28277c4 --- /dev/null +++ b/test/fixtures/teaching_rating/teacher_review_not_configured.html @@ -0,0 +1,325 @@ + + + + + >> - + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+    + Sistema de Evaluaci?n Estudiantil de Cursos +  
+ +   0000000-3743   +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Cursos Matriculados +
+
+ + + + +
+
+ +
+
+
+
+ +
+ + + + + +
+ + + + + + + + + +
+ + + + + + +
INGRESAR EVALUACION +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + TULUA +
+ + ESCUELA DE INGENIER?A DE SISTEMAS Y COMPUTACI?N +
+ + AGOSTO/2026 - DICIEMBRE/2026 +
+ + + DOCENTE PRUEBA UNO +
+ + + 750039C - 54 TRABAJO DE GRADO II +
+ + NO DEFINIDO (99) +
-
+ +
+ + + + + +
+
+
+
+
+
+ +
+ + + + + +
 Copyright © 2004-2005
Universidad del Valle
Desarrollado por
OITEL
+ + +
+
+ + + diff --git a/test/fixtures/teaching_rating/teacher_review_questions.html b/test/fixtures/teaching_rating/teacher_review_questions.html new file mode 100644 index 0000000..6312a67 --- /dev/null +++ b/test/fixtures/teaching_rating/teacher_review_questions.html @@ -0,0 +1,973 @@ + + + + + >> - + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+    + Sistema de Evaluaci?n Estudiantil de Cursos +  
+ +   0000000-2724   +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Cursos Matriculados +
+
+ + + + +
+
+ +
+
+
+
+ +
+ + + + + +
+ + + + + + + + + +
+ + + + + + +
INGRESAR EVALUACION +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+ + TULUA +
+ + ESCUELA DE CIENCIAS DEL LENGUAJE +
+ + AGOSTO/2026 - DICIEMBRE/2026 +
+ + + DOCENTE PRUEBA DOS +
+ + + 204133C - 51 COMPRENSI?N Y PRODUCCI?N DE TEXTOS ACAD?MICOS GENERALES +
+ + ENCUESTA DE EVALUACI?N DE ASIGNATURAS TE?RICAS +
Esta es una encuesta para conocer la opini?n que usted tiene sobre el programa de la asignatura, el desempe?o del/la docente y su propio desempe?o en la asignatura matriculada. +Le invitamos a evaluar cada ?tem desde los tres componentes: asignatura, docente, estudiante. Usted seleccionar? dentro de la escala de evaluaci?n la opci?n que mejor refleje su opini?n frente a cada uno de los ?tems. En caso de no contar con todos los elementos para dar una opini?n puede marcar la casilla ?no aplica?. El diligenciamiento de la encuesta ser? an?nimo y sus respuestas ser?n tratadas de manera confidencial. La evaluaci?n es un compromiso que implica ?tica, responsabilidad y honestidad. +

+ +

Escala de Evaluaci?n:

+ +
+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
Totalmente en desacuerdoEn desacuerdoMedianamente de acuerdoDe acuerdoTotalmente de acuerdoNo Aplica
+
+ +
+ Evaluaci?n de la asignatura +
1 + En el programa de la asignatura se presentaron los objetivos o aprendizajes esperados que contribuyen a la formaci?n de estudiantes. + + + + + + + + + + + + +
Totalmente en desacuerdoEn desacuerdoMedianamente de acuerdoDe acuerdoTotalmente de acuerdoNo Aplica
2 + En el programa de la asignatura se plante? una metodolog?a en funci?n de los aprendizajes esperados. + + + + + + + + + + + + +
Totalmente en desacuerdoEn desacuerdoMedianamente de acuerdoDe acuerdoTotalmente de acuerdoNo Aplica
3 + En el programa de la asignatura se incluyeron fuentes bibliogr?ficas para fundamentar el desarrollo de los contenidos. + + + + + + + + + + + + +
Totalmente en desacuerdoEn desacuerdoMedianamente de acuerdoDe acuerdoTotalmente de acuerdoNo Aplica
4 + En el programa de la asignatura se definieron criterios de evaluaci?n para valorar los aprendizajes esperados. + + + + + + + + + + + + +
Totalmente en desacuerdoEn desacuerdoMedianamente de acuerdoDe acuerdoTotalmente de acuerdoNo Aplica
+
+
+ + + + + +
+
+ + + + + +
+ + + +
+
+
+
+
+
+ +
+ + + + + +
 Copyright © 2004-2005
Universidad del Valle
Desarrollado por
OITEL
+ + +
+ + + + + + diff --git a/test/fixtures/teaching_rating/teachers_to_rate.html b/test/fixtures/teaching_rating/teachers_to_rate.html new file mode 100644 index 0000000..9ef459b --- /dev/null +++ b/test/fixtures/teaching_rating/teachers_to_rate.html @@ -0,0 +1,192 @@ + + + + + >> - + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+    + Sistema de Evaluaci?n Estudiantil de Cursos +  
+ +   0000000-3743   +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Cursos Matriculados +
+
+ + + + +
+
+ +
+
+
+
+ + + + + + +
+ +

+
+

+ + Estimado estudiante,

+ + La Universidad del Valle avanza por un camino de búsqueda de la excelencia académica en el cual debe + estar comprometida toda la Comunidad Universitaria. Los procesos de evaluación y autoevaluación son + elementos fundamentales de esa búsqueda, en tanto que permiten un mejoramiento continuo en todos los + procesos y actividades académicas.

+ + Usted la oportunidad de opinar sobre el desempeño docente en las asignaturas + que ha cursado este semestre. Su participación en este proceso nos compromete a todos con una cultura de + evaluación permanente, que finalmente solo conduce a hacer las cosas mejor, en beneficio suyo y de las + generaciones venideras. + +
+
+ + + + + +
 Copyright © 2004-2005
Universidad del Valle
Desarrollado por
OITEL
+ + +
+ + + + + +