([\s\S]*?)<\/small>/i.exec(html);
+ if (smallMatch) {
+ const lines = smallMatch[1].split(/
/i).map(cleanText).filter(Boolean);
+ result.semester = lines[lines.length - 1] || '';
+ }
+
+ const kvRe = /]*>([\s\S]*?)<\/td>\s* | ]*class="bold"[^>]*>([\s\S]*?)<\/td>/gi;
+ let m;
+ while ((m = kvRe.exec(html)) !== null) {
+ const key = cleanText(m[1]);
+ const val = cleanText(m[2]);
+ if (/full name/i.test(key)) result.fullName = val;
+ else if (/student id/i.test(key)) result.studentId = val;
+ else if (/year level/i.test(key)) result.yearLevel = val;
+ else if (/student type/i.test(key)) result.studentType = val;
+ }
+ return result;
+ }
+```
+
+Update the return statement:
+```js
+ return { parseStudyLoadTable, parseProfilePage };
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
+Expected: PASS (4 tests).
+
+- [ ] **Step 5: Commit**
+
+```bash
+cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
+git add parsers.js test/parsers.test.js
+git commit -m "feat: parse profile page header"
+```
+
+---
+
+### Task 4: `parsers.js` — parseOfferedCourses
+
+**Files:**
+- Modify: `parsers.js`
+- Test: `test/parsers.test.js`
+
+- [ ] **Step 1: Write the failing test**
+
+Append to `test/parsers.test.js`:
+```js
+const { parseOfferedCourses } = require('../parsers.js');
+const offeredFixture = fs.readFileSync(path.join(__dirname, 'fixtures', 'offered-courses.html'), 'utf8');
+
+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)));
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
+Expected: FAIL — `parseOfferedCourses is not a function`.
+
+- [ ] **Step 3: Add `parseOfferedCourses` to `parsers.js`**
+
+```js
+ function parseOfferedCourses(html) {
+ return extractRows(html)
+ .filter((tds) => tds.length >= 2)
+ .map((tds) => ({
+ courseCode: tds[0].split('-')[0].trim(),
+ description: tds[1]
+ }));
+ }
+```
+
+Update the return statement:
+```js
+ return { parseStudyLoadTable, parseProfilePage, parseOfferedCourses };
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
+Expected: PASS (6 tests).
+
+- [ ] **Step 5: Commit**
+
+```bash
+cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
+git add parsers.js test/parsers.test.js
+git commit -m "feat: parse offered courses descriptions"
+```
+
+---
+
+### Task 5: `pdf.js` — buildStudyLoadPdf
+
+**Files:**
+- Create: `pdf.js`
+- Test: `test/pdf.test.js`
+
+- [ ] **Step 1: Write the failing test**
+
+Create `test/pdf.test.js`:
+```js
+const { test } = require('node:test');
+const assert = require('node:assert');
+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'
+};
+
+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);
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
+Expected: FAIL — `Cannot find module '../pdf.js'`.
+
+- [ ] **Step 3: Write `pdf.js`**
+
+```js
+(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 : [''];
+ }
+
+ function buildStudyLoadPdf(data) {
+ const PDFLib = getPDFLib();
+ const { PDFDocument, StandardFonts, rgb } = PDFLib;
+ const header = data.header || {};
+ const courses = data.courses || [];
+
+ const doc = PDFDocument.create();
+ const page = doc.addPage([612, 792]); // Letter
+ const font = doc.getFont(StandardFonts.Helvetica);
+ const bold = doc.getFont(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: 40 },
+ { key: 'units', label: 'UNITS', x: margin + 40, w: 45 },
+ { key: 'teacher', label: 'TEACHER', x: margin + 85, w: 110 },
+ { key: 'description', label: 'COURSE DESCRIPTION', x: margin + 195, w: 150 },
+ { key: 'courseCode', label: 'COURSE', x: margin + 345, w: 70 },
+ { key: 'groupSchedule', label: 'GROUP', x: margin + 415, w: 97 }
+ ];
+ 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.apply(null, linesPerCol.map((l) => l.length)) * 11 + 8;
+ if (y - rowHeight < 50) {
+ // new page
+ page.drawText('', { x: margin, y: y, size: 1, font, color: black });
+ // (simple: just stop drawing further rows on overflow)
+ return false;
+ }
+ // background for header
+ if (isHeader) {
+ page.drawRectangle({ x: margin, y: y - rowHeight, width: contentWidth, height: rowHeight, color: rgb(0.85, 0.9, 0.95) });
+ }
+ // cell borders
+ 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 });
+ }
+ // text
+ 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;
+ }
+
+ // header row
+ const headerCells = {};
+ cols.forEach((c) => { headerCells[c.key] = c.label; });
+ drawRow(headerCells, true);
+
+ // data rows
+ 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;
+ }
+
+ // Footer
+ 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
+ }
+
+ return { buildStudyLoadPdf };
+});
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
+Expected: PASS (7 tests).
+
+- [ ] **Step 5: Commit**
+
+```bash
+cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
+git add pdf.js test/pdf.test.js
+git commit -m "feat: build study load PDF"
+```
+
+---
+
+### Task 6: `content.js` — auto-login, privacy accept, scraping
+
+**Files:**
+- Create: `content.js`
+
+- [ ] **Step 1: Write `content.js`**
+
+```js
+(function () {
+ 'use strict';
+
+ const MASTER_KEY = 'enabled';
+
+ async function isEnabled() {
+ const { enabled } = await browser.storage.local.get(MASTER_KEY);
+ return enabled !== false;
+ }
+
+ function autoAcceptPrivacy() {
+ const modal = document.getElementById('myModal');
+ if (!modal) return;
+ const checkbox = document.getElementById('Confirmation');
+ const submit = document.getElementById('Submit');
+ if (checkbox && !checkbox.checked) checkbox.click();
+ if (submit) submit.click();
+ }
+
+ function watchPrivacyModal() {
+ autoAcceptPrivacy();
+ const observer = new MutationObserver(() => autoAcceptPrivacy());
+ observer.observe(document.body, { childList: true, subtree: true });
+ setTimeout(() => observer.disconnect(), 30000);
+ }
+
+ async function autoLogin() {
+ const { profiles } = await browser.storage.local.get('profiles');
+ const active = (profiles || []).find((p) => p.enabled);
+ if (!active) return;
+ const userField = document.querySelector(
+ 'input[name="UserName"], input[name="username"], input[id="UserName"], input[id="username"]'
+ );
+ const passField = document.querySelector('input[type="password"]');
+ if (!userField || !passField) return;
+ userField.value = active.username;
+ passField.value = active.password;
+ userField.dispatchEvent(new Event('input', { bubbles: true }));
+ passField.dispatchEvent(new Event('input', { bubbles: true }));
+ const form = userField.closest('form') || passField.closest('form');
+ if (!form) return;
+ setTimeout(() => {
+ const submitBtn = form.querySelector('input[type="submit"], button[type="submit"]');
+ if (submitBtn) submitBtn.click();
+ else form.submit();
+ }, 300);
+ }
+
+ async function fetchCourseDescription(courseCode, academicPeriod, academicYear, token) {
+ const body = new URLSearchParams();
+ body.set('Courses', courseCode);
+ body.set('AcademicPeriod', academicPeriod);
+ body.set('AcademicYear', academicYear);
+ body.set('AjaxFormVM.Controller', 'CourseSchedule');
+ body.set('AjaxFormVM.Action', 'CourseScheduleOffered');
+ body.set('AjaxFormVM.ResultTargetId', 'CourseScheduleOfferedList');
+ body.set('AjaxFormVM.LinkToReturn', '');
+ body.set('AjaxFormVM.LinkToReturnTargetId', '');
+ body.set('AjaxFormVM.FormHeader', 'Choose term to view courses offered');
+ body.set('AjaxFormVM.FormHeaderIcon', '');
+ body.set('AjaxFormVM.FormId', 'CourseScheduleCourseScheduleOffered');
+ body.set('AjaxFormVM.ShowCloseButton', 'True');
+ body.set('AjaxFormVM.PreventAutoUpdateTarget', 'True');
+ body.set('AjaxFormVM.CloseModalAfterSubmit', 'False');
+ body.set('AjaxFormVM.AjaxFormType', 'Search');
+ body.set('AjaxFormVM.FormNoteLocation', '');
+ body.set('AjaxFormVM.SubmitButton.FaIcon', '');
+ body.set('AjaxFormVM.SubmitButton.ButtonText', '');
+ body.set('AjaxFormVM.SubmitButton.ButtonSize', '');
+ body.set('__RequestVerificationToken', token);
+ const resp = await fetch('/CourseSchedule/CourseScheduleOffered?Length=14', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
+ 'X-Requested-With': 'XMLHttpRequest'
+ },
+ body: body.toString(),
+ credentials: 'same-origin'
+ });
+ const html = await resp.text();
+ const courses = ISMIStools.parseOfferedCourses(html);
+ const match = courses.find((c) => c.courseCode === courseCode);
+ return match ? match.description : '';
+ }
+
+ function extractTotalUnits(text) {
+ const m = /Total Units:\s*([\d.]+)/i.exec(text);
+ return m ? m[1] : '';
+ }
+
+ async function scrapeStudyLoad() {
+ const table = document.querySelector('table.table-forum');
+ if (!table) throw new Error('No study load table found on this page.');
+ const courses = ISMIStools.parseStudyLoadTable(table.outerHTML);
+ if (!courses.length) throw new Error('No courses found in study load table.');
+
+ const periodEl = document.getElementById('AcademicPeriod');
+ const yearEl = document.getElementById('AcademicYear');
+ const tokenEl = document.querySelector('input[name="__RequestVerificationToken"]');
+ const academicPeriod = periodEl ? periodEl.value : '';
+ const academicYear = yearEl ? yearEl.value : '';
+ const token = tokenEl ? tokenEl.value : '';
+
+ const uniqueCodes = [...new Set(courses.map((c) => c.courseCode))];
+ const descMap = {};
+ for (const code of uniqueCodes) {
+ try {
+ descMap[code] = await fetchCourseDescription(code, academicPeriod, academicYear, token);
+ } catch (e) {
+ descMap[code] = '';
+ }
+ }
+ courses.forEach((c) => { c.description = descMap[c.courseCode] || ''; });
+
+ let header = {};
+ try {
+ const resp = await fetch('/Manage/Student', { credentials: 'same-origin' });
+ const html = await resp.text();
+ header = ISMIStools.parseProfilePage(html);
+ } catch (e) {
+ header = {};
+ }
+
+ const totalUnits = extractTotalUnits(document.body.innerText);
+ return { header, courses, totalUnits };
+ }
+
+ browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
+ if (msg && msg.type === 'scrape-study-load') {
+ scrapeStudyLoad()
+ .then((data) => sendResponse({ ok: true, data }))
+ .catch((err) => sendResponse({ ok: false, error: String((err && err.message) || err) }));
+ return true; // async response
+ }
+ });
+
+ (async function init() {
+ if (!(await isEnabled())) return;
+ watchPrivacyModal();
+ if (/\/Account\/Login/i.test(location.href)) autoLogin();
+ })();
+})();
+```
+
+- [ ] **Step 2: Sanity-check syntax**
+
+Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && node --check content.js`
+Expected: no output (syntax OK).
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
+git add content.js
+git commit -m "feat: content script for auto-login, privacy, scraping"
+```
+
+---
+
+### Task 7: `background.js` — message routing + PDF generation
+
+**Files:**
+- Create: `background.js`
+
+- [ ] **Step 1: Write `background.js`**
+
+```js
+'use strict';
+
+importScripts('libs/pdf-lib.min.js', 'parsers.js', 'pdf.js');
+
+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';
+ await browser.downloads.download({ url, filename, saveAs: true });
+ setTimeout(() => URL.revokeObjectURL(url), 10000);
+}
+```
+
+- [ ] **Step 2: Sanity-check syntax**
+
+Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && node --check background.js`
+Expected: no output (syntax OK).
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
+git add background.js
+git commit -m "feat: background message routing and PDF download"
+```
+
+---
+
+### Task 8: Popup UI — profiles, master toggle, study-load editor
+
+**Files:**
+- Create: `popup.html`
+- Create: `popup.css`
+- Create: `popup.js`
+
+- [ ] **Step 1: Write `popup.html`**
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+ Profiles
+
+
+
+
+
+
+ Study Load PDF
+
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 2: Write `popup.css`**
+
+```css
+* { box-sizing: border-box; }
+body {
+ width: 360px;
+ margin: 0;
+ font-family: system-ui, sans-serif;
+ font-size: 13px;
+ color: #222;
+}
+.top {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 12px;
+ background: #006699;
+ color: #fff;
+}
+.top h1 { margin: 0; font-size: 16px; }
+.master { display: flex; align-items: center; gap: 6px; }
+.switch { position: relative; display: inline-block; width: 40px; height: 22px; }
+.switch input { display: none; }
+.slider {
+ position: absolute; cursor: pointer; inset: 0;
+ background: #ccc; border-radius: 22px; transition: 0.2s;
+}
+.slider::before {
+ content: ""; position: absolute; height: 18px; width: 18px;
+ left: 2px; top: 2px; background: #fff; border-radius: 50%; transition: 0.2s;
+}
+.switch input:checked + .slider { background: #4caf50; }
+.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 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 li {
+ display: flex; align-items: center; gap: 6px;
+ padding: 6px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 6px;
+}
+.profile-list .name { flex: 1; }
+.profile-list .user { color: #777; font-size: 11px; }
+.btn {
+ padding: 6px 10px; border: 1px solid #ccc; border-radius: 4px;
+ background: #fff; cursor: pointer;
+}
+.btn.primary { background: #006699; color: #fff; border-color: #006699; }
+.btn-row { display: flex; gap: 6px; margin-top: 6px; }
+.form input {
+ width: 100%; padding: 6px; margin-bottom: 6px;
+ border: 1px solid #ccc; border-radius: 4px;
+}
+.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; }
+```
+
+- [ ] **Step 3: Write `popup.js`**
+
+```js
+'use strict';
+
+const $ = (sel) => document.querySelector(sel);
+
+const MASTER_KEY = 'enabled';
+const PROFILES_KEY = 'profiles';
+
+let editingProfileId = null;
+
+// ---- Master toggle ----
+async function loadMaster() {
+ const { enabled } = await browser.storage.local.get(MASTER_KEY);
+ $('#masterToggle').checked = enabled !== false;
+ $('#masterLabel').textContent = enabled === false ? 'Disabled' : 'Enabled';
+}
+$('#masterToggle').addEventListener('change', async (e) => {
+ await browser.storage.local.set({ [MASTER_KEY]: e.target.checked });
+ $('#masterLabel').textContent = e.target.checked ? 'Enabled' : 'Disabled';
+});
+
+// ---- Profiles ----
+async function getProfiles() {
+ const { profiles } = await browser.storage.local.get(PROFILES_KEY);
+ return profiles || [];
+}
+async function saveProfiles(profiles) {
+ await browser.storage.local.set({ [PROFILES_KEY]: profiles });
+}
+
+function renderProfiles(profiles) {
+ const list = $('#profileList');
+ list.innerHTML = '';
+ for (const p of profiles) {
+ const li = document.createElement('li');
+ const name = document.createElement('div');
+ name.className = 'name';
+ name.innerHTML = '' + escapeHtml(p.name) + ' ' + escapeHtml(p.username) + '';
+ const toggle = document.createElement('input');
+ toggle.type = 'checkbox';
+ toggle.checked = !!p.enabled;
+ toggle.title = 'Enable/disable';
+ toggle.addEventListener('change', async () => {
+ p.enabled = toggle.checked;
+ await saveProfiles(profiles);
+ });
+ const edit = document.createElement('button');
+ edit.className = 'btn';
+ edit.textContent = 'Edit';
+ edit.addEventListener('click', () => openProfileForm(p));
+ const del = document.createElement('button');
+ del.className = 'btn';
+ del.textContent = 'Delete';
+ del.addEventListener('click', async () => {
+ await saveProfiles(profiles.filter((x) => x.id !== p.id));
+ renderProfiles(await getProfiles());
+ });
+ li.append(toggle, name, edit, del);
+ list.appendChild(li);
+ }
+}
+
+function escapeHtml(s) {
+ return String(s).replace(/[&<>"']/g, (c) => ({
+ '&': '&', '<': '<', '>': '>', '"': '"', "'": '''
+ }[c]));
+}
+
+function openProfileForm(p) {
+ editingProfileId = p ? p.id : null;
+ $('#pName').value = p ? p.name : '';
+ $('#pUsername').value = p ? p.username : '';
+ $('#pPassword').value = p ? p.password : '';
+ $('#pEnabled').checked = p ? !!p.enabled : true;
+ $('#profileForm').hidden = false;
+}
+$('#addProfile').addEventListener('click', () => openProfileForm(null));
+$('#cancelProfile').addEventListener('click', () => { $('#profileForm').hidden = true; });
+$('#saveProfile').addEventListener('click', async () => {
+ const profiles = await getProfiles();
+ const data = {
+ id: editingProfileId || Date.now().toString(),
+ name: $('#pName').value.trim(),
+ username: $('#pUsername').value.trim(),
+ password: $('#pPassword').value,
+ enabled: $('#pEnabled').checked
+ };
+ if (!data.name || !data.username) return;
+ if (editingProfileId) {
+ const i = profiles.findIndex((x) => x.id === editingProfileId);
+ if (i >= 0) profiles[i] = data;
+ } else {
+ profiles.push(data);
+ }
+ await saveProfiles(profiles);
+ $('#profileForm').hidden = true;
+ 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 =
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ ' ' +
+ '' +
+ '';
+ 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 downloaded.', 'ok');
+ } catch (err) {
+ setStatus('Error: ' + err.message, 'error');
+ }
+});
+
+// ---- Init ----
+(async function init() {
+ await loadMaster();
+ renderProfiles(await getProfiles());
+})();
+```
+
+- [ ] **Step 4: Sanity-check syntax**
+
+Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && node --check popup.js`
+Expected: no output (syntax OK).
+
+- [ ] **Step 5: Commit**
+
+```bash
+cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
+git add popup.html popup.css popup.js
+git commit -m "feat: popup UI for profiles and study load"
+```
+
+---
+
+### Task 9: End-to-end manual verification in Firefox
+
+**Files:** none (verification only)
+
+- [ ] **Step 1: Run the full test suite**
+
+Run: `cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper && npm test`
+Expected: all tests PASS.
+
+- [ ] **Step 2: Load the extension in Firefox**
+
+1. Open Firefox → `about:debugging` → "This Firefox" → "Load Temporary Add-on".
+2. Select `manifest.json` in `ismis-helper/`.
+3. Confirm the extension appears with no load errors.
+
+- [ ] **Step 3: Verify auto-login**
+1. Open `https://ismis.usc.edu.ph/Account/Login`.
+2. Add a profile in the popup (Auto-Login tab) and enable it.
+3. Reload the login page. Expected: username/password filled and form submitted.
+
+- [ ] **Step 4: Verify privacy auto-accept**
+1. With the master toggle on, open any portal page that shows the privacy modal.
+2. Expected: the `#myModal` modal is dismissed automatically.
+
+- [ ] **Step 5: Verify master toggle**
+1. Turn the master toggle OFF in the popup.
+2. Reload the login page. Expected: no auto-fill/submit occurs.
+
+- [ ] **Step 6: Verify Study Load PDF**
+1. Open the Study Load page in the portal.
+2. In the popup Study Load tab, click "Load from portal".
+3. Expected: header and courses populate; descriptions filled in.
+4. Click "Generate PDF". Expected: a PDF downloads matching the official layout.
+
+- [ ] **Step 7: Verify manual fallback**
+1. Open a non-portal page (or a portal page without the table).
+2. Click "Load from portal". Expected: manual entry form appears.
+3. Enter a course, click "Generate PDF". Expected: PDF downloads.
+
+- [ ] **Step 8: Commit any fixes**
+
+```bash
+cd /Users/adrianbonpin/Documents/Code/personal/ismis-helper
+git add -A
+git commit -m "fix: verification adjustments"
+```
+
+---
+
+## Self-Review
+
+**Spec coverage:**
+- Auto-login (fill + submit) → Task 6, Task 9.
+- Master toggle → Task 8 (popup), Task 6 (content gate), Task 9.
+- Auto-accept privacy policy → Task 6, Task 9.
+- Profiles (add/edit/delete/toggle) → Task 8.
+- Study Load scrape → Task 6.
+- Description lookup via Offered Courses → Task 6 + Task 4 parser.
+- Header from My Profile → Task 6 + Task 3 parser.
+- Manual fallback → Task 8 (popup editor), Task 9.
+- PDF generation replicating official look → Task 5.
+- Download via downloads API → Task 7.
+
+**Placeholder scan:** No TBD/TODO; all steps contain concrete code and commands.
+
+**Type consistency:** `ISMIStools` namespace used consistently across `parsers.js`, `pdf.js`, `content.js`, `background.js`. Message types `scrape-study-load` and `generate-pdf` match between popup, background, and content. Data shape `{ header, courses, totalUnits }` consistent across scrape, popup, and PDF builder.
diff --git a/test/fixtures/offered-courses.html b/test/fixtures/offered-courses.html
new file mode 100644
index 0000000..0b59242
--- /dev/null
+++ b/test/fixtures/offered-courses.html
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+ | CourseCode |
+ Course Description |
+ Course Status |
+ Teacher/s |
+ Schedule |
+ Department Reserved |
+ Enrolled Students |
+
+
+
+ |
+
+ |
+
+
+
+ |
+ IT 3206 - Group 1
+ |
+ INTEGRATIVE PROGRAMMING AND TECHNOLOGY |
+ PARALLEL |
+ DELA PAZ, KHENT L.
|
+
+ TTh 03:00 PM - 04:30 PM LB468TCp
+ |
+
+DEPARTMENT OF COMPUTER, INFORMATION SCIENCES AND MATHEMATICS (5)
+
+ |
+ 10/10 |
+
+
+ |
+ IT 3206 - Group 2
+ |
+ INTEGRATIVE PROGRAMMING AND TECHNOLOGY |
+ PARALLEL |
+ |
+
+ TTh 06:00 PM - 07:30 PM LB468TCp
+ |
+
+DEPARTMENT OF COMPUTER, INFORMATION SCIENCES AND MATHEMATICS (5)
+
+ |
+ 5/5 |
+
+
+ |
+ IT 3206 - Group 3
+ |
+ INTEGRATIVE PROGRAMMING AND TECHNOLOGY |
+ PARALLEL |
+ |
+
+ TTh 12:30 PM - 02:00 PM LB447TCp
+ |
+
+DEPARTMENT OF COMPUTER, INFORMATION SCIENCES AND MATHEMATICS (4)
+
+ |
+ 4/4 |
+
+
+ |
+ IT 3206N - Group 1
+ |
+ INTEGRATIVE PROGRAMMING AND TECHNOLOGY |
+ BLOCKSECTION |
+ DELA PAZ, KHENT L.
|
+
+ MW 06:00 PM - 08:30 PM LB442TC
+ |
+
+DEPARTMENT OF COMPUTER, INFORMATION SCIENCES AND MATHEMATICS (20)
+
+ |
+ 19/29 |
+
+
+ |
+ IT 3206N - Group 2
+ |
+ INTEGRATIVE PROGRAMMING AND TECHNOLOGY |
+ BLOCKSECTION |
+ DELA PAZ, KHENT L.
|
+
+ TTh 06:00 PM - 08:30 PM LB468TC
+ |
+
+DEPARTMENT OF COMPUTER, INFORMATION SCIENCES AND MATHEMATICS (20)
+
+ |
+ 28/29 |
+
+
+ |
+ IT 3206N - Group 3
+ |
+ INTEGRATIVE PROGRAMMING AND TECHNOLOGY |
+ BLOCKSECTION |
+ DELA PAZ, KHENT L.
|
+
+ MW 12:30 PM - 03:00 PM LB448TC
+ |
+
+DEPARTMENT OF COMPUTER, INFORMATION SCIENCES AND MATHEMATICS (40)
+
+ |
+ 29/29 |
+
+
+ |
+ IT 3206N - Group 4
+ |
+ INTEGRATIVE PROGRAMMING AND TECHNOLOGY |
+ BLOCKSECTION |
+ DELA PAZ, KHENT L.
|
+
+ TTh 03:00 PM - 05:30 PM LB445TC
+ |
+
+DEPARTMENT OF COMPUTER, INFORMATION SCIENCES AND MATHEMATICS (20)
+
+ |
+ 28/29 |
+
+
+ |
+ IT 3206N - Group 5
+ |
+ INTEGRATIVE PROGRAMMING AND TECHNOLOGY |
+ REGULAR |
+ DIONSON, PRIMO JR. L.
|
+
+ TTh 12:30 PM - 03:00 PM LB446TC
+ |
+
+DEPARTMENT OF COMPUTER, INFORMATION SCIENCES AND MATHEMATICS (20)
+
+ |
+ 28/29 |
+
+
+ |
+ IT 3206N - Group 6
+ |
+ INTEGRATIVE PROGRAMMING AND TECHNOLOGY |
+ REGULAR |
+ |
+
+ Sat 07:30 AM - 12:30 PM LB446TC
+ |
+
+DEPARTMENT OF COMPUTER, INFORMATION SCIENCES AND MATHEMATICS (40)
+
+ |
+ 9/25 |
+
+
+
+ |
+
+
+ Page 1 of 1
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/profile.html b/test/fixtures/profile.html
new file mode 100644
index 0000000..8a81225
--- /dev/null
+++ b/test/fixtures/profile.html
@@ -0,0 +1,70 @@
+
+
+
+
+ Student Profile
+
+
+
+
+
+
+
+
+
+
+ BACHELOR OF SCIENCE IN INFORMATION TECHNOLOGY
+
+ DEPARTMENT OF COMPUTER, INFORMATION SCIENCES AND MATHEMATICS
+ 1ST Semester 2026
+
+
+
+
+
+
+ 
+
+
+
+
+
+ | Full Name |
+ ADRIAN ALFRED CHIONG BONPIN |
+
+
+ | Student ID |
+ 22103625 |
+
+
+ | Date of birth |
+ Wednesday, January 22, 2003 |
+
+
+ | Year Level |
+ 4 |
+
+
+ | Student Type |
+ CONTINUING |
+
+
+ | Foreign |
+ NO |
+
+
+ | Faculty Evaluation |
+ COMPLETED 1ST Semester 2026 |
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/study-load-table.html b/test/fixtures/study-load-table.html
new file mode 100644
index 0000000..494f491
--- /dev/null
+++ b/test/fixtures/study-load-table.html
@@ -0,0 +1,104 @@
+
+
+
+ | Group # |
+ Course Code |
+ Schedule |
+ Faculty |
+ Units |
+ Status |
+
+
+
+ |
+1
+
+
+ |
+
+CS 5105 |
+
+ F 01:30 PM - 04:30 PM LB486TC TC
+ |
+
+ |
+
+3.00 |
+
+DISSOLVED |
+
+
+ |
+1
+
+
+ |
+
+IT 3206 |
+
+ T Th 03:00 PM - 04:30 PM LB468TCp TC
+ |
+
+ DELA PAZ, KHENT LOZANO
+ |
+
+3.00 |
+
+PARALLEL |
+
+
+ |
+4
+
+
+ |
+
+IT 4102N |
+
+ Sun 07:30 AM - 10:30 AM LB486TCp TC
+ |
+
+ |
+
+3.00 |
+
+REGULAR |
+
+
+ |
+1
+
+
+ |
+
+IT 4103 |
+
+ F 06:00 PM - 09:00 PM LB484TCp TC
+ |
+
+ |
+
+3.00 |
+
+PARALLEL |
+
+
+ |
+1
+
+
+ |
+
+IT 5101 |
+
+ F 07:30 AM - 10:30 AM LB442TCp TC
+ |
+
+ |
+
+3.00 |
+
+PARALLEL |
+
+
+
\ No newline at end of file
|