diff --git a/Index.html b/Index.html index bba72d9..8fdb4c1 100644 --- a/Index.html +++ b/Index.html @@ -478,6 +478,7 @@

PDF 下載選項 + diff --git a/JavaScript.html b/JavaScript.html index d73089b..edfa895 100644 --- a/JavaScript.html +++ b/JavaScript.html @@ -82,321 +82,9 @@ }, // --- DATA & SCHEDULE LOGIC --- - showFirstTimeScheduleSelector: function () { - const modal = AppElements.firstTimeScheduleSelectModal; - const select = AppElements.firstTimeScheduleSelect; - const confirmBtn = AppElements.firstTimeScheduleConfirmBtn; - - select.innerHTML = ''; // Clear previous options - const scheduleIds = Object.keys(this.schedules); - - if (scheduleIds.length === 0) { - select.innerHTML = ''; - confirmBtn.disabled = true; - } else { - scheduleIds.forEach(id => { - const option = document.createElement('option'); - option.value = id; - option.textContent = this.schedules[id].name; - select.appendChild(option); - }); - confirmBtn.disabled = false; - } - - modal.style.display = 'flex'; - - confirmBtn.onclick = () => { - const selectedId = select.value; - if (selectedId) { - modal.style.display = 'none'; - this.loadSchedule(selectedId); - this.ui.updateScheduleSelect(); // Ensure main dropdown is also updated - } - }; - }, - - handleEditClassroom: function (oldName, newName) { - if (!newName) { - this.ui.showNotification('教室名稱不能為空!', 'error'); - return; - } - if (this.classrooms.includes(newName)) { - this.ui.showNotification(`教室名稱 "${newName}" 已存在!`, 'error'); - return; - } - - const index = this.classrooms.indexOf(oldName); - if (index > -1) { - this.classrooms[index] = newName; - } - - // Rename the key in the scheduleData object - if (this.scheduleData[oldName]) { - this.scheduleData[newName] = this.scheduleData[oldName]; - delete this.scheduleData[oldName]; - } - - this.ui.updateClassroomList(); - this.ui.renderScheduleTable(); - this.saveDataToLocal(); - this.historyModule.saveState(); - this.ui.showNotification(`教室名稱已從 "${oldName}" 更新為 "${newName}"`); - }, - - loadInitialSchedules: function () { - const localActiveId = localStorage.getItem('activeScheduleId'); - - // Check if the locally stored ID is a valid schedule that we just got from the server - if (localActiveId && this.schedules[localActiveId]) { - this.loadSchedule(localActiveId); - this.ui.updateScheduleSelect(); - } else { - // If no local preference, or the preference is invalid (e.g., schedule was deleted) - if (Object.keys(this.schedules).length > 0) { - // We have schedules from the server, so we MUST ask the user to choose one. - this.showFirstTimeScheduleSelector(); - } else { - // No schedules on the server at all. This is a rare edge case. - this.ui.showNotification("雲端沒有任何課表。請聯絡管理員新增一個。", "info", 5000); - this.schedules = {}; - this.activeScheduleId = null; - this.ui.renderScheduleTable(); // Render an empty table - this.ui.updateScheduleSelect(); - } - } - }, - - loadSchedule: function (scheduleId) { - // Release lock on the previous schedule before acquiring a new one - if (this.activeScheduleId) { - this.releaseLock(this.activeScheduleId); - } - - if (scheduleId === AppConfig.ALL_SCHEDULES_ID) { - this.isReadOnly = true; // "All schedules" is always read-only - this.activeScheduleId = AppConfig.ALL_SCHEDULES_ID; - this.classrooms = []; - this.scheduleData = {}; - this.tags = []; - this.ui.renderScheduleTable(); - this.ui.updateHeaderUIState(); - localStorage.setItem('activeScheduleId', scheduleId); - return; - } - - // Acquire lock for the new schedule - const hasLock = this.acquireLock(scheduleId); - this.isReadOnly = !hasLock; - - if (!this.schedules[scheduleId]) { - this.ui.showNotification(`錯誤:找不到指定的課表 ID: ${scheduleId}`, 'error'); - const firstId = Object.keys(this.schedules)[0]; - if (firstId) this.loadSchedule(firstId); - return; - } - this.activeScheduleId = scheduleId; - const schedule = this.schedules[scheduleId]; - - if (!schedule.data) { - this.ui.showNotification(`課表 "${schedule.name || '未命名'}" 格式錯誤,將為其創建新的空資料。`, 'error', 5000); - schedule.data = { classrooms: [], scheduleData: {}, tags: [] }; - } - - this.classrooms = schedule.data.classrooms || []; - this.scheduleData = this.ensureDataIds(schedule.data.scheduleData || {}); - this.tags = this.getAllTags(); // Dynamically get all tags - - // Initialize the tag filter for the current schedule - this.ui.initializeTagFilter(); - - // Load and apply persisted tag filters - this.loadAndApplyPersistedFilters(); - - this.buildCourseColorMap(); - this.ui.updateClassroomList(); - this.ui.renderScheduleTable(); - this.ui.updateHeaderUIState(); // Update UI based on lock and schedule mode - this.historyModule.resetHistory(); - localStorage.setItem('activeScheduleId', scheduleId); - }, - - saveSchedulesToLocal: function () { - localStorage.setItem('schedules', JSON.stringify(this.schedules)); - }, - - handleAddSchedule: async function (scheduleDetails) { - const { name, isDraft } = scheduleDetails; - this.ui.showLoading('正在新增課表...'); - const newId = `schedule_${Date.now()}`; - try { - const result = await ServerApi.call('addSchedule', { - id: newId, - name: name, - isDraft: isDraft, - metadataTimestamp: this.activeMetadataTimestamp - }); - if (result.error) { - throw new Error(result.error); - } - this.activeMetadataTimestamp = result.newMetadataTimestamp; - this.schedules[newId] = { - name: name, - createdBy: result.createdBy, - isDraft: isDraft, - data: { classrooms: [], scheduleData: {}, tags: [] } - }; - this.scheduleLastModified[newId] = result.lastModified; - this.ui.renderScheduleList(); - this.ui.updateScheduleSelect(); - this.ui.showNotification(`已新增課表: ${name}`); - } catch (err) { - this.ui.showNotification(`新增課表失敗: ${err.message}`, 'error'); - } finally { - this.ui.hideLoading(); - } - }, - - handleScheduleListClick: async function (e) { - const renameBtn = e.target.closest('.rename-schedule-btn'); - const deleteBtn = e.target.closest('.delete-schedule-btn'); - const copyBtn = e.target.closest('.copy-schedule-btn'); - - if (renameBtn) { - const id = renameBtn.dataset.id; - const oldSchedule = this.schedules[id]; - const result = await this.modals.showScheduleEditor({ - id: id, - name: oldSchedule.name, - isDraft: oldSchedule.isDraft || false - }); - - if (result) { - const hasChanged = result.name !== oldSchedule.name || result.isDraft !== oldSchedule.isDraft; - if (hasChanged) { - this.ui.showLoading('正在更新課表...'); - try { - const backendResult = await ServerApi.call('updateScheduleMetadata', { - id: id, - newName: result.name, - isDraft: result.isDraft, - metadataTimestamp: this.activeMetadataTimestamp - }); - if (backendResult.error) { throw new Error(backendResult.error); } - this.activeMetadataTimestamp = backendResult.newMetadataTimestamp; - this.schedules[id].name = result.name; - this.schedules[id].isDraft = result.isDraft; - // CRITICAL FIX: Update the lastModified timestamp after a metadata change - if (backendResult.lastModified) { - this.scheduleLastModified[id] = backendResult.lastModified; - } - this.ui.renderScheduleList(); - this.ui.updateScheduleSelect(); - this.ui.showNotification('課表已更新'); - } catch (err) { - this.ui.showNotification(`更新失敗: ${err.message}`, 'error'); - } finally { - this.ui.hideLoading(); - } - } - } - } - - if (deleteBtn) { - const id = deleteBtn.dataset.id; - if (await this.modals.showConfirm(`確定要刪除課表 "${this.schedules[id].name}" 嗎?此操作無法復原。`)) { - this.ui.showLoading('正在刪除課表...'); - try { - const result = await ServerApi.call('deleteSchedule', { id: id, metadataTimestamp: this.activeMetadataTimestamp }); - if (result.error) { throw new Error(result.error); } - this.activeMetadataTimestamp = result.newMetadataTimestamp; - delete this.schedules[id]; - if (id === this.activeScheduleId) { - this.activeScheduleId = Object.keys(this.schedules)[0]; - this.loadSchedule(this.activeScheduleId); - } - this.ui.renderScheduleList(); - this.ui.updateScheduleSelect(); - this.ui.showNotification('課表已刪除'); - } catch (err) { - this.ui.showNotification(`刪除失敗: ${err.message}`, 'error'); - } finally { - this.ui.hideLoading(); - } - } - } - - if (copyBtn) { - const sourceId = copyBtn.dataset.id; - const sourceName = this.schedules[sourceId].name; - const newName = await this.modals.showPrompt('請為複製的課表命名:', `${sourceName} (複製)`); - if (newName && newName.trim()) { - this.ui.showLoading('正在複製課表,請稍候...'); - try { - const result = await ServerApi.call('copySchedule', { sourceId: sourceId, newName: newName.trim(), metadataTimestamp: this.activeMetadataTimestamp }); - if (result.error) { throw new Error(result.error); } - this.activeMetadataTimestamp = result.newMetadataTimestamp; - const newId = result.newId; - this.schedules[newId] = { - name: newName.trim(), - createdBy: result.createdBy, - isDraft: result.isDraft, // Also copy the draft status - data: JSON.parse(JSON.stringify(this.schedules[sourceId].data)) - }; - // CRITICAL FIX: Set the lastModified timestamp for the new schedule - this.scheduleLastModified[newId] = result.lastModified; - this.ui.renderScheduleList(); - this.ui.updateScheduleSelect(); - this.ui.showNotification('課表複製成功!'); - } catch (err) { - this.ui.showNotification(`複製失敗: ${err.message}`, 'error'); - } finally { - this.ui.hideLoading(); - } - } - } - }, - - handleScheduleSelectChange: async function (e) { - const newId = e.target.value; - if (this.isDirty) { - if (!await this.modals.showConfirm('您有未儲存的變更,確定要切換課表並捨棄變更嗎?')) { - e.target.value = this.activeScheduleId; // Revert selection - return; - } - } - if (newId === AppConfig.ALL_SCHEDULES_ID) { - this.activeScheduleId = AppConfig.ALL_SCHEDULES_ID; - - const allSchedulesData = {}; - Object.values(this.schedules) - .filter(schedule => !schedule.isDraft) // Exclude draft schedules - .forEach(schedule => { - if (schedule.data && schedule.data.scheduleData) { - Object.entries(schedule.data.scheduleData).forEach(([classroom, days]) => { - if (!allSchedulesData[classroom]) allSchedulesData[classroom] = {}; - Object.entries(days).forEach(([day, courses]) => { - if (!allSchedulesData[classroom][day]) allSchedulesData[classroom][day] = []; - allSchedulesData[classroom][day].push(...courses); - }); - }); - } - }); - - this.tags = this.getGlobalAllTags(); - this.ui.initializeTagFilter(); - this.loadAndApplyPersistedFilters(); - this.modals.populateFilterModal(); // Ensure filter modal is updated for global view - - this.buildCourseColorMap(allSchedulesData); - - this.ui.renderScheduleTable(); - this.ui.updateGlobalUIForScheduleMode(); - this.ui.updateClearAllFiltersButtonVisibility(); - localStorage.setItem('activeScheduleId', this.activeScheduleId); - } else { - this.loadSchedule(newId); - } - }, + // showFirstTimeScheduleSelector, handleEditClassroom — moved to ScheduleManager.js.html (Phase 1 PR6) + // loadInitialSchedules, loadSchedule, saveSchedulesToLocal — moved to ScheduleManager.js.html (Phase 1 PR6) + // handleAddSchedule, handleScheduleListClick, handleScheduleSelectChange — moved to ScheduleManager.js.html (Phase 1 PR6) // loadAndApplyPersistedFilters, toggleAllFilterCheckboxes, applyFilters — moved to FilterEngine.js.html (Phase 1 PR4) // clearAdvancedFilters, clearAllFilters — moved to FilterEngine.js.html (Phase 1 PR4) @@ -454,23 +142,7 @@ // getShortUserName — moved to UtilityFunctions.js.html (Phase 1 PR1) - isCurrentUserAdmin: function () { - return typeof IS_ADMIN !== 'undefined' ? IS_ADMIN : false; - }, - - canManageCurrentScheduleSettings: function () { - if (this.isCurrentUserAdmin()) { - return true; - } - if (this.activeScheduleId === AppConfig.ALL_SCHEDULES_ID) { - return false; // Cannot manage settings in "All Schedules" view - } - const schedule = this.schedules[this.activeScheduleId]; - if (!schedule || !schedule.createdBy) { - return false; // No schedule or creator info - } - return this.currentUserEmail === this.getShortUserName(schedule.createdBy); - }, + // isCurrentUserAdmin, canManageCurrentScheduleSettings — moved to ScheduleManager.js.html (Phase 1 PR6) // ensureDataIds, buildCourseColorMap, sortClassrooms, checkTimeConflict — moved to DataCollection.js.html (Phase 1 PR3) @@ -488,46 +160,7 @@ // _forEachCourse, countOccurrences, updateAllOccurrences — moved to DataCollection.js.html (Phase 1 PR3) - handleDrop: function (evt) { - const { from, to, item, newIndex } = evt; - const classId = item.dataset.id; - const fromClassroom = from.dataset.classroom, fromDay = parseInt(from.dataset.day); - const toClassroom = to.dataset.classroom, toDay = parseInt(to.dataset.day); - - this.originalSourceListElement = null; - - if (from === to) { - this.ui.renderScheduleTable(); - return; - } - - const fromDaySchedule = this.scheduleData[fromClassroom]?.[fromDay] || []; - const itemIndex = fromDaySchedule.findIndex(c => c.id === classId); - - if (itemIndex === -1) { - console.error("Could not find dragged item in data model."); - this.ui.renderScheduleTable(); - return; - } - - const [movedItem] = fromDaySchedule.splice(itemIndex, 1); - - if (!this.scheduleData[toClassroom]) this.scheduleData[toClassroom] = {}; - if (!this.scheduleData[toClassroom][toDay]) this.scheduleData[toClassroom][toDay] = []; - - this.scheduleData[toClassroom][toDay].splice(newIndex, 0, movedItem); - - if (this.scheduleData[fromClassroom]?.[fromDay]?.length === 0) { - delete this.scheduleData[fromClassroom][fromDay]; - } - - this.scheduleData[toClassroom][toDay].sort((a, b) => this.timeToMinutes(a.timeStart) - this.timeToMinutes(b.timeStart)); - - this.ui.renderScheduleTable(); - this.saveDataToLocal(); - this.ui.showNotification('已移動課程'); - this.historyModule.saveState(); - }, + // handleDrop — moved to ScheduleManager.js.html (Phase 1 PR6) // hexToRgb — moved to UtilityFunctions.js.html (Phase 1 PR1) diff --git a/ScheduleManager.js.html b/ScheduleManager.js.html new file mode 100644 index 0000000..01d4afd --- /dev/null +++ b/ScheduleManager.js.html @@ -0,0 +1,405 @@ + diff --git a/tests/unit/appWiringContracts.test.js b/tests/unit/appWiringContracts.test.js index ddb96e7..b277afe 100644 --- a/tests/unit/appWiringContracts.test.js +++ b/tests/unit/appWiringContracts.test.js @@ -60,13 +60,9 @@ const privateMethods = extractPrivateMethods(jsHtmlSource); * These are tested via their extracted copies. */ const EXTRACTED_TO_LIB = new Set([ - // stateHelpers.js - 'handleEditClassroom', - // interactionHelpers.js (handleDrop → applyDrop) - 'handleDrop', - // appLifecycleHelpers.js (new — this wave) - 'loadInitialSchedules', 'loadSchedule', 'canManageCurrentScheduleSettings', - 'saveSchedulesToLocal', + // All methods previously here have been IIFE-extracted in Phase 1 PRs. + // DI tests in tests/lib/ still reference them but classifications moved to IIFE_EXTRACTED. + // Kept empty — Phase 1 complete, no methods remain in JavaScript.html with DI copies. ]); /** @@ -76,12 +72,7 @@ const EXTRACTED_TO_LIB = new Set([ */ const NOT_EXTRACTABLE = new Set([ 'init', // DOM setup + timers + module init - 'showFirstTimeScheduleSelector', // DOM manipulation (modal, select options, event handlers) - 'handleAddSchedule', // ServerApi + DOM + state orchestration - 'handleScheduleListClick', // DOM event delegation + ServerApi + modals - 'handleScheduleSelectChange', // DOM event + modal confirm + state (aggregateScheduleData already extracted) 'applyTagFilters', // Tagify instance + modal confirm + DOM - 'isCurrentUserAdmin', // Global var IS_ADMIN (trivial, 1 line) 'printScheduleToPdf', // jsPDF + DOM + ServerApi (massively coupled) ]); @@ -121,6 +112,12 @@ const IIFE_EXTRACTED = new Set([ // DataIO.js.html (PR5) 'loadVersions', 'handleLoadVersion', 'saveDataToLocal', 'loadDataFromServer', 'saveDataToServer', + // ScheduleManager.js.html (PR6) + 'showFirstTimeScheduleSelector', 'handleEditClassroom', + 'loadInitialSchedules', 'loadSchedule', 'saveSchedulesToLocal', + 'handleAddSchedule', 'handleScheduleListClick', 'handleScheduleSelectChange', + 'isCurrentUserAdmin', 'canManageCurrentScheduleSettings', + 'handleDrop', ]); // ─── Tests ───────────────────────────────────────────────────────────────── @@ -133,8 +130,8 @@ describe('JavaScript.html App method wiring contracts (#116)', () => { it('should have expected total App method count', () => { // All public methods (excluding underscore-prefixed private helpers) const publicMethods = appMethods.filter(m => !m.startsWith('_')); - // 48 original - 7 PR1 - 4 PR2 public - 11 PR3 public - 7 PR4 public - 5 PR5 = 14 remaining in JavaScript.html - expect(publicMethods.length).toBe(14); + // 48 original - 7 PR1 - 4 PR2 public - 11 PR3 public - 7 PR4 public - 5 PR5 - 11 PR6 = 3 remaining in JavaScript.html + expect(publicMethods.length).toBe(3); }); it('every public App method should be classified (extracted OR not-extractable)', () => { diff --git a/tests/unit/asyncMethodsWiring.test.js b/tests/unit/asyncMethodsWiring.test.js index 62a70dd..21a5c63 100644 --- a/tests/unit/asyncMethodsWiring.test.js +++ b/tests/unit/asyncMethodsWiring.test.js @@ -28,6 +28,12 @@ const dataIOSource = readFileSync( 'utf-8' ); +// ScheduleManager methods moved to IIFE module (Phase 1 PR6) +const scheduleManagerSource = readFileSync( + resolve(import.meta.dirname, '../../ScheduleManager.js.html'), + 'utf-8' +); + const gasSource = readFileSync( resolve(import.meta.dirname, '../../程式碼.js'), 'utf-8' @@ -62,13 +68,17 @@ const KNOWN_BACKEND_FUNCTIONS = extractGasFunctionNames(gasSource); */ // Methods in JavaScript.html const JS_HTML_ASYNC_METHODS = [ - ['handleAddSchedule', 227, ['addSchedule']], - ['handleScheduleListClick', 259, ['updateScheduleMetadata', 'deleteSchedule', 'copySchedule']], - ['handleScheduleSelectChange', 359, []], // No direct ServerApi.call — delegates to loadSchedule ['applyTagFilters', 433, []], // Pure frontend, no ServerApi ['printScheduleToPdf', 1117, ['getFontBase64FromDrive']], ]; +// Methods moved to ScheduleManager.js.html (Phase 1 PR6) +const SCHEDULE_MANAGER_ASYNC_METHODS = [ + ['handleAddSchedule', 0, ['addSchedule']], + ['handleScheduleListClick', 0, ['updateScheduleMetadata', 'deleteSchedule', 'copySchedule']], + ['handleScheduleSelectChange', 0, []], // No direct ServerApi.call — delegates to loadSchedule +]; + // Methods moved to DataIO.js.html (Phase 1 PR5) const DATA_IO_ASYNC_METHODS = [ ['loadVersions', 0, ['getVersions']], @@ -77,7 +87,7 @@ const DATA_IO_ASYNC_METHODS = [ ['saveDataToServer', 0, ['saveData']], ]; -const ASYNC_METHOD_WIRING = [...JS_HTML_ASYNC_METHODS, ...DATA_IO_ASYNC_METHODS]; +const ASYNC_METHOD_WIRING = [...JS_HTML_ASYNC_METHODS, ...SCHEDULE_MANAGER_ASYNC_METHODS, ...DATA_IO_ASYNC_METHODS]; // ─── Helpers ───────────────────────────────────────────────────────────── @@ -136,7 +146,7 @@ describe('Async App Methods — Wiring Smoke Tests (Static Analysis)', () => { // ─── 1. Method existence in source ──────────────────────────────────── - describe('all 5 JavaScript.html async methods exist', () => { + describe('all 2 JavaScript.html async methods exist', () => { it.each(JS_HTML_ASYNC_METHODS)( '%s is declared as async method in JavaScript.html', (methodName, _line, _expectedCalls) => { @@ -147,6 +157,17 @@ describe('Async App Methods — Wiring Smoke Tests (Static Analysis)', () => { ); }); + describe('all 3 ScheduleManager.js.html async methods exist', () => { + it.each(SCHEDULE_MANAGER_ASYNC_METHODS)( + '%s is declared as async method in ScheduleManager.js.html', + (methodName, _line, _expectedCalls) => { + const body = extractMethodBody(scheduleManagerSource, methodName); + expect(body).not.toBeNull(); + expect(body).toContain('async function'); + } + ); + }); + describe('all 4 DataIO.js.html async methods exist', () => { it.each(DATA_IO_ASYNC_METHODS)( '%s is declared as async method in DataIO.js.html', @@ -165,6 +186,7 @@ describe('Async App Methods — Wiring Smoke Tests (Static Analysis)', () => { */ function resolveSource(methodName) { if (DATA_IO_ASYNC_METHODS.some(([n]) => n === methodName)) return dataIOSource; + if (SCHEDULE_MANAGER_ASYNC_METHODS.some(([n]) => n === methodName)) return scheduleManagerSource; return jsHtmlSource; } diff --git a/tests/unit/lifecycleRegression.test.js b/tests/unit/lifecycleRegression.test.js index bad1d5e..578fdd0 100644 --- a/tests/unit/lifecycleRegression.test.js +++ b/tests/unit/lifecycleRegression.test.js @@ -23,6 +23,8 @@ const jsHtmlSource = loadSource(import.meta.dirname, '../../JavaScript.html'); const dataIOSource = loadSource(import.meta.dirname, '../../DataIO.js.html'); const interactionSource = loadSource(import.meta.dirname, '../../Interaction.js.html'); const uiSource = loadSource(import.meta.dirname, '../../UI.js.html'); +// ScheduleManager methods moved to IIFE module (Phase 1 PR6) +const scheduleManagerSource = loadSource(import.meta.dirname, '../../ScheduleManager.js.html'); // ─── Tests ─────────────────────────────────────────────────────────────── @@ -120,7 +122,8 @@ describe('Lifecycle Regression — init → load → render → interact → sav }); describe('Phase B.1: loadInitialSchedules — routing logic', () => { - const loadInitBody = extractMethodBody(jsHtmlSource, 'loadInitialSchedules'); + // Method now in ScheduleManager.js.html + const loadInitBody = extractMethodBody(scheduleManagerSource, 'loadInitialSchedules'); it('loadInitialSchedules method exists', () => { expect(loadInitBody).not.toBeNull(); @@ -131,15 +134,15 @@ describe('Lifecycle Regression — init → load → render → interact → sav }); it('calls loadSchedule for valid stored ID', () => { - expect(containsCall(loadInitBody, 'this\\.loadSchedule')).toBe(true); + expect(containsCall(loadInitBody, 'App\\.loadSchedule')).toBe(true); }); it('calls showFirstTimeScheduleSelector when no valid stored ID', () => { - expect(containsCall(loadInitBody, 'this\\.showFirstTimeScheduleSelector')).toBe(true); + expect(containsCall(loadInitBody, 'App\\.showFirstTimeScheduleSelector')).toBe(true); }); it('updates UI via updateScheduleSelect', () => { - expect(containsCall(loadInitBody, 'this\\.ui\\.updateScheduleSelect')).toBe(true); + expect(containsCall(loadInitBody, 'App\\.ui\\.updateScheduleSelect')).toBe(true); }); }); @@ -148,17 +151,18 @@ describe('Lifecycle Regression — init → load → render → interact → sav // ═══════════════════════════════════════════════════════════════════════ describe('Phase C: render — loadSchedule triggers rendering', () => { - const loadScheduleBody = extractMethodBody(jsHtmlSource, 'loadSchedule'); + // Method now in ScheduleManager.js.html + const loadScheduleBody = extractMethodBody(scheduleManagerSource, 'loadSchedule'); it('loadSchedule method exists', () => { expect(loadScheduleBody).not.toBeNull(); }); const renderCalls = [ - 'this\\.ui\\.renderScheduleTable', - 'this\\.ui\\.updateHeaderUIState', - 'this\\.ui\\.updateClassroomList', - 'this\\.ui\\.initializeTagFilter', + 'App\\.ui\\.renderScheduleTable', + 'App\\.ui\\.updateHeaderUIState', + 'App\\.ui\\.updateClassroomList', + 'App\\.ui\\.initializeTagFilter', ]; it.each(renderCalls)( @@ -169,17 +173,17 @@ describe('Lifecycle Regression — init → load → render → interact → sav ); it('loadSchedule acquires lock on new schedule', () => { - expect(containsCall(loadScheduleBody, 'this\\.acquireLock')).toBe(true); + expect(containsCall(loadScheduleBody, 'App\\.acquireLock')).toBe(true); }); it('loadSchedule releases lock on previous schedule', () => { - expect(containsCall(loadScheduleBody, 'this\\.releaseLock')).toBe(true); + expect(containsCall(loadScheduleBody, 'App\\.releaseLock')).toBe(true); }); it('loadSchedule unpacks schedule data to working state', () => { - expect(containsCall(loadScheduleBody, 'this\\.classrooms\\s*=')).toBe(true); - expect(containsCall(loadScheduleBody, 'this\\.scheduleData\\s*=')).toBe(true); - expect(containsCall(loadScheduleBody, 'this\\.activeScheduleId\\s*=')).toBe(true); + expect(containsCall(loadScheduleBody, 'App\\.classrooms\\s*=')).toBe(true); + expect(containsCall(loadScheduleBody, 'App\\.scheduleData\\s*=')).toBe(true); + expect(containsCall(loadScheduleBody, 'App\\.activeScheduleId\\s*=')).toBe(true); }); it('loadSchedule persists selection to localStorage', () => { @@ -187,11 +191,11 @@ describe('Lifecycle Regression — init → load → render → interact → sav }); it('loadSchedule resets history module', () => { - expect(containsCall(loadScheduleBody, 'this\\.historyModule\\.resetHistory')).toBe(true); + expect(containsCall(loadScheduleBody, 'App\\.historyModule\\.resetHistory')).toBe(true); }); it('loadSchedule calls buildCourseColorMap', () => { - expect(containsCall(loadScheduleBody, 'this\\.buildCourseColorMap')).toBe(true); + expect(containsCall(loadScheduleBody, 'App\\.buildCourseColorMap')).toBe(true); }); }); @@ -324,8 +328,8 @@ describe('Lifecycle Regression — init → load → render → interact → sav const loadBody = extractMethodBody(dataIOSource, 'loadDataFromServer'); expect(containsCall(loadBody, 'App\\.loadInitialSchedules')).toBe(true); - const loadInitBody = extractMethodBody(jsHtmlSource, 'loadInitialSchedules'); - expect(containsCall(loadInitBody, 'this\\.loadSchedule')).toBe(true); + const loadInitBody = extractMethodBody(scheduleManagerSource, 'loadInitialSchedules'); + expect(containsCall(loadInitBody, 'App\\.loadSchedule')).toBe(true); }); it('render → interact: init calls addEventListeners before loadDataFromServer', () => { diff --git a/tests/unit/syncMethodsWiring.test.js b/tests/unit/syncMethodsWiring.test.js index b13a7ab..e985107 100644 --- a/tests/unit/syncMethodsWiring.test.js +++ b/tests/unit/syncMethodsWiring.test.js @@ -31,6 +31,12 @@ const filterEngineSource = readFileSync( 'utf-8' ); +// ScheduleManager methods moved to separate IIFE module (Phase 1 PR6) +const scheduleManagerSource = readFileSync( + resolve(import.meta.dirname, '../../ScheduleManager.js.html'), + 'utf-8' +); + // ─── Helpers (shared pattern from Wave 2) ──────────────────────────────── /** @@ -100,12 +106,12 @@ const SYNC_METHOD_WIRING = [ 'AppElements\\.firstTimeScheduleSelect', 'AppElements\\.firstTimeScheduleConfirmBtn', 'document\\.createElement', - 'this\\.loadSchedule', + 'App\\.loadSchedule', ]], ['saveSchedulesToLocal', [ 'localStorage\\.setItem', 'JSON\\.stringify', - 'this\\.schedules', + 'App\\.schedules', ]], ['toggleAllFilterCheckboxes', [ 'AppElements\\.filterCourseList', @@ -175,10 +181,11 @@ describe('Sync App Methods — Wiring Smoke Tests (Static Analysis)', () => { // ─── 1. Method existence ────────────────────────────────────────────── describe('all sync methods exist in JavaScript.html or IIFE modules', () => { - // Non-lock, non-filter methods from JavaScript.html + // Non-lock, non-filter, non-schedule methods from JavaScript.html const jsHtmlMethods = SYNC_METHOD_WIRING.filter(([n]) => !['_getLocks', '_saveLocks', 'releaseCurrentLock', 'refreshLockHeartbeat', - 'toggleAllFilterCheckboxes', 'clearAdvancedFilters', 'clearAllFilters'].includes(n) + 'toggleAllFilterCheckboxes', 'clearAdvancedFilters', 'clearAllFilters', + 'showFirstTimeScheduleSelector', 'saveSchedulesToLocal'].includes(n) ); it.each(jsHtmlMethods)( '%s is declared in JavaScript.html', @@ -212,6 +219,18 @@ describe('Sync App Methods — Wiring Smoke Tests (Static Analysis)', () => { expect(body).not.toBeNull(); } ); + + // Schedule methods from ScheduleManager.js.html (Phase 1 PR6) + const scheduleMethods = SYNC_METHOD_WIRING.filter(([n]) => + ['showFirstTimeScheduleSelector', 'saveSchedulesToLocal'].includes(n) + ); + it.each(scheduleMethods)( + '%s is declared in ScheduleManager.js.html', + (methodName, _patterns) => { + const body = extractMethodBody(scheduleManagerSource, methodName); + expect(body).not.toBeNull(); + } + ); }); // ─── 2. Wiring correctness ──────────────────────────────────────────── @@ -231,7 +250,8 @@ describe('Sync App Methods — Wiring Smoke Tests (Static Analysis)', () => { describe('showFirstTimeScheduleSelector — DOM modal wiring', () => { const [, patterns] = SYNC_METHOD_WIRING.find(([n]) => n === 'showFirstTimeScheduleSelector'); - const body = extractMethodBody(jsHtmlSource, 'showFirstTimeScheduleSelector'); + // Method now in ScheduleManager.js.html + const body = extractMethodBody(scheduleManagerSource, 'showFirstTimeScheduleSelector'); it.each(patterns)( 'showFirstTimeScheduleSelector references %s', @@ -244,7 +264,8 @@ describe('Sync App Methods — Wiring Smoke Tests (Static Analysis)', () => { describe('saveSchedulesToLocal — localStorage persistence', () => { const [, patterns] = SYNC_METHOD_WIRING.find(([n]) => n === 'saveSchedulesToLocal'); - const body = extractMethodBody(jsHtmlSource, 'saveSchedulesToLocal'); + // Method now in ScheduleManager.js.html + const body = extractMethodBody(scheduleManagerSource, 'saveSchedulesToLocal'); it.each(patterns)( 'saveSchedulesToLocal uses %s', @@ -309,7 +330,7 @@ describe('Sync App Methods — Wiring Smoke Tests (Static Analysis)', () => { }); it('saveSchedulesToLocal is a one-liner localStorage.setItem', () => { - const body = extractMethodBody(jsHtmlSource, 'saveSchedulesToLocal'); + const body = extractMethodBody(scheduleManagerSource, 'saveSchedulesToLocal'); expect(body).not.toBeNull(); // Should be a simple wrapper — body should be short const lines = body.split('\n').filter(l => l.trim().length > 0);