ci: use Gitea API for releases #2

Closed
adrianbonpin wants to merge 56 commits from feat/ismis-helper into prod
9 changed files with 304 additions and 197 deletions
Showing only changes of commit 1537881ebc - Show all commits
+35
View File
@@ -0,0 +1,35 @@
'use strict';
function errMsg(err) {
return String((err && err.message) || err);
}
browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg && msg.type === 'generate-pdf') {
generatePdf(msg.data)
.then(() => sendResponse({ ok: true }))
.catch((err) => sendResponse({ ok: false, error: errMsg(err) }));
return true;
}
});
async function generatePdf(data) {
const bytes = await ISMIStools.buildStudyLoadPdf(data);
const blob = new Blob([bytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const name = (data.header && data.header.fullName) || 'Student';
const filename = 'Study Load - ' + name + '.pdf';
try {
const id = await browser.downloads.download({ url, filename, saveAs: true });
const onChanged = (delta) => {
if (delta.id === id && delta.state && (delta.state.current === 'complete' || delta.state.current === 'interrupted')) {
browser.downloads.onChanged.removeListener(onChanged);
URL.revokeObjectURL(url);
}
};
browser.downloads.onChanged.addListener(onChanged);
} catch (err) {
URL.revokeObjectURL(url);
throw err;
}
}
+7 -15
View File
@@ -150,25 +150,17 @@
const btn = document.getElementById(BTN_ID); const btn = document.getElementById(BTN_ID);
try { try {
btn.disabled = true; btn.disabled = true;
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Opening...'; btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Downloading...';
const { header, courses } = await scrapeStudyLoad(); const { header, courses } = await scrapeStudyLoad();
const included = ISMIStools.filterStudyLoadCourses(courses); const included = ISMIStools.filterStudyLoadCourses(courses);
if (!included.length) throw new Error('No enrolled courses found.'); if (!included.length) throw new Error('No enrolled courses found.');
const totalUnits = ISMIStools.sumUnits(included); const totalUnits = ISMIStools.sumUnits(included);
const html = ISMIStools.buildStudyLoadHtml({ header, courses: included, totalUnits }); const res = await browser.runtime.sendMessage({
const blob = new Blob([html], { type: 'text/html' }); type: 'generate-pdf',
const url = URL.createObjectURL(blob); data: { header, courses: included, totalUnits }
const win = window.open(url, '_blank'); });
if (!win) { if (!res || !res.ok) throw new Error((res && res.error) || 'PDF generation failed');
URL.revokeObjectURL(url); showToast('PDF downloaded.');
showToast('Popup blocked — allow popups for this site, then try again.', true);
return;
}
// The blob document runs its own onload → window.print(). Never touch
// win.document here: if the new tab navigates cross-origin, accessing it
// throws "Permission denied".
setTimeout(() => URL.revokeObjectURL(url), 60000);
showToast('Opened printable Study Load.');
} catch (err) { } catch (err) {
showToast('Error: ' + ((err && err.message) || err), true); showToast('Error: ' + ((err && err.message) || err), true);
} finally { } finally {
@@ -11,10 +11,10 @@ student portal experience. It provides three features:
1. **Auto-login** — auto-fill and auto-submit the login form from a saved profile. 1. **Auto-login** — auto-fill and auto-submit the login form from a saved profile.
2. **Auto-accept Privacy Policy** — dismiss the recurring privacy-policy modal. 2. **Auto-accept Privacy Policy** — dismiss the recurring privacy-policy modal.
3. **Printable Study Load** — an on-page "Download PDF" button on the Study Load page 3. **Study Load PDF** — an on-page "Download PDF" button on the Study Load page that
that opens a print-ready Letter-size document replicating the official ISMIS Study generates a PDF replicating the official ISMIS Study Load document (Letter size,
Load (with course descriptions looked up from the Offered Courses endpoint), with course descriptions looked up from the Offered Courses endpoint) and
triggering the browser print dialog so the user can print or "Save as PDF". downloads it directly.
A single **master toggle** disables all extension behavior on the portal. A single **master toggle** disables all extension behavior on the portal.
@@ -25,21 +25,29 @@ runs in the content script (scraping and print-document building happen in-page)
``` ```
ismis-helper/ ismis-helper/
├── manifest.json # MV3 manifest, permissions, content script, popup ├── manifest.json # MV3 manifest, permissions, content script, popup, background
├── content.js # runs on ismis.usc.edu.ph: auto-login, privacy, button, print flow ├── background.js # message handler: build PDF (pdf-lib) + download via downloads API
├── content.js # runs on ismis.usc.edu.ph: auto-login, privacy, button, scrape
├── parsers.js # UMD: table/profile/offered-courses parsing + filtering ├── parsers.js # UMD: table/profile/offered-courses parsing + filtering
├── printdoc.js # UMD: buildStudyLoadHtml (Letter-size printable document) ├── pdf.js # UMD: buildStudyLoadPdf (Letter PDF, official layout, pagination)
├── popup.html # popup UI (master toggle + profiles) ├── popup.html # popup UI (master toggle + profiles)
├── popup.js # popup logic ├── popup.js # popup logic
├── popup.css # popup styles ├── popup.css # popup styles
├── libs/pdf-lib.min.js # vendored pdf-lib library (used by background)
└── icons/ # extension icons (16, 32, 48, 128) └── icons/ # extension icons (16, 32, 48, 128)
``` ```
### Permissions (manifest.json) ### Permissions (manifest.json)
- `storage` — persist profiles and the master toggle. - `storage` — persist profiles and the master toggle.
- `downloads` — save the generated Study Load PDF.
- `host_permissions` — `https://ismis.usc.edu.ph/*` for the content script. - `host_permissions` — `https://ismis.usc.edu.ph/*` for the content script.
- No background, no `downloads` permission (the print flow uses `window.open` + - Background loads `libs/pdf-lib.min.js` + `pdf.js` via `background.scripts`.
the browser's print dialog).
## Background script
- `background.js` handles the `generate-pdf` message from the content script:
builds the PDF via `ISMIStools.buildStudyLoadPdf`, wraps the bytes in a Blob,
downloads via `browser.downloads.download({ saveAs: true })`, and revokes the
object URL when the download reaches `complete`/`interrupted` (and on failure).
## Component 1 — Auto-login ## Component 1 — Auto-login
@@ -68,13 +76,14 @@ ismis-helper/
that profile is used for auto-login. that profile is used for auto-login.
- Master toggle stored under key `enabled` (boolean). - Master toggle stored under key `enabled` (boolean).
## Component 4 — Printable Study Load ## Component 4 — Study Load PDF
### On-page button ### On-page button
- The content script watches the Study Load page (`#StudyLoad`). When the course - The content script watches the Study Load page (`#StudyLoad`). When the course
table appears (after the user selects academic period/year and searches), it table appears (after the user selects academic period/year and searches), it
injects a **"Download PDF"** button below the `12.00 Units / Total Units: 18.00` injects a **"Download PDF"** button inline with the
footer, styled like the page's buttons (`btn green pull-right`). `12.00 Units / Total Units: 18.00` footer (button on the left, units text on the
right, via a flex row), styled like the page's buttons (`btn green`).
### Scrape ### Scrape
- Parse the course table (columns: Group #, Course Code, Schedule, Faculty, Units, - Parse the course table (columns: Group #, Course Code, Schedule, Faculty, Units,
@@ -94,44 +103,43 @@ ismis-helper/
- If lookup fails for a course, the description is left blank and generation - If lookup fails for a course, the description is left blank and generation
continues. continues.
### Print document ### PDF generation
- `buildStudyLoadHtml` (printdoc.js) produces a self-contained HTML document at the - `pdf.js` (`buildStudyLoadPdf`, using **pdf-lib**) builds a Letter-size (612×792pt)
official dimensions (**Letter, 8.5in × 11in** via `@page`), replicating the official PDF replicating the official layout: title block (STUDENT'S STUDY LOAD, semester,
layout: title block (STUDENT'S STUDY LOAD, semester, date range, date enrolled), date range, date enrolled), student info row (ID, name, program, year level), the
student info row (ID, name, program, year level), the course table (GROUP, COURSE, course table (GROUP, COURSE, COURSE DESCRIPTION, TEACHER, UNITS, NOTE — NOTE is
COURSE DESCRIPTION, TEACHER, UNITS, NOTE — NOTE is the status), and a footer the status), and a footer (total units, "Printed by: …"). Multi-page
(total units, "Printed by: …"). pagination re-draws the table header. NOTE column is wide enough for `REGULAR`.
- The button opens this document in a new window and auto-triggers - The content script sends the data to the background (`generate-pdf`), which
`window.print()`, so the user prints or "Save as PDF" at the official dimensions. downloads the PDF via the `downloads` API (`saveAs: true`).
- All scraped/entered values are HTML-escaped.
## Data flow ## Data flow
``` ```
Study Load page ──▶ content script (scrape + filter + description lookup) Study Load page ──▶ content script (scrape + filter DISSOLVED + description lookup)
│
▼ browser.runtime.sendMessage({ type: 'generate-pdf', data })
background.js (pdf-lib buildStudyLoadPdf)
│ │
▼ ▼
buildStudyLoadHtml (printdoc.js) browser.downloads.download → Study Load PDF file
│
▼
window.open + window.print() → print / Save as PDF (Letter)
``` ```
## Error handling ## Error handling
- Master toggle off → no extension behavior on the portal. - Master toggle off → no extension behavior on the portal.
- No enabled profile → no auto-login. - No enabled profile → no auto-login.
- Description lookup fails for a course → blank description, document still opens. - Description lookup fails for a course → blank description, PDF still generated.
- No enrolled (non-DISSOLVED) courses → error toast, no window opened. - No enrolled (non-DISSOLVED) courses → error toast, no download.
- Popup blocked → toast explaining popups must be allowed for the site. - Download fails → error toast; object URL is revoked.
## Testing ## Testing
- Node `node:test` suites: parsers (table/profile/offered-courses/filter/sum) and - Node `node:test` suites: parsers (table/profile/offered-courses/filter/sum) and
printdoc (page size, layout, escaping, auto-print). pdf (valid Letter PDF output).
- Load via `about:debugging` → "Load Temporary Add-on". - Load via `about:debugging` → "Load Temporary Add-on".
- Verify auto-login on the login page. - Verify auto-login on the login page.
- Verify privacy modal auto-accept. - Verify privacy modal auto-accept.
- Verify profile add/edit/delete/toggle and master toggle. - Verify profile add/edit/delete/toggle and master toggle.
- Verify the on-page Download PDF button: appears below the total-units footer, - Verify the on-page Download PDF button: appears inline with the total-units
excludes DISSOLVED courses, and opens the Letter-size printable document. footer, excludes DISSOLVED courses, and downloads a Letter-size PDF.
+16
View File
File diff suppressed because one or more lines are too long
+7 -4
View File
@@ -1,14 +1,17 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "ISMIS Helper", "name": "ISMIS Helper",
"version": "1.1.0", "version": "1.2.0",
"description": "Auto-login, auto-accept privacy policy, and printable Study Load for the USC ISMIS portal.", "description": "Auto-login, auto-accept privacy policy, and Study Load PDF for the USC ISMIS portal.",
"permissions": ["storage"], "permissions": ["storage", "downloads"],
"host_permissions": ["https://ismis.usc.edu.ph/*"], "host_permissions": ["https://ismis.usc.edu.ph/*"],
"background": {
"scripts": ["libs/pdf-lib.min.js", "pdf.js", "background.js"]
},
"content_scripts": [ "content_scripts": [
{ {
"matches": ["https://ismis.usc.edu.ph/*"], "matches": ["https://ismis.usc.edu.ph/*"],
"js": ["parsers.js", "printdoc.js", "content.js"], "js": ["parsers.js", "content.js"],
"run_at": "document_idle" "run_at": "document_idle"
} }
], ],
+146
View File
@@ -0,0 +1,146 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.ISMIStools = Object.assign(root.ISMIStools || {}, factory());
}
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
function getPDFLib() {
return (typeof self !== 'undefined' && self.PDFLib) ||
(typeof globalThis !== 'undefined' && globalThis.PDFLib);
}
function wrapText(text, font, size, maxWidth) {
const words = String(text).split(/\s+/).filter(Boolean);
const lines = [];
let line = '';
for (const word of words) {
const candidate = line ? line + ' ' + word : word;
if (font.widthOfTextAtSize(candidate, size) <= maxWidth) {
line = candidate;
} else {
if (line) lines.push(line);
line = word;
}
}
if (line) lines.push(line);
return lines.length ? lines : [''];
}
async function buildStudyLoadPdf(data) {
const PDFLib = getPDFLib();
const { PDFDocument, StandardFonts, rgb } = PDFLib;
const header = data.header || {};
const courses = data.courses || [];
const doc = await PDFDocument.create();
let page = doc.addPage([612, 792]); // Letter
const font = doc.embedStandardFont(StandardFonts.Helvetica);
const bold = doc.embedStandardFont(StandardFonts.HelveticaBold);
const margin = 50;
const contentWidth = 612 - margin * 2;
let y = 792 - 50;
const black = rgb(0, 0, 0);
const gray = rgb(0.3, 0.3, 0.3);
function center(text, size, fontToUse, dy) {
const w = fontToUse.widthOfTextAtSize(text, size);
page.drawText(text, { x: (612 - w) / 2, y: y, size, font: fontToUse, color: black });
y -= dy;
}
function left(text, size, fontToUse, x, dy) {
page.drawText(text, { x, y, size, font: fontToUse, color: black });
y -= dy;
}
// Header
center(header.semester || '', 14, bold, 20);
center("STUDENT'S STUDY LOAD", 16, bold, 24);
center(header.dateRange || '', 11, font, 18);
if (header.dateEnrolled) left('Date Enrolled: ' + header.dateEnrolled, 10, font, margin, 16);
y -= 6;
if (header.studentId) left('Student ID: ' + header.studentId, 10, font, margin, 14);
if (header.fullName) left('Name: ' + header.fullName, 10, font, margin, 14);
if (header.program) left('Program: ' + header.program, 10, font, margin, 14);
if (header.yearLevel) left('Year Level: ' + header.yearLevel, 10, font, margin, 14);
y -= 10;
// Table columns: NOTE, UNITS, TEACHER, COURSE DESCRIPTION, COURSE, GROUP
const cols = [
{ key: 'status', label: 'NOTE', x: margin, w: 50 },
{ key: 'units', label: 'UNITS', x: margin + 50, w: 45 },
{ key: 'teacher', label: 'TEACHER', x: margin + 95, w: 110 },
{ key: 'description', label: 'COURSE DESCRIPTION', x: margin + 205, w: 150 },
{ key: 'courseCode', label: 'COURSE', x: margin + 355, w: 70 },
{ key: 'groupSchedule', label: 'GROUP', x: margin + 425, w: 87 }
];
const cellSize = 9;
const headerSize = 9;
function drawRow(cells, isHeader) {
const f = isHeader ? bold : font;
const size = isHeader ? headerSize : cellSize;
const linesPerCol = cols.map((c) => wrapText(cells[c.key] || '', f, size, c.w - 6));
const rowHeight = Math.max(...linesPerCol.map((l) => l.length)) * 11 + 8;
if (!isHeader && y - rowHeight < 50) {
// Row doesn't fit on the current page: start a new page and re-draw the header.
page = doc.addPage([612, 792]);
y = 792 - 50;
drawHeaderRow();
}
if (isHeader) {
page.drawRectangle({ x: margin, y: y - rowHeight, width: contentWidth, height: rowHeight, color: rgb(0.85, 0.9, 0.95) });
}
page.drawRectangle({ x: margin, y: y - rowHeight, width: contentWidth, height: rowHeight, borderColor: black, borderWidth: 0.5 });
for (const c of cols) {
page.drawRectangle({ x: c.x, y: y - rowHeight, width: c.w, height: rowHeight, borderColor: black, borderWidth: 0.5 });
}
for (let i = 0; i < cols.length; i++) {
const c = cols[i];
const lines = linesPerCol[i];
let ty = y - 10;
for (const line of lines) {
page.drawText(line, { x: c.x + 3, y: ty, size, font: f, color: black });
ty -= 11;
}
}
y -= rowHeight;
return true;
}
function drawHeaderRow() {
const headerCells = {};
cols.forEach((c) => { headerCells[c.key] = c.label; });
drawRow(headerCells, true);
}
drawHeaderRow();
for (const course of courses) {
const cells = {
status: course.status,
units: course.units,
teacher: course.teacher,
description: course.description,
courseCode: course.courseCode,
groupSchedule: (course.group ? course.group + ' ' : '') + (course.schedule || '')
};
if (!drawRow(cells, false)) break;
}
y -= 12;
if (data.totalUnits) {
page.drawText('Total Units: ' + data.totalUnits, { x: margin, y: y, size: 11, font: bold, color: black });
y -= 16;
}
const printedBy = 'Printed by: ' + (header.studentId || '') + ' on ' + new Date().toLocaleDateString();
page.drawText(printedBy, { x: margin, y: y, size: 9, font, color: gray });
return doc.save(); // returns Promise<Uint8Array>
}
return { buildStudyLoadPdf };
});
-85
View File
@@ -1,85 +0,0 @@
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.ISMIStools = Object.assign(root.ISMIStools || {}, factory());
}
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[c]));
}
// Builds a printable HTML document replicating the official ISMIS Study Load
// layout (Letter size, 8.5in x 11in). Auto-triggers window.print() on load so
// the user can print or "Save as PDF" at the official dimensions.
function buildStudyLoadHtml(data) {
const header = data.header || {};
const courses = data.courses || [];
const rows = courses.map((c) =>
'<tr>' +
'<td class="col-g">' + escapeHtml(c.group) + '</td>' +
'<td class="col-c">' + escapeHtml(c.courseCode) + '</td>' +
'<td class="col-d">' + escapeHtml(c.description) +
(c.schedule ? '<div class="sched">' + escapeHtml(c.schedule) + '</div>' : '') +
'</td>' +
'<td class="col-t">' + escapeHtml(c.teacher) + '</td>' +
'<td class="col-u">' + escapeHtml(c.units) + '</td>' +
'<td class="col-n">' + escapeHtml(c.status) + '</td>' +
'</tr>'
).join('\n');
const studentLine = [
header.studentId ? '<b>' + escapeHtml(header.studentId) + '</b>' : '',
escapeHtml(header.fullName || ''),
escapeHtml(header.program || ''),
header.yearLevel ? 'Year Level: ' + escapeHtml(header.yearLevel) : ''
].filter(Boolean).join('&nbsp;&nbsp;&nbsp;');
const semester = header.semester ? '<div class="centered">' + escapeHtml(header.semester) + '</div>\n' : '';
const dateRange = header.dateRange ? '<div class="centered">' + escapeHtml(header.dateRange) + '</div>\n' : '';
const dateEnrolled = header.dateEnrolled
? '<div class="date-enrolled">Date Enrolled: ' + escapeHtml(header.dateEnrolled) + '</div>\n'
: '';
const printedBy = 'Printed by: ' + (header.studentId || '') + ' on ' + new Date().toLocaleDateString();
return '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="utf-8">\n' +
'<title>Student\'s Study Load</title>\n<style>\n' +
'@page { size: 8.5in 11in; margin: 0.45in 0.5in; }\n' +
'html, body { margin: 0; padding: 0; }\n' +
'body { font-family: "Segoe UI", Arial, sans-serif; font-size: 9pt; color: #000; }\n' +
'.title { text-align: center; font-weight: bold; font-size: 9.5pt; margin: 0 0 2pt 0; }\n' +
'.centered { text-align: center; margin: 2pt 0; }\n' +
'.date-enrolled { font-style: italic; margin: 3pt 0; }\n' +
'.student { margin: 4pt 0 8pt 0; }\n' +
'table { width: 100%; border-collapse: collapse; border-bottom: 0.75pt solid #000; }\n' +
'thead th { font-weight: normal; font-size: 8pt; text-align: left; ' +
'border-top: 0.75pt solid #000; border-bottom: 0.75pt solid #000; padding: 1pt 2pt; }\n' +
'td { font-size: 8pt; padding: 1pt 2pt; vertical-align: top; }\n' +
'.col-g { width: 5%; } .col-c { width: 9%; } .col-d { width: 37%; }\n' +
'.col-t { width: 24%; } .col-u { width: 6%; text-align: right; } ' +
'.col-n { width: 19%; text-align: right; }\n' +
'.sched { margin-top: 1pt; }\n' +
'.total-row td { padding-top: 6pt; }\n' +
'</style>\n</head>\n<body>\n' +
'<div class="title">STUDENT\'S STUDY LOAD</div>\n' +
semester + dateRange + dateEnrolled +
'<div class="student">' + studentLine + '</div>\n' +
'<table>\n<thead><tr>' +
'<th>GROUP</th><th>COURSE</th><th>COURSE DESCRIPTION</th><th>TEACHER</th><th>UNITS</th><th>NOTE</th>' +
'</tr></thead>\n<tbody>\n' + rows + '\n' +
'<tr class="total-row"><td colspan="6" style="text-align:right;">' +
escapeHtml(data.totalUnits || '') +
'</td></tr>\n</tbody>\n</table>\n' +
'<div style="text-align:right; margin-top:3pt; font-size:8pt;">' + escapeHtml(printedBy) + '</div>\n' +
'<script>window.onload = function () { window.focus(); window.print(); };</script>\n' +
'</body>\n</html>';
}
return { buildStudyLoadHtml };
});
+52
View File
@@ -0,0 +1,52 @@
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 } = 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' });
}
test('buildStudyLoadPdf returns valid PDF bytes', async () => {
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);
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');
});
-60
View File
@@ -1,60 +0,0 @@
const { test } = require('node:test');
const assert = require('node:assert');
const { buildStudyLoadHtml } = require('../printdoc.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'
};
test('buildStudyLoadHtml uses Letter page size', () => {
const html = buildStudyLoadHtml(sampleData);
assert.match(html, /@page\s*\{[^}]*size:\s*8\.5in\s*11in/i);
});
test('buildStudyLoadHtml includes title, student info, and table headers', () => {
const html = buildStudyLoadHtml(sampleData);
assert.ok(html.includes("STUDENT'S STUDY LOAD"));
assert.ok(html.includes('ADRIAN ALFRED CHIONG BONPIN'));
assert.ok(html.includes('22103625'));
assert.ok(html.includes('Year Level: 4'));
for (const h of ['GROUP', 'COURSE', 'COURSE DESCRIPTION', 'TEACHER', 'UNITS', 'NOTE']) {
assert.ok(html.includes(h), 'missing header: ' + h);
}
});
test('buildStudyLoadHtml includes course data, schedule, and total', () => {
const html = buildStudyLoadHtml(sampleData);
assert.ok(html.includes('CIS 2101'));
assert.ok(html.includes('DATA STRUCTURES AND ALGORITHMS'));
assert.ok(html.includes('M,W 03:00 PM - 05:30 PM LB443TC'));
assert.ok(html.includes('6.00'));
});
test('buildStudyLoadHtml escapes user content', () => {
const html = buildStudyLoadHtml({
header: { fullName: '<script>alert(1)</script>' },
courses: [{ group: '', courseCode: 'A&B', description: 'x"y', units: '1', teacher: '', schedule: '', status: 'R' }],
totalUnits: '1.00'
});
assert.ok(!html.includes('<script>alert(1)</script>'), 'script must be escaped');
assert.ok(html.includes('&lt;script&gt;'), 'script must appear escaped');
assert.ok(html.includes('A&amp;B'), 'ampersand must be escaped');
});
test('buildStudyLoadHtml auto-prints on load', () => {
const html = buildStudyLoadHtml(sampleData);
assert.ok(/window\.print\(\)/.test(html), 'must call window.print()');
});