feat: on-page Download PDF opens Letter-size printable Study Load; drop pdf-lib/background path
This commit is contained in:
@@ -1,47 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
function errMsg(err) {
|
|
||||||
return String((err && err.message) || err);
|
|
||||||
}
|
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
||||||
if (msg && msg.type === 'scrape-study-load') {
|
|
||||||
browser.tabs.query({ active: true, currentWindow: true })
|
|
||||||
.then((tabs) => {
|
|
||||||
const tab = tabs[0];
|
|
||||||
if (!tab || tab.id == null) throw new Error('No active tab.');
|
|
||||||
return browser.tabs.sendMessage(tab.id, { type: 'scrape-study-load' });
|
|
||||||
})
|
|
||||||
.then((data) => sendResponse(data))
|
|
||||||
.catch((err) => sendResponse({ ok: false, error: errMsg(err) }));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+76
-9
@@ -123,22 +123,89 @@
|
|||||||
header = {};
|
header = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalUnits = extractTotalUnits(document.body.innerText);
|
return { header, courses };
|
||||||
return { header, courses, totalUnits };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
// ---- On-page Download PDF button ----
|
||||||
if (msg && msg.type === 'scrape-study-load') {
|
const BTN_ID = 'ismisHelperDownloadBtn';
|
||||||
scrapeStudyLoad()
|
|
||||||
.then((data) => sendResponse({ ok: true, data }))
|
function showToast(msg, isError) {
|
||||||
.catch((err) => sendResponse({ ok: false, error: String((err && err.message) || err) }));
|
if (window.toastr) {
|
||||||
return true; // async response
|
if (isError) window.toastr.error(msg);
|
||||||
|
else window.toastr.success(msg);
|
||||||
|
} else if (isError) {
|
||||||
|
alert(msg);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function findTotalUnitsP() {
|
||||||
|
const ps = document.querySelectorAll('#StudyLoad p');
|
||||||
|
for (const p of ps) {
|
||||||
|
if (/Total Units/i.test(p.textContent || '')) return p;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDownloadPdf() {
|
||||||
|
const btn = document.getElementById(BTN_ID);
|
||||||
|
try {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Opening...';
|
||||||
|
const { header, courses } = await scrapeStudyLoad();
|
||||||
|
const included = ISMIStools.filterStudyLoadCourses(courses);
|
||||||
|
if (!included.length) throw new Error('No enrolled courses found.');
|
||||||
|
const totalUnits = ISMIStools.sumUnits(included);
|
||||||
|
const html = ISMIStools.buildStudyLoadHtml({ header, courses: included, totalUnits });
|
||||||
|
const win = window.open('', '_blank');
|
||||||
|
if (!win) {
|
||||||
|
showToast('Popup blocked — allow popups for this site, then try again.', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
win.document.write(html);
|
||||||
|
win.document.close();
|
||||||
|
showToast('Opened printable Study Load.');
|
||||||
|
} catch (err) {
|
||||||
|
showToast('Error: ' + ((err && err.message) || err), true);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="fa fa-download"></i> Download PDF';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectDownloadButton() {
|
||||||
|
if (document.getElementById(BTN_ID)) return;
|
||||||
|
const unitsP = findTotalUnitsP();
|
||||||
|
if (!unitsP) return;
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.id = BTN_ID;
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = 'btn green pull-right';
|
||||||
|
btn.style.marginTop = '10px';
|
||||||
|
btn.innerHTML = '<i class="fa fa-download"></i> Download PDF';
|
||||||
|
btn.addEventListener('click', onDownloadPdf);
|
||||||
|
unitsP.insertAdjacentElement('afterend', btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryInjectButton() {
|
||||||
|
if (document.getElementById(BTN_ID)) return;
|
||||||
|
if (document.querySelector('#StudyLoad table.table-forum')) injectDownloadButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchStudyLoad() {
|
||||||
|
tryInjectButton();
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
const inStudyLoad = mutations.some(
|
||||||
|
(m) => m.target && m.target.closest && m.target.closest('#StudyLoad')
|
||||||
|
);
|
||||||
|
if (inStudyLoad) tryInjectButton();
|
||||||
|
});
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
|
||||||
(async function init() {
|
(async function init() {
|
||||||
if (!(await isEnabled())) return;
|
if (!(await isEnabled())) return;
|
||||||
watchPrivacyModal();
|
watchPrivacyModal();
|
||||||
if (/\/Account\/Login/i.test(location.href)) autoLogin();
|
if (/\/Account\/Login/i.test(location.href)) autoLogin();
|
||||||
|
if (document.getElementById('StudyLoad')) watchStudyLoad();
|
||||||
})();
|
})();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -11,40 +11,42 @@ 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. **Study Load PDF** — generate a PDF of the student's study load, replicating the
|
3. **Printable Study Load** — an on-page "Download PDF" button on the Study Load page
|
||||||
official ISMIS document, including course descriptions looked up from the
|
that opens a print-ready Letter-size document replicating the official ISMIS Study
|
||||||
Offered Courses endpoint.
|
Load (with course descriptions looked up from the Offered Courses endpoint),
|
||||||
|
triggering the browser print dialog so the user can print or "Save as PDF".
|
||||||
|
|
||||||
A single **master toggle** disables all extension behavior on the portal.
|
A single **master toggle** disables all extension behavior on the portal.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Single MV3 extension, plain JavaScript, no bundler.
|
Single MV3 extension, plain JavaScript, no bundler. No background script — all logic
|
||||||
|
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.js # service worker: message routing + PDF generation
|
├── content.js # runs on ismis.usc.edu.ph: auto-login, privacy, button, print flow
|
||||||
├── content.js # runs on ismis.usc.edu.ph: auto-login, privacy, scraping
|
├── parsers.js # UMD: table/profile/offered-courses parsing + filtering
|
||||||
├── popup.html # popup UI
|
├── printdoc.js # UMD: buildStudyLoadHtml (Letter-size printable document)
|
||||||
|
├── 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
|
|
||||||
└── 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.
|
||||||
- `activeTab` — access the current tab for scraping.
|
- No background, no `downloads` permission (the print flow uses `window.open` +
|
||||||
- Content script matches `https://ismis.usc.edu.ph/*`.
|
the browser's print dialog).
|
||||||
|
|
||||||
## Component 1 — Auto-login
|
## Component 1 — Auto-login
|
||||||
|
|
||||||
- Content script runs on `https://ismis.usc.edu.ph/Account/Login`.
|
- Content script runs on `https://ismis.usc.edu.ph/Account/Login`.
|
||||||
- Reads the active (first enabled) profile from `browser.storage.local`.
|
- Reads the active (enabled) profile from `browser.storage.local`.
|
||||||
- Locates the username and password input fields (by common `name`/`id` values such
|
- Locates the username and password input fields (`name`/`id` = `Username` /
|
||||||
as `UserName`, `Password`, `username`, `password`).
|
`Password`, with fallback variants).
|
||||||
- Fills both fields, then submits the form (auto-submit).
|
- Fills both fields, then submits the form (auto-submit).
|
||||||
- If no profile is enabled, or the master toggle is off, does nothing.
|
- If no profile is enabled, or the master toggle is off, does nothing.
|
||||||
|
|
||||||
@@ -62,68 +64,74 @@ ismis-helper/
|
|||||||
- Stored in `browser.storage.local` under key `profiles` as an array of:
|
- Stored in `browser.storage.local` under key `profiles` as an array of:
|
||||||
`{ id, name, username, password, enabled }`.
|
`{ id, name, username, password, enabled }`.
|
||||||
- Popup provides add, edit, delete, and enable/disable per profile.
|
- Popup provides add, edit, delete, and enable/disable per profile.
|
||||||
- The first profile with `enabled === true` is used for auto-login.
|
- Only one profile can be enabled at a time (enabling one disables the others);
|
||||||
|
that profile is used for auto-login.
|
||||||
- Master toggle stored under key `enabled` (boolean).
|
- Master toggle stored under key `enabled` (boolean).
|
||||||
|
|
||||||
## Component 4 — Study Load PDF
|
## Component 4 — Printable Study Load
|
||||||
|
|
||||||
|
### On-page button
|
||||||
|
- The content script watches the Study Load page (`#StudyLoad`). When the course
|
||||||
|
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`
|
||||||
|
footer, styled like the page's buttons (`btn green pull-right`).
|
||||||
|
|
||||||
### Scrape
|
### Scrape
|
||||||
- On the Study Load page, parse the course table with columns:
|
- Parse the course table (columns: Group #, Course Code, Schedule, Faculty, Units,
|
||||||
**Group #, Course Code, Schedule, Faculty, Units, Status**.
|
Status) via `parseStudyLoadTable`.
|
||||||
- Read the total-units footer (e.g. `12.00 Units / Total Units: 18.00`).
|
|
||||||
- **Header info** (full name, student ID, program, year level, semester) is scraped
|
- **Header info** (full name, student ID, program, year level, semester) is scraped
|
||||||
from the **My Profile** page (`/Manage/Student`), which exposes: Full Name,
|
from the **My Profile** page (`/Manage/Student`).
|
||||||
Student ID, Program, Year Level, and current semester. If the profile page is not
|
|
||||||
available, the header fields are entered manually in the popup before generating
|
### Filtering
|
||||||
the PDF.
|
- **DISSOLVED courses are excluded** (`filterStudyLoadCourses`).
|
||||||
|
- Total units = sum of the included courses' units (`sumUnits`).
|
||||||
|
|
||||||
### Description lookup
|
### Description lookup
|
||||||
- For each unique course code, POST to
|
- For each unique course code, POST to
|
||||||
`https://ismis.usc.edu.ph/CourseSchedule/CourseScheduleOffered` with form data:
|
`https://ismis.usc.edu.ph/CourseSchedule/CourseScheduleOffered` with the
|
||||||
- `Courses=<course code>`
|
`__RequestVerificationToken` and selected `AcademicPeriod`/`AcademicYear`, then
|
||||||
- `AcademicPeriod=<selected value>`
|
parse the returned table for the **Course Description**.
|
||||||
- `AcademicYear=<selected value>`
|
- If lookup fails for a course, the description is left blank and generation
|
||||||
- `__RequestVerificationToken=<token from page>`
|
continues.
|
||||||
- plus the `AjaxFormVM.*` fields observed in the HAR capture.
|
|
||||||
- Parse the returned HTML table to extract the **Course Description** for the course.
|
|
||||||
- If lookup fails for a course, leave the description blank and continue.
|
|
||||||
|
|
||||||
### Manual fallback
|
### Print document
|
||||||
- If scraping finds no table, the popup shows a manual entry form for the same fields
|
- `buildStudyLoadHtml` (printdoc.js) produces a self-contained HTML document at the
|
||||||
(including optional course descriptions).
|
official dimensions (**Letter, 8.5in × 11in** via `@page`), replicating the official
|
||||||
|
layout: title block (STUDENT'S STUDY LOAD, semester, date range, date enrolled),
|
||||||
### PDF generation
|
student info row (ID, name, program, year level), the course table (GROUP, COURSE,
|
||||||
- `background.js` uses **pdf-lib** to build the PDF replicating the official layout:
|
COURSE DESCRIPTION, TEACHER, UNITS, NOTE — NOTE is the status), and a footer
|
||||||
- Header block: semester, "STUDENT'S STUDY LOAD", date range, date enrolled,
|
(total units, "Printed by: …").
|
||||||
student ID, name, program, year level.
|
- The button opens this document in a new window and auto-triggers
|
||||||
- Course table: Course, Course Description, Units, Teacher, Group, Note
|
`window.print()`, so the user prints or "Save as PDF" at the official dimensions.
|
||||||
(status: REGULAR/PARALLEL/DISSOLVED), Schedule.
|
- All scraped/entered values are HTML-escaped.
|
||||||
- Footer: total units, printed-by line.
|
|
||||||
- Downloads the PDF via the `downloads` API.
|
|
||||||
|
|
||||||
## Data flow
|
## Data flow
|
||||||
|
|
||||||
```
|
```
|
||||||
popup ──message──▶ background ──message──▶ content (scrape + description lookup)
|
Study Load page ──▶ content script (scrape + filter + description lookup)
|
||||||
▲ │
|
│
|
||||||
└────────────── data back ◀────────────────────┘
|
▼
|
||||||
│
|
buildStudyLoadHtml (printdoc.js)
|
||||||
▼
|
│
|
||||||
pdf-lib → download
|
▼
|
||||||
|
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, PDF still generated.
|
- Description lookup fails for a course → blank description, document still opens.
|
||||||
- Scrape fails entirely → manual entry form in the popup.
|
- No enrolled (non-DISSOLVED) courses → error toast, no window opened.
|
||||||
- Login fields not found → log a warning, no crash.
|
- Popup blocked → toast explaining popups must be allowed for the site.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
- Node `node:test` suites: parsers (table/profile/offered-courses/filter/sum) and
|
||||||
|
printdoc (page size, layout, escaping, auto-print).
|
||||||
- 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 Study Load PDF generation: scrape, description lookup, manual fallback.
|
- Verify the on-page Download PDF button: appears below the total-units footer,
|
||||||
|
excludes DISSOLVED courses, and opens the Letter-size printable document.
|
||||||
|
|||||||
Vendored
-16
File diff suppressed because one or more lines are too long
+4
-7
@@ -1,17 +1,14 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "ISMIS Helper",
|
"name": "ISMIS Helper",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "Auto-login, auto-accept privacy policy, and Study Load PDF for the USC ISMIS portal.",
|
"description": "Auto-login, auto-accept privacy policy, and printable Study Load for the USC ISMIS portal.",
|
||||||
"permissions": ["storage", "downloads", "activeTab"],
|
"permissions": ["storage"],
|
||||||
"host_permissions": ["https://ismis.usc.edu.ph/*"],
|
"host_permissions": ["https://ismis.usc.edu.ph/*"],
|
||||||
"background": {
|
|
||||||
"scripts": ["libs/pdf-lib.min.js", "parsers.js", "pdf.js", "background.js"]
|
|
||||||
},
|
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["https://ismis.usc.edu.ph/*"],
|
"matches": ["https://ismis.usc.edu.ph/*"],
|
||||||
"js": ["parsers.js", "content.js"],
|
"js": ["parsers.js", "printdoc.js", "content.js"],
|
||||||
"run_at": "document_idle"
|
"run_at": "document_idle"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,146 +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 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 };
|
|
||||||
});
|
|
||||||
@@ -28,14 +28,8 @@ body {
|
|||||||
}
|
}
|
||||||
.switch input:checked + .slider { background: #4caf50; }
|
.switch input:checked + .slider { background: #4caf50; }
|
||||||
.switch input:checked + .slider::before { transform: translateX(18px); }
|
.switch input:checked + .slider::before { transform: translateX(18px); }
|
||||||
.tabs { display: flex; border-bottom: 1px solid #ddd; }
|
|
||||||
.tabs button {
|
|
||||||
flex: 1; padding: 8px; border: none; background: #f5f5f5; cursor: pointer;
|
|
||||||
}
|
|
||||||
.tabs button.active { background: #fff; font-weight: bold; border-bottom: 2px solid #006699; }
|
|
||||||
.tab { padding: 12px; }
|
.tab { padding: 12px; }
|
||||||
.tab h2 { margin: 0 0 8px; font-size: 14px; }
|
.tab h2 { margin: 0 0 8px; font-size: 14px; }
|
||||||
.tab h3 { margin: 12px 0 6px; font-size: 12px; color: #555; }
|
|
||||||
.profile-list { list-style: none; margin: 0 0 8px; padding: 0; }
|
.profile-list { list-style: none; margin: 0 0 8px; padding: 0; }
|
||||||
.profile-list li {
|
.profile-list li {
|
||||||
display: flex; align-items: center; gap: 6px;
|
display: flex; align-items: center; gap: 6px;
|
||||||
@@ -54,11 +48,3 @@ body {
|
|||||||
border: 1px solid #ccc; border-radius: 4px;
|
border: 1px solid #ccc; border-radius: 4px;
|
||||||
}
|
}
|
||||||
.form .row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
|
.form .row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
|
||||||
.course-row {
|
|
||||||
border: 1px solid #ddd; border-radius: 4px; padding: 6px; margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
.course-row .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
|
|
||||||
.course-row input { margin-bottom: 4px; }
|
|
||||||
.status { margin-top: 8px; font-size: 12px; }
|
|
||||||
.status.error { color: #c00; }
|
|
||||||
.status.ok { color: #080; }
|
|
||||||
|
|||||||
+1
-27
@@ -16,12 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="tabs">
|
<section class="tab">
|
||||||
<button data-tab="login" class="active">Auto-Login</button>
|
|
||||||
<button data-tab="studyload">Study Load</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<section id="tab-login" class="tab">
|
|
||||||
<h2>Profiles</h2>
|
<h2>Profiles</h2>
|
||||||
<ul id="profileList" class="profile-list"></ul>
|
<ul id="profileList" class="profile-list"></ul>
|
||||||
<button id="addProfile" class="btn">+ Add Profile</button>
|
<button id="addProfile" class="btn">+ Add Profile</button>
|
||||||
@@ -37,27 +32,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="tab-studyload" class="tab" hidden>
|
|
||||||
<h2>Study Load PDF</h2>
|
|
||||||
<button id="loadFromPortal" class="btn primary">Load from portal</button>
|
|
||||||
<div id="studyLoadForm" class="form" hidden>
|
|
||||||
<h3>Header</h3>
|
|
||||||
<input id="hFullName" placeholder="Full name">
|
|
||||||
<input id="hStudentId" placeholder="Student ID">
|
|
||||||
<input id="hProgram" placeholder="Program">
|
|
||||||
<input id="hYearLevel" placeholder="Year level">
|
|
||||||
<input id="hSemester" placeholder="Semester">
|
|
||||||
<input id="hDateEnrolled" placeholder="Date enrolled">
|
|
||||||
<input id="hDateRange" placeholder="Date range (e.g. AUGUST - JANUARY)">
|
|
||||||
<h3>Courses</h3>
|
|
||||||
<div id="coursesEditor"></div>
|
|
||||||
<button id="addCourse" class="btn">+ Add course</button>
|
|
||||||
<input id="totalUnits" placeholder="Total units">
|
|
||||||
<button id="generatePdf" class="btn primary">Generate PDF</button>
|
|
||||||
</div>
|
|
||||||
<div id="status" class="status"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -101,115 +101,6 @@ $('#saveProfile').addEventListener('click', async () => {
|
|||||||
renderProfiles(profiles);
|
renderProfiles(profiles);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- Tabs ----
|
|
||||||
document.querySelectorAll('.tabs button').forEach((btn) => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
document.querySelectorAll('.tabs button').forEach((b) => b.classList.remove('active'));
|
|
||||||
btn.classList.add('active');
|
|
||||||
$('#tab-login').hidden = btn.dataset.tab !== 'login';
|
|
||||||
$('#tab-studyload').hidden = btn.dataset.tab !== 'studyload';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- Study Load ----
|
|
||||||
function setStatus(msg, cls) {
|
|
||||||
const el = $('#status');
|
|
||||||
el.textContent = msg;
|
|
||||||
el.className = 'status ' + (cls || '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCourses(courses) {
|
|
||||||
const box = $('#coursesEditor');
|
|
||||||
box.innerHTML = '';
|
|
||||||
courses.forEach((c, i) => {
|
|
||||||
const row = document.createElement('div');
|
|
||||||
row.className = 'course-row';
|
|
||||||
row.innerHTML =
|
|
||||||
'<div class="grid">' +
|
|
||||||
'<input data-i="' + i + '" data-f="group" placeholder="Group" value="' + escapeHtml(c.group || '') + '">' +
|
|
||||||
'<input data-i="' + i + '" data-f="courseCode" placeholder="Course code" value="' + escapeHtml(c.courseCode || '') + '">' +
|
|
||||||
'<input data-i="' + i + '" data-f="description" placeholder="Description" value="' + escapeHtml(c.description || '') + '">' +
|
|
||||||
'<input data-i="' + i + '" data-f="units" placeholder="Units" value="' + escapeHtml(c.units || '') + '">' +
|
|
||||||
'<input data-i="' + i + '" data-f="teacher" placeholder="Teacher" value="' + escapeHtml(c.teacher || '') + '">' +
|
|
||||||
'<input data-i="' + i + '" data-f="status" placeholder="Status" value="' + escapeHtml(c.status || '') + '">' +
|
|
||||||
'</div>' +
|
|
||||||
'<input data-i="' + i + '" data-f="schedule" placeholder="Schedule" value="' + escapeHtml(c.schedule || '') + '">' +
|
|
||||||
'<button class="btn remove-course" data-i="' + i + '">Remove</button>';
|
|
||||||
box.appendChild(row);
|
|
||||||
});
|
|
||||||
box.querySelectorAll('input').forEach((inp) => {
|
|
||||||
inp.addEventListener('input', () => {
|
|
||||||
const i = +inp.dataset.i;
|
|
||||||
const f = inp.dataset.f;
|
|
||||||
studyLoadCourses[i][f] = inp.value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
box.querySelectorAll('.remove-course').forEach((btn) => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
studyLoadCourses.splice(+btn.dataset.i, 1);
|
|
||||||
renderCourses(studyLoadCourses);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let studyLoadCourses = [];
|
|
||||||
|
|
||||||
$('#loadFromPortal').addEventListener('click', async () => {
|
|
||||||
setStatus('Loading from portal...');
|
|
||||||
try {
|
|
||||||
const res = await browser.runtime.sendMessage({ type: 'scrape-study-load' });
|
|
||||||
if (!res || !res.ok) throw new Error((res && res.error) || 'Scrape failed');
|
|
||||||
const d = res.data;
|
|
||||||
$('#hFullName').value = d.header.fullName || '';
|
|
||||||
$('#hStudentId').value = d.header.studentId || '';
|
|
||||||
$('#hProgram').value = d.header.program || '';
|
|
||||||
$('#hYearLevel').value = d.header.yearLevel || '';
|
|
||||||
$('#hSemester').value = d.header.semester || '';
|
|
||||||
$('#hDateEnrolled').value = d.header.dateEnrolled || '';
|
|
||||||
$('#hDateRange').value = d.header.dateRange || '';
|
|
||||||
$('#totalUnits').value = d.totalUnits || '';
|
|
||||||
studyLoadCourses = d.courses || [];
|
|
||||||
renderCourses(studyLoadCourses);
|
|
||||||
$('#studyLoadForm').hidden = false;
|
|
||||||
setStatus('Loaded. Review and generate.', 'ok');
|
|
||||||
} catch (err) {
|
|
||||||
setStatus('Could not load from portal: ' + err.message + ' — enter manually below.', 'error');
|
|
||||||
studyLoadCourses = [];
|
|
||||||
renderCourses(studyLoadCourses);
|
|
||||||
$('#studyLoadForm').hidden = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#addCourse').addEventListener('click', () => {
|
|
||||||
studyLoadCourses.push({ group: '', courseCode: '', description: '', units: '', teacher: '', schedule: '', status: '' });
|
|
||||||
renderCourses(studyLoadCourses);
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#generatePdf').addEventListener('click', async () => {
|
|
||||||
const data = {
|
|
||||||
header: {
|
|
||||||
fullName: $('#hFullName').value.trim(),
|
|
||||||
studentId: $('#hStudentId').value.trim(),
|
|
||||||
program: $('#hProgram').value.trim(),
|
|
||||||
yearLevel: $('#hYearLevel').value.trim(),
|
|
||||||
semester: $('#hSemester').value.trim(),
|
|
||||||
dateEnrolled: $('#hDateEnrolled').value.trim(),
|
|
||||||
dateRange: $('#hDateRange').value.trim()
|
|
||||||
},
|
|
||||||
courses: studyLoadCourses,
|
|
||||||
totalUnits: $('#totalUnits').value.trim()
|
|
||||||
};
|
|
||||||
if (!data.courses.length) { setStatus('Add at least one course.', 'error'); return; }
|
|
||||||
setStatus('Generating PDF...');
|
|
||||||
try {
|
|
||||||
const res = await browser.runtime.sendMessage({ type: 'generate-pdf', data });
|
|
||||||
if (!res || !res.ok) throw new Error((res && res.error) || 'Generation failed');
|
|
||||||
setStatus('PDF download started.', 'ok');
|
|
||||||
} catch (err) {
|
|
||||||
setStatus('Error: ' + err.message, 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- Init ----
|
// ---- Init ----
|
||||||
(async function init() {
|
(async function init() {
|
||||||
await loadMaster();
|
await loadMaster();
|
||||||
|
|||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
(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) => ({
|
||||||
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||||
|
}[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(' ');
|
||||||
|
|
||||||
|
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 };
|
||||||
|
});
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
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');
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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('<script>'), 'script must appear escaped');
|
||||||
|
assert.ok(html.includes('A&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()');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user