ISMIS Helper: cross-browser build, CI, PDF fixes, Chrome download fix
build-extension / build (push) Successful in 37s

Squash merge of feat/ismis-helper into prod.
This commit was merged in pull request #1.
This commit is contained in:
2026-08-18 19:13:49 +00:00
parent a778840845
commit 9cc4fa5c97
31 changed files with 1928 additions and 1414 deletions
+97
View File
@@ -0,0 +1,97 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const { parseStudyLoadTable } = require('../parsers.js');
const { parseProfilePage } = require('../parsers.js');
const { parseOfferedCourses } = require('../parsers.js');
const fixture = fs.readFileSync(path.join(__dirname, 'fixtures', 'study-load-table.html'), 'utf8');
const profileFixture = fs.readFileSync(path.join(__dirname, 'fixtures', 'profile.html'), 'utf8');
const offeredFixture = fs.readFileSync(path.join(__dirname, 'fixtures', 'offered-courses.html'), 'utf8');
test('parseStudyLoadTable extracts all course rows', () => {
const rows = parseStudyLoadTable(fixture);
assert.strictEqual(rows.length, 5);
});
test('parseStudyLoadTable maps columns correctly', () => {
const rows = parseStudyLoadTable(fixture);
const first = rows[0];
assert.strictEqual(first.group, '1');
assert.strictEqual(first.courseCode, 'CS 5105');
assert.strictEqual(first.units, '3.00');
assert.strictEqual(first.status, 'DISSOLVED');
assert.ok(first.schedule.includes('F 01:30 PM'));
});
test('parseStudyLoadTable handles empty faculty', () => {
const rows = parseStudyLoadTable(fixture);
const it4102 = rows.find(r => r.courseCode === 'IT 4102N');
assert.strictEqual(it4102.faculty, '');
});
test('parseProfilePage extracts header fields', () => {
const p = parseProfilePage(profileFixture);
assert.strictEqual(p.fullName, 'ADRIAN ALFRED CHIONG BONPIN');
assert.strictEqual(p.studentId, '22103625');
assert.strictEqual(p.yearLevel, '4');
assert.strictEqual(p.studentType, 'CONTINUING');
assert.strictEqual(p.program, 'BACHELOR OF SCIENCE IN INFORMATION TECHNOLOGY');
assert.strictEqual(p.semester, '1ST Semester 2026');
});
test('parseProfilePage matches bold class token with single quotes and extra classes', () => {
const html = `
<h3><b>BACHELOR OF SCIENCE</b></h3>
<table>
<tr><td>Full Name</td><td class='bold text-right'>JOHN DOE</td></tr>
<tr><td>Student Type</td><td class="bold">CONTINUING</td></tr>
</table>
`;
const p = parseProfilePage(html);
assert.strictEqual(p.fullName, 'JOHN DOE');
assert.strictEqual(p.studentType, 'CONTINUING');
assert.strictEqual(p.program, 'BACHELOR OF SCIENCE');
});
test('parseProfilePage degrades gracefully on empty input', () => {
const p = parseProfilePage('');
assert.deepStrictEqual(p, {});
});
test('parseOfferedCourses extracts course code and description', () => {
const courses = parseOfferedCourses(offeredFixture);
const it3206 = courses.find(c => c.courseCode === 'IT 3206');
assert.ok(it3206, 'should find IT 3206');
assert.strictEqual(it3206.description, 'INTEGRATIVE PROGRAMMING AND TECHNOLOGY');
});
test('parseOfferedCourses strips group suffix from code', () => {
const courses = parseOfferedCourses(offeredFixture);
assert.ok(courses.every(c => !/Group/.test(c.courseCode)));
const it3206n = courses.find(c => c.courseCode === 'IT 3206N');
assert.ok(it3206n, 'should find IT 3206N');
});
const { filterStudyLoadCourses, sumUnits } = require('../parsers.js');
test('filterStudyLoadCourses excludes DISSOLVED courses', () => {
const courses = [
{ courseCode: 'CS 5105', status: 'DISSOLVED', units: '3.00' },
{ courseCode: 'IT 3206', status: 'PARALLEL', units: '3.00' },
{ courseCode: 'IT 4102N', status: 'REGULAR', units: '3.00' }
];
const filtered = filterStudyLoadCourses(courses);
assert.deepStrictEqual(filtered.map((c) => c.courseCode), ['IT 3206', 'IT 4102N']);
});
test('filterStudyLoadCourses handles missing status', () => {
const filtered = filterStudyLoadCourses([{ courseCode: 'X', status: '', units: '1' }]);
assert.strictEqual(filtered.length, 1);
});
test('sumUnits computes total from course units', () => {
const courses = [{ units: '3.00' }, { units: '3.00' }, { units: '2.00' }];
assert.strictEqual(sumUnits(courses), '8.00');
});
+130
View File
@@ -0,0 +1,130 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
global.PDFLib = require('pdf-lib');
const { buildStudyLoadPdf, compactProgram, deriveDateRange, formatSchedule, formatFullName, formatTeacher, truncateToWidth } = require('../pdf.js');
const sampleData = {
header: {
fullName: 'ADRIAN ALFRED CHIONG BONPIN',
studentId: '22103625',
program: 'BACHELOR OF SCIENCE IN INFORMATION TECHNOLOGY',
yearLevel: '4',
semester: '1ST Semester 2026',
dateEnrolled: '8/5/2024',
dateRange: 'AUGUST - JANUARY'
},
courses: [
{ group: '7', courseCode: 'CIS 2101', description: 'DATA STRUCTURES AND ALGORITHMS', units: '3.00', teacher: 'CHRISTIAN MADERAZO', schedule: 'M,W 03:00 PM - 05:30 PM LB443TC', status: 'REGULAR' },
{ group: '2', courseCode: 'IT 3103N', description: 'SYSTEMS INTEGRATION AND ARCHITECTURE', units: '3.00', teacher: 'KHENT DELA PAZ', schedule: 'T,Th 01:30 PM - 03:00 PM DCISMVR3TC', status: 'REGULAR' }
],
totalUnits: '6.00'
};
function extractPdfText(bytes) {
const tmp = path.join(os.tmpdir(), 'study-load-test.pdf');
fs.writeFileSync(tmp, Buffer.from(bytes));
const script = [
'import fitz',
`doc = fitz.open('${tmp}')`,
'text = ""',
'for page in doc:',
' text += page.get_text()',
'print(text)'
].join('\n');
return execFileSync('python3', ['-c', script], { encoding: 'utf8' });
}
// PyMuPDF (fitz) is only needed for the text-extraction assertions below. It is
// installed in CI, but if it is missing locally we skip those assertions rather
// than hard-failing.
function fitzAvailable() {
try {
execFileSync('python3', ['-c', 'import fitz'], { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
test('buildStudyLoadPdf returns valid PDF bytes', async (t) => {
const bytes = await buildStudyLoadPdf(sampleData);
assert.ok(bytes instanceof Uint8Array);
const head = Buffer.from(bytes.slice(0, 5)).toString('latin1');
assert.strictEqual(head, '%PDF-');
const doc = await PDFLib.PDFDocument.load(bytes);
assert.strictEqual(doc.getPageCount(), 1);
if (!fitzAvailable()) {
t.skip('PyMuPDF (fitz) not available; skipping PDF text assertions');
return;
}
const text = extractPdfText(bytes);
assert.ok(text.includes('CIS 2101'), 'PDF text should contain course code CIS 2101');
assert.ok(text.includes("STUDENT'S STUDY LOAD"), 'PDF text should contain the title');
assert.ok(text.includes('CHRISTIAN MADERAZO'), 'PDF text should contain the teacher');
assert.ok(text.includes('BS IT'), 'PDF text should contain the compacted program');
});
test('compactProgram shortens degree names', () => {
assert.strictEqual(compactProgram('BACHELOR OF SCIENCE IN INFORMATION TECHNOLOGY'), 'BS IT');
assert.strictEqual(compactProgram('BACHELOR OF SCIENCE IN COMPUTER SCIENCE'), 'BS CS');
assert.strictEqual(compactProgram('BACHELOR OF ARTS IN POLITICAL SCIENCE'), 'AB PS');
assert.strictEqual(compactProgram('BACHELOR OF SCIENCE IN ACCOUNTANCY'), 'BS Accountancy');
assert.strictEqual(compactProgram('UNKNOWN PROGRAM'), 'UNKNOWN PROGRAM');
});
test('formatSchedule normalizes schedule text', () => {
assert.strictEqual(formatSchedule('M W 07:30 AM - 10:00 AM LB447TC TC'), 'M,W 07:30 AM - 10:00 AM LB447TC');
assert.strictEqual(formatSchedule('T Th 09:00 AM - 10:30 AM LB443TC TC'), 'T,Th 09:00 AM - 10:30 AM LB443TC');
assert.strictEqual(formatSchedule('M,W 03:00 PM - 05:30 PM LB443TC'), 'M,W 03:00 PM - 05:30 PM LB443TC');
assert.strictEqual(
formatSchedule('T 07:30 AM - 10:30 AM LB448TC TC Th 07:30 AM - 10:30 AM LB448TC'),
'T 07:30 AM - 10:30 AM LB448TC\nTh 07:30 AM - 10:30 AM LB448TC'
);
assert.strictEqual(
formatSchedule('W 01:30 PM - 04:30 PM LB448TC TC M 01:30 PM - 04:30 PM LB448TC'),
'W 01:30 PM - 04:30 PM LB448TC\nM 01:30 PM - 04:30 PM LB448TC'
);
});
test('formatFullName reorders first-last names to last-first', () => {
assert.strictEqual(formatFullName('ADRIAN ALFRED CHIONG BONPIN'), 'BONPIN, ADRIAN ALFRED CHIONG');
assert.strictEqual(formatFullName('BONPIN, ADRIAN ALFRED CHIONG'), 'BONPIN, ADRIAN ALFRED CHIONG');
assert.strictEqual(formatFullName('JOHN DOE'), 'DOE, JOHN');
assert.strictEqual(formatFullName(''), '');
});
test('formatTeacher renders names First [MiddleInitial] Last like the reference', () => {
assert.strictEqual(formatTeacher('FILLO, FR. PETER'), 'FR. PETER FILLO');
assert.strictEqual(formatTeacher('BACALSO, REUELLA J. AWAYAN'), 'REUELLA J. BACALSO');
assert.strictEqual(formatTeacher('PEPITO, GLENN BULLECER'), 'GLENN PEPITO');
assert.strictEqual(formatTeacher('MADERAZO, CHRISTIAN'), 'CHRISTIAN MADERAZO');
assert.strictEqual(formatTeacher('DELA PAZ, KHENT'), 'KHENT DELA PAZ');
assert.strictEqual(formatTeacher('CHRISTIAN MADERAZO'), 'CHRISTIAN MADERAZO');
assert.strictEqual(formatTeacher(''), '');
});
test('deriveDateRange maps semesters', () => {
assert.strictEqual(deriveDateRange('1ST Semester 2026'), 'AUGUST - JANUARY');
assert.strictEqual(deriveDateRange('2ND Semester 2026'), 'JANUARY - MAY');
assert.strictEqual(deriveDateRange('Summer 2026'), 'JUNE - JULY');
assert.strictEqual(deriveDateRange(''), '');
});
test('truncateToWidth hard-cuts text to fit within maxWidth', async () => {
const doc = await global.PDFLib.PDFDocument.create();
const font = doc.embedStandardFont(global.PDFLib.StandardFonts.Helvetica);
const longText = 'APPLICATION DEVELOPMENT AND EMERGING TECHNOLOGIES';
const tr = truncateToWidth(longText, font, 8, 100);
assert.ok(tr.length < longText.length, 'long text should be cut down');
assert.ok(longText.startsWith(tr), 'result must be a prefix of the input');
assert.ok(font.widthOfTextAtSize(tr, 8) <= 100, 'truncated line must fit maxWidth');
const short = 'DATA STRUCTURES';
assert.strictEqual(truncateToWidth(short, font, 8, 200), short);
assert.strictEqual(truncateToWidth('', font, 8, 200), '');
});