feat(v1.4.0): CSV/Excel/JSON/NextUp exports + hybrid smart year standing
- Export modules (export-csv/json/xlsx) mirroring the PDF layout, plus a Next Up import CSV matching the raycast/nextup column contract. - Hybrid Year Level: View Grades history (exact per-semester standing), falling back to units math (prospectus total - lacking), then profile. Popup adds a year-mode setting (auto/custom/off). - On-page button row now offers PDF / CSV / Excel / JSON / Next Up. - Bump version to 1.4.0. - Tag-only build workflow with an optional AMO sign step for a signed .xpi; separate ci.yml keeps tests on pushes/PRs. - Tests: year-standing parser tests + export tests (CSV quoting, JSON schema, XLSX structure); CI validates XLSX via openpyxl.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
|
||||
const {
|
||||
parseGradeHistory, yearLevelForSemester,
|
||||
parseProspectusYearUnits, parseLackingSubjects, parseLackingSubjectsUnits,
|
||||
computeYearStandingFromUnits
|
||||
} = require('../parsers.js');
|
||||
|
||||
// ---- View Grades history ----
|
||||
|
||||
test('parseGradeHistory extracts ordered semester terms', () => {
|
||||
const html = `
|
||||
<h3>FIRST SEMESTER - 2022</h3><table>...</table>
|
||||
<h3>SECOND SEMESTER - 2022</h3><table>...</table>
|
||||
<h3>FIRST SEMESTER - 2023</h3><table>...</table>
|
||||
<h3>SUMMER - 2025</h3><table>...</table>
|
||||
<h3>FIRST SEMESTER - 2026</h3><table>...</table>
|
||||
`;
|
||||
const h = parseGradeHistory(html);
|
||||
assert.deepStrictEqual(h, [
|
||||
{ semester: 'FIRST', year: 2022 },
|
||||
{ semester: 'SECOND', year: 2022 },
|
||||
{ semester: 'FIRST', year: 2023 },
|
||||
{ semester: 'SUMMER', year: 2025 },
|
||||
{ semester: 'FIRST', year: 2026 }
|
||||
]);
|
||||
});
|
||||
|
||||
test('parseGradeHistory handles empty input', () => {
|
||||
assert.deepStrictEqual(parseGradeHistory(''), []);
|
||||
});
|
||||
|
||||
test('yearLevelForSemester returns standing from history position', () => {
|
||||
const history = [
|
||||
{ semester: 'FIRST', year: 2022 },
|
||||
{ semester: 'SECOND', year: 2022 },
|
||||
{ semester: 'FIRST', year: 2023 },
|
||||
{ semester: 'SECOND', year: 2023 },
|
||||
{ semester: 'FIRST', year: 2024 },
|
||||
{ semester: 'SECOND', year: 2024 },
|
||||
{ semester: 'FIRST', year: 2025 },
|
||||
{ semester: 'SECOND', year: 2025 },
|
||||
{ semester: 'FIRST', year: 2026 }
|
||||
];
|
||||
// 2022 -> Y1, 2023 -> Y2, 2024 -> Y3, 2025 -> Y4, 2026 -> Y5
|
||||
assert.strictEqual(yearLevelForSemester(history, 2022), 1);
|
||||
assert.strictEqual(yearLevelForSemester(history, 2023), 2);
|
||||
assert.strictEqual(yearLevelForSemester(history, 2024), 3);
|
||||
assert.strictEqual(yearLevelForSemester(history, 2025), 4);
|
||||
assert.strictEqual(yearLevelForSemester(history, 2026), 5);
|
||||
// Summer of 2025 is the same standing as 2025 -> Year 4 (period is not
|
||||
// passed in; the standing is purely the history position).
|
||||
assert.strictEqual(yearLevelForSemester(history, 2025), 4);
|
||||
});
|
||||
|
||||
test('yearLevelForSemester clamps to program length and handles gaps', () => {
|
||||
// History ends at 2025; a query beyond it must not exceed the final level.
|
||||
const history = [
|
||||
{ semester: 'FIRST', year: 2022 },
|
||||
{ semester: 'SECOND', year: 2022 },
|
||||
{ semester: 'FIRST', year: 2023 },
|
||||
{ semester: 'SECOND', year: 2023 },
|
||||
{ semester: 'FIRST', year: 2024 },
|
||||
{ semester: 'SECOND', year: 2024 },
|
||||
{ semester: 'FIRST', year: 2025 },
|
||||
{ semester: 'SECOND', year: 2025 }
|
||||
];
|
||||
assert.strictEqual(yearLevelForSemester(history, 2028), 4); // clamped
|
||||
// Gap year: a skipped academic year still advances standing.
|
||||
const gap = [
|
||||
{ semester: 'FIRST', year: 2022 },
|
||||
{ semester: 'SECOND', year: 2022 },
|
||||
{ semester: 'FIRST', year: 2024 },
|
||||
{ semester: 'SECOND', year: 2024 }
|
||||
];
|
||||
assert.strictEqual(yearLevelForSemester(gap, 2024), 2);
|
||||
});
|
||||
|
||||
test('yearLevelForSemester handles missing year and empty history', () => {
|
||||
assert.strictEqual(yearLevelForSemester([], 2024), null);
|
||||
assert.strictEqual(yearLevelForSemester([{ semester: 'FIRST', year: 2022 }], NaN), null);
|
||||
});
|
||||
|
||||
// ---- Prospectus ----
|
||||
|
||||
test('parseProspectusYearUnits totals units per year level', () => {
|
||||
const html = `
|
||||
<h3>Year Level: 1</h3>
|
||||
<table><tr><th>Course</th></tr>
|
||||
<tr><td>CIS 1101</td><td>PROGRAMMING I</td><td>3.0</td></tr>
|
||||
<tr><td>CIS 1102</td><td>INTRO</td><td>3.0</td></tr>
|
||||
</table>
|
||||
<h3>Year Level: 2</h3>
|
||||
<table><tr><th>Course</th></tr>
|
||||
<tr><td>CIS 2101</td><td>DATA STRUCTURES</td><td>3.0</td></tr>
|
||||
</table>
|
||||
<h3>Year Level: 3</h3>
|
||||
<table><tr><th>Course</th></tr>
|
||||
<tr><td>IT 3101N</td><td>PLATFORM</td><td>4.0</td></tr>
|
||||
</table>
|
||||
<h3>Year Level: 4</h3>
|
||||
<table><tr><th>Course</th></tr>
|
||||
<tr><td>IT 4101</td><td>CAPSTONE</td><td>2.0</td></tr>
|
||||
</table>
|
||||
`;
|
||||
const y = parseProspectusYearUnits(html);
|
||||
assert.deepStrictEqual(y, [
|
||||
{ year: 1, units: 6 },
|
||||
{ year: 2, units: 3 },
|
||||
{ year: 3, units: 4 },
|
||||
{ year: 4, units: 2 }
|
||||
]);
|
||||
});
|
||||
|
||||
// ---- Lacking subjects ----
|
||||
|
||||
test('parseLackingSubjects returns remaining units and courses', () => {
|
||||
const html = `
|
||||
<table>
|
||||
<tr><th>Course Code</th><th>Description</th><th>Units</th></tr>
|
||||
<tr><td>IT 3206</td><td>INTEGRATIVE PROGRAMMING</td><td>3.0</td></tr>
|
||||
<tr><td>IT FREE EL</td><td>FREE ELECTIVE</td><td>3.0</td></tr>
|
||||
<tr><td>IT 4202N</td><td>PRACTICUM 2</td><td>6.0</td></tr>
|
||||
</table>
|
||||
`;
|
||||
const l = parseLackingSubjects(html);
|
||||
assert.strictEqual(l.units, '12.00');
|
||||
assert.strictEqual(l.courses.length, 3);
|
||||
assert.strictEqual(parseLackingSubjectsUnits(html), '12.00');
|
||||
});
|
||||
|
||||
test('parseLackingSubjects handles empty input', () => {
|
||||
assert.strictEqual(parseLackingSubjectsUnits(''), '0.00');
|
||||
});
|
||||
|
||||
// ---- Units math ----
|
||||
|
||||
test('computeYearStandingFromUnits maps finished units to year standing', () => {
|
||||
// Cumulative 52/99/148/181 (matches the real BSIT prospectus).
|
||||
const yearUnits = [
|
||||
{ year: 1, units: 52 },
|
||||
{ year: 2, units: 47 },
|
||||
{ year: 3, units: 49 },
|
||||
{ year: 4, units: 33 }
|
||||
];
|
||||
// finished = 181 - 24 = 157 -> Year 4 (real user data).
|
||||
assert.strictEqual(computeYearStandingFromUnits(yearUnits, 24), 4);
|
||||
// No lacking -> finished everything -> final year.
|
||||
assert.strictEqual(computeYearStandingFromUnits(yearUnits, 0), 4);
|
||||
// finished 40 -> still Year 1 (< 52).
|
||||
assert.strictEqual(computeYearStandingFromUnits(yearUnits, 141), 1);
|
||||
// finished 60 -> Year 2 (52 <= 60 < 99).
|
||||
assert.strictEqual(computeYearStandingFromUnits(yearUnits, 121), 2);
|
||||
// exact boundary: finished 52 -> Year 2.
|
||||
assert.strictEqual(computeYearStandingFromUnits(yearUnits, 129), 2);
|
||||
// finished 99 -> Year 3.
|
||||
assert.strictEqual(computeYearStandingFromUnits(yearUnits, 82), 3);
|
||||
});
|
||||
|
||||
test('computeYearStandingFromUnits handles empty and negative', () => {
|
||||
assert.strictEqual(computeYearStandingFromUnits([], 5), null);
|
||||
// lacking > total -> clamps to first year.
|
||||
assert.strictEqual(computeYearStandingFromUnits([{ year: 1, units: 10 }], 99), 1);
|
||||
});
|
||||
Reference in New Issue
Block a user