diff --git a/.eslintrc.json b/.eslintrc.json index 25418f3..2837a8e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,7 +14,8 @@ "createInteractionModule": "readonly", "SCRIPT_USER_EMAIL": "readonly", "IS_ADMIN": "readonly", - "escapeHtml": "readonly" + "escapeHtml": "readonly", + "App": "writable" }, "rules": { "no-unused-vars": "warn", diff --git a/Index.html b/Index.html index c391182..f70241a 100644 --- a/Index.html +++ b/Index.html @@ -473,5 +473,7 @@

PDF 下載選項 + + diff --git a/JavaScript.html b/JavaScript.html index 02a3597..450ad63 100644 --- a/JavaScript.html +++ b/JavaScript.html @@ -855,10 +855,7 @@ }); }, - getShortUserName: function (email) { - if (!email || email.indexOf('@') === -1) return email; - return email.split('@')[0]; - }, + // getShortUserName — moved to UtilityFunctions.js.html (Phase 1 PR1) isCurrentUserAdmin: function () { return typeof IS_ADMIN !== 'undefined' ? IS_ADMIN : false; @@ -895,17 +892,9 @@ return scheduleData; }, - generateUniqueId: function () { - return Date.now().toString(36) + Math.random().toString(36).substr(2, 9); - }, + // generateUniqueId — moved to UtilityFunctions.js.html (Phase 1 PR1) - stringToHashCode: function (str) { - let hash = 5381; - for (let i = 0; i < str.length; i++) { - hash = ((hash << 5) + hash) + str.charCodeAt(i); - } - return hash; - }, + // stringToHashCode — moved to UtilityFunctions.js.html (Phase 1 PR1) buildCourseColorMap: function (dataSource = this.scheduleData) { this.courseColorMap = {}; @@ -957,36 +946,11 @@ }); }, - timeToMinutes: function (timeStr) { - try { - const [hours, minutes] = timeStr.split(':').map(Number); - return hours * 60 + minutes; - } catch (e) { - console.error('時間轉換失敗:', e, timeStr); - return 0; - } - }, + // timeToMinutes — moved to UtilityFunctions.js.html (Phase 1 PR1) - formatTime: function (timeStr) { - if (!timeStr) return '00:00'; - timeStr = timeStr.trim(); - if (AppConfig.TIME_REGEX.test(timeStr)) { - const [hours, minutes] = timeStr.split(':'); - return hours.padStart(2, '0') + ':' + minutes.padStart(2, '0'); - } - return '00:00'; - }, + // formatTime — moved to UtilityFunctions.js.html (Phase 1 PR1) - formatTimestampForFilename: function (timestamp) { - if (!timestamp) return ''; - const date = new Date(timestamp); - const YYYY = date.getFullYear(); - const MM = String(date.getMonth() + 1).padStart(2, '0'); - const DD = String(date.getDate()).padStart(2, '0'); - const HH = String(date.getHours()).padStart(2, '0'); - const mm = String(date.getMinutes()).padStart(2, '0'); - return `${YYYY}${MM}${DD}_${HH}${mm}`; - }, + // formatTimestampForFilename — moved to UtilityFunctions.js.html (Phase 1 PR1) _filterScheduleData: function (data, filterPredicate) { const filteredData = {}; @@ -1105,14 +1069,7 @@ this.historyModule.saveState(); }, - hexToRgb: function (hex) { - if (!hex) return [255, 255, 255]; // Default to white if color is undefined - const bigint = parseInt(hex.slice(1), 16); - const r = (bigint >> 16) & 255; - const g = (bigint >> 8) & 255; - const b = bigint & 255; - return [r, g, b]; - }, + // hexToRgb — moved to UtilityFunctions.js.html (Phase 1 PR1) printScheduleToPdf: async function (pdfOptions) { this.ui.showLoading('正在準備下載,請稍候...'); @@ -1437,7 +1394,7 @@ // --- Initialization --- window.App = App; // Expose App to global scope for modules - App.init(); + // App.init() moved to Index.html — runs after all domain modules load (Phase 1 PR1) })(); \ No newline at end of file diff --git a/UtilityFunctions.js.html b/UtilityFunctions.js.html new file mode 100644 index 0000000..26e7c98 --- /dev/null +++ b/UtilityFunctions.js.html @@ -0,0 +1,72 @@ + diff --git a/tests/unit/appWiringContracts.test.js b/tests/unit/appWiringContracts.test.js index 85fd589..db4709d 100644 --- a/tests/unit/appWiringContracts.test.js +++ b/tests/unit/appWiringContracts.test.js @@ -63,20 +63,19 @@ const EXTRACTED_TO_LIB = new Set([ // stateHelpers.js 'handleEditClassroom', 'findNextUpcomingClasses', 'saveDataToServer', 'countOccurrences', 'updateAllOccurrences', - // utilityFunctions.js - 'stringToHashCode', 'hexToRgb', 'getShortUserName', 'formatTime', - 'formatTimestampForFilename', 'sortClassrooms', 'ensureDataIds', + // utilityFunctions.js (remaining in JavaScript.html) + 'sortClassrooms', 'ensureDataIds', 'buildCourseColorMap', // dataCollectionHelpers.js 'getAllTags', 'getGlobalAllTags', 'getGlobalAllCourseNames', 'getGlobalAllTeachers', // frontendUtils.js - 'timeToMinutes', 'checkTimeConflict', 'filterDataByTags', 'filterDataByActiveFilters', + 'checkTimeConflict', 'filterDataByTags', 'filterDataByActiveFilters', // interactionHelpers.js (handleDrop → applyDrop) 'handleDrop', // appLifecycleHelpers.js (new — this wave) 'loadInitialSchedules', 'loadSchedule', 'canManageCurrentScheduleSettings', 'loadAndApplyPersistedFilters', 'applyFilters', 'clearAdvancedFilters', - 'clearAllFilters', 'generateUniqueId', 'refreshLockHeartbeat', + 'clearAllFilters', 'refreshLockHeartbeat', 'saveSchedulesToLocal', ]); @@ -117,6 +116,17 @@ const PRIVATE_HELPERS = new Set([ '_filterScheduleData', ]); +/** + * Methods moved to IIFE domain modules (Phase 1 #129). + * These still exist on App at runtime but are no longer in JavaScript.html source. + * They are tested via their DI copies in tests/lib/. + */ +const IIFE_EXTRACTED = new Set([ + // UtilityFunctions.js.html (PR1) + 'getShortUserName', 'generateUniqueId', 'stringToHashCode', + 'timeToMinutes', 'formatTime', 'formatTimestampForFilename', 'hexToRgb', +]); + // ─── Tests ───────────────────────────────────────────────────────────────── describe('JavaScript.html App method wiring contracts (#116)', () => { @@ -127,8 +137,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('_')); - // If this changes, a method was added/removed — update the classification above - expect(publicMethods.length).toBe(48); + // 48 original - 7 IIFE-extracted = 41 remaining in JavaScript.html + expect(publicMethods.length).toBe(41); }); it('every public App method should be classified (extracted OR not-extractable)', () => { @@ -138,13 +148,15 @@ describe('JavaScript.html App method wiring contracts (#116)', () => { ); expect( unclassified, - `Unclassified methods found — add to EXTRACTED_TO_LIB or NOT_EXTRACTABLE: ${unclassified.join(', ')}` + `Unclassified methods found — add to EXTRACTED_TO_LIB, NOT_EXTRACTABLE, or IIFE_EXTRACTED: ${unclassified.join(', ')}` ).toEqual([]); }); - it('no method should be in both EXTRACTED and NOT_EXTRACTABLE', () => { - const overlap = [...EXTRACTED_TO_LIB].filter(m => NOT_EXTRACTABLE.has(m)); - expect(overlap).toEqual([]); + it('no method should be in multiple classifications', () => { + const allSets = [EXTRACTED_TO_LIB, NOT_EXTRACTABLE, IIFE_EXTRACTED]; + const allItems = [...EXTRACTED_TO_LIB, ...NOT_EXTRACTABLE, ...IIFE_EXTRACTED]; + const duplicates = allItems.filter((item, idx) => allItems.indexOf(item) !== idx); + expect(duplicates).toEqual([]); }); it('private helpers should be accounted for', () => { @@ -180,8 +192,11 @@ describe('JavaScript.html App method wiring contracts (#116)', () => { it('should have good extraction ratio', () => { const publicMethods = appMethods.filter(m => !m.startsWith('_')); const extractedCount = publicMethods.filter(m => EXTRACTED_TO_LIB.has(m)).length; - const ratio = extractedCount / publicMethods.length; - // Target: at least 60% of public methods should be extracted + const iifeCount = IIFE_EXTRACTED.size; + const totalExtracted = extractedCount + iifeCount; + const totalPublicIncludingIife = publicMethods.length + iifeCount; + const ratio = totalExtracted / totalPublicIncludingIife; + // Target: at least 60% of ALL public methods (including IIFE-extracted) should be extracted expect(ratio).toBeGreaterThanOrEqual(0.6); }); });