From 2aa149566e6b1c7840b581b537a5cf848cb9f810 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 04:47:24 +0000 Subject: [PATCH] feat: enhance SPA solver to maximize scheduled courses and report conflicts - Update `dist/static/js/solver.js` to implement a Max-Subset DFS algorithm. - Add absolute conflict detection to fail fast on pairwise incompatibility. - Add logic to skip courses (pruning) to resolve complex conflicts while maximizing the total count. - Update `dist/index.html` to display a warning for missing courses in the schedule details. - Apply a score penalty for missing courses. --- dist/index.html | 4 + dist/static/js/solver.js | 267 +++++++++++++++++---------- server.log | 28 ++- verification/spa_failed.png | Bin 0 -> 29408 bytes verification/spa_missing_courses.png | Bin 0 -> 36025 bytes verification/test_solver_spa.js | 122 ++++++++++++ verification/verify_spa_frontend.py | 95 ++++++++++ 7 files changed, 409 insertions(+), 107 deletions(-) create mode 100644 verification/spa_failed.png create mode 100644 verification/spa_missing_courses.png create mode 100644 verification/test_solver_spa.js create mode 100644 verification/verify_spa_frontend.py diff --git a/dist/index.html b/dist/index.html index 16cbb35..906634f 100644 --- a/dist/index.html +++ b/dist/index.html @@ -283,6 +283,10 @@

第 {{ currentScheduleIdx + 1 }} 方案课表

周平均学时: {{ schedules[currentScheduleIdx].stats?.avg_weekly_hours }} 活跃周次: {{ schedules[currentScheduleIdx].stats?.week_span }} +
+ ⚠️ 未排课程 (冲突): {{ schedules[currentScheduleIdx].missing_course_names.join(', ') }} +
评分详情: 基准分: 100 diff --git a/dist/static/js/solver.js b/dist/static/js/solver.js index c8d960b..0e14762 100644 --- a/dist/static/js/solver.js +++ b/dist/static/js/solver.js @@ -164,20 +164,6 @@ class ScheduleSolver { }); } - static coursesConflict(courseA, courseB) { - const bmpA = ScheduleSolver.parseBitmap(courseA.schedule_bitmaps); - const bmpB = ScheduleSolver.parseBitmap(courseB.schedule_bitmaps); - const length = Math.min(bmpA.length, bmpB.length); - - for (let w = 1; w < length; w++) { - if ((bmpA[w] & bmpB[w]) !== 0n) { - return true; - } - } - return false; - } - - // Check conflicts but return details (simplified for JS port, mostly for frontend use if needed) static coursesConflictWithDetails(courseA, courseB) { const bmpA = ScheduleSolver.parseBitmap(courseA.schedule_bitmaps); const bmpB = ScheduleSolver.parseBitmap(courseB.schedule_bitmaps); @@ -231,53 +217,63 @@ class ScheduleSolver { } if (allConflict) { - conflicts.push({ group1: i, group2: j, reason: firstReason || "Unknown" }); + const nameA = activeA[0].name || `Group ${i}`; + const nameB = activeB[0].name || `Group ${j}`; + return { error: `检测到绝对冲突: [${nameA}] 与 [${nameB}] 无法同时选择 (冲突原因: ${firstReason})` }; } } } - return conflicts; + return null; } static generateSchedules(groups, preferences) { const maxResults = 20; preferences = preferences || {}; - // 0. Preprocess: Merge Groups + // 0. Preprocess: Filter Empty Groups & Merge const mergedGroupsMap = new Map(); - for (const g of groups) { - const candidates = g.candidates || []; - if (candidates.length === 0) continue; + // Use filtered groups for logic + const nonEmptyGroups = groups.filter(g => g.candidates && g.candidates.some(c => c.selected)); - let courseName = candidates[0].name; + if (nonEmptyGroups.length === 0) { + return { schedules: [], total_found: 0 }; + } + + // 1. Check for Absolute Conflicts First + const conflictErr = ScheduleSolver.checkConflicts(nonEmptyGroups); + if (conflictErr) { + return { schedules: [], total_found: 0, error: conflictErr.error }; + } + + // 2. Prepare Meta-Groups + for (const g of nonEmptyGroups) { + const active = g.candidates.filter(c => c.selected); + let courseName = active[0].name; if (!courseName) courseName = `__ID_${g.id}__`; if (!mergedGroupsMap.has(courseName)) { mergedGroupsMap.set(courseName, { id: g.id, - candidates: candidates.filter(c => c.selected) + name: courseName, + candidates: active }); } else { const existing = mergedGroupsMap.get(courseName); - const newActive = candidates.filter(c => c.selected); - existing.candidates = existing.candidates.concat(newActive); + existing.candidates = existing.candidates.concat(active); } } - const processedGroups = Array.from(mergedGroupsMap.values()); - - // 1. Meta-Candidates const metaGroups = []; - for (const g of processedGroups) { + for (const g of mergedGroupsMap.values()) { const active = g.candidates; - if (active.length === 0) return { schedules: [], total_found: 0, error: "One of the groups has no selected candidates." }; + // Cluster by time slots (bitmaps) const clusters = new Map(); for (const c of active) { const rawBm = c.schedule_bitmaps || []; const intBm = ScheduleSolver.parseBitmap(rawBm); - // Create key from bitmap content - const key = intBm.join(','); // Array to string key + const key = intBm.join(','); if (!clusters.has(key)) { clusters.set(key, { bitmaps: intBm, list: [] }); @@ -294,58 +290,131 @@ class ScheduleSolver { }); } - // Sort by density (fewest bits set) + // Sort by density (heuristic: fewer classes first might leave more room? or opposite?) + // Just stick to density sort metaCandidates.sort((a, b) => { const countA = a.bitmaps.reduce((acc, val) => acc + ScheduleRanker.countSetBits(val), 0); const countB = b.bitmaps.reduce((acc, val) => acc + ScheduleRanker.countSetBits(val), 0); return countA - countB; }); - metaGroups.push(metaCandidates); + metaGroups.push({ + name: g.name, + candidates: metaCandidates + }); } - // Sort groups by size (MRV) - metaGroups.sort((a, b) => a.length - b.length); + // Sort groups by size (MRV - Minimum Remaining Values) to fail fast + metaGroups.sort((a, b) => a.candidates.length - b.candidates.length); - // 2. DFS - const topNHeap = []; // Array of {score, schedule} + const totalGroupsCount = metaGroups.length; + + // 3. Max-Subset DFS + const topNHeap = []; + let maxCoursesFound = 0; let totalFound = 0; const currentBitmap = Array(30).fill(0n); function backtrack(groupIdx, currentScheduleMeta) { - if (groupIdx === metaGroups.length) { - totalFound++; - - // Reconstruct - const finalSchedule = currentScheduleMeta.map(m => { - const rep = { ...m.representative }; // Shallow copy - rep.alternatives = m.alternatives; - return rep; - }); + const scheduledCount = currentScheduleMeta.length; + + // Base Case: All groups processed + if (groupIdx === totalGroupsCount) { + // We reached a leaf. Update maxCoursesFound + if (scheduledCount > maxCoursesFound) { + maxCoursesFound = scheduledCount; + // Clear heap because we found a better size? + // Usually we prefer larger schedules over higher scores of smaller schedules. + // Yes: "must arrange as many courses as possible first" + topNHeap.length = 0; + } - const score = ScheduleRanker.scoreSchedule(finalSchedule, preferences); - const entry = { score, schedule: finalSchedule }; + if (scheduledCount === maxCoursesFound) { + totalFound++; + + // Reconstruct + const finalSchedule = currentScheduleMeta.map(m => { + const rep = { ...m.representative }; + rep.alternatives = m.alternatives; + return rep; + }); + + // Missing Groups + const presentNames = new Set(finalSchedule.map(c => c.name)); + const missingNames = []; + for(const mg of metaGroups) { + if (!presentNames.has(mg.name)) { + missingNames.push(mg.name); + } + } - if (topNHeap.length < maxResults) { - topNHeap.push(entry); - topNHeap.sort((a, b) => a.score - b.score); // Ascending order (min-heap like) - } else if (score > topNHeap[0].score) { - topNHeap[0] = entry; - topNHeap.sort((a, b) => a.score - b.score); + const missingCount = missingNames.length; + + // Score + const evalResult = ScheduleRanker.evaluateSchedule(finalSchedule, preferences); + let score = evalResult.score; + + // Apply Missing Penalty + const penalty = missingCount * 10.0; + score -= penalty; + + const entry = { + score, + schedule: finalSchedule, + missingNames, + missingCount, + details: evalResult.details + }; + // Add missing penalty to details for display + if (missingCount > 0) { + entry.details['缺课惩罚'] = -penalty; + } + + if (topNHeap.length < maxResults) { + topNHeap.push(entry); + topNHeap.sort((a, b) => a.score - b.score); + } else if (score > topNHeap[0].score) { + topNHeap[0] = entry; + topNHeap.sort((a, b) => a.score - b.score); + } } return; } - // Pruning (Optional) - /*if (topNHeap.length === maxResults) { - const partialSched = currentScheduleMeta.map(m => m.representative); - const partialScore = ScheduleRanker.scoreSchedule(partialSched, preferences); - if (partialScore < topNHeap[0].score - 50) return; - }*/ + // Pruning: Calculate Future Potential + // Count how many future groups have AT LEAST ONE candidate compatible with currentBitmap + let compatibleFuture = 0; + for (let i = groupIdx; i < totalGroupsCount; i++) { + const group = metaGroups[i]; + let canFit = false; + for (const cand of group.candidates) { + // Check compatibility + let ok = true; + const limit = Math.min(cand.bitmaps.length, currentBitmap.length); + for (let w = 1; w < limit; w++) { + if ((cand.bitmaps[w] & currentBitmap[w]) !== 0n) { + ok = false; + break; + } + } + if (ok) { + canFit = true; + break; + } + } + if (canFit) compatibleFuture++; + } - const candidates = metaGroups[groupIdx]; + if (scheduledCount + compatibleFuture < maxCoursesFound) { + // Cannot possibly beat the best found size + return; + } - for (const meta of candidates) { + // Branch 1: Try to pick a candidate from current group + const currentGroup = metaGroups[groupIdx]; + let pickedSomething = false; + + for (const meta of currentGroup.candidates) { const metaBmp = meta.bitmaps; // Check Conflict @@ -359,57 +428,55 @@ class ScheduleSolver { } if (!isValid) continue; - //forward check - let futureIsDead = false; - - for (let nextG = groupIdx + 1; nextG < metaGroups.length; nextG++) { - let nextCandidate = false; - - for (const nextMeta of metaGroups[nextG]) { - let conflictFound = false; - const limitF = Math.min(nextMeta.bitmaps.length, currentBitmap.length); - // Apply - for (let w = 1; w < limitF; w++) { - if (((currentBitmap[w] | metaBmp[w]) & nextMeta.bitmaps[w]) !== 0n) { - conflictFound = true; - break; - } - } - if (!conflictFound) { - nextCandidate = true; - break; - } - } - if (!nextCandidate) { - futureIsDead = true; - break; - } - } - - if (futureIsDead) continue; + // Optimization: Future Lookahead (Dead End Check) + // If picking this candidate reduces future compatible groups such that we can't reach maxCoursesFound? + // This is expensive to check fully, but let's do a light check if needed. + // The main pruning above handles the "Skip" branch logic. + // Here we just proceed. currentScheduleMeta.push(meta); + // Update bitmap + for (let w = 1; w < limit; w++) { + currentBitmap[w] |= metaBmp[w]; + } backtrack(groupIdx + 1, currentScheduleMeta); - // Undo + // Backtrack currentScheduleMeta.pop(); for (let w = 1; w < limit; w++) { - currentBitmap[w] ^= metaBmp[w]; // XOR to unset + currentBitmap[w] ^= metaBmp[w]; // Unset } + pickedSomething = true; } + + // Branch 2: Skip this group (allow missing) + // We only skip if we HAVE to? No, user might want to skip a specific group to fit others? + // "Must arrange as many courses as possible". + // If we found valid candidates above (pickedSomething), we usually don't want to skip THIS group *unless* skipping it allows us to pick MORE future groups. + // But if we explored all valid candidates above, `backtrack` recursed. + // Do we also explore the "Skip" branch? + // Yes, because maybe skipping this one (even if it fits) allows 2 others to fit later. + // However, if we picked a candidate, `scheduledCount` increased. + // If we skip, `scheduledCount` stays same. + // We should always explore skipping unless we are sure we don't need to. + // Given the complexity, let's explore skipping. + // BUT to optimize: if we successfully picked a candidate, we might assume it's better than skipping IF conflicts are rare. + // With "Absolute Conflict" already handled, conflicts are subtler. + // To ensure GLOBAL maximum, we must allow skipping. + + backtrack(groupIdx + 1, currentScheduleMeta); } backtrack(0, []); - // Sort descending by score for output + // Sort descending const sortedResults = topNHeap.sort((a, b) => b.score - a.score); - // Enrich results with stats + // Map to final format const finalSchedules = sortedResults.map(item => { const sched = item.schedule; - const evalResult = ScheduleRanker.evaluateSchedule(sched, preferences); let totalCredits = 0; let totalHours = 0; @@ -423,20 +490,18 @@ class ScheduleSolver { } }); - // Calculate span const weeks = Array.from(weekSet).sort((a,b)=>a-b); let weekSpan = ""; if (weeks.length > 0) { - // simple grouping - // ... logic to format 1-16 etc. - // Just using start-end for now weekSpan = `${weeks[0]}-${weeks[weeks.length-1]}`; } return { score: item.score, - score_details: evalResult.details, + score_details: item.details, courses: sched, + missing_course_names: item.missingNames, + missing_groups: [], // user didn't ask for IDs, just names in UI. stats: { total_credits: totalCredits, total_hours: totalHours, @@ -453,7 +518,7 @@ class ScheduleSolver { } } -// Export for module use or browser global +// Export if (typeof window !== 'undefined') { window.Solver = ScheduleSolver; window.Ranker = ScheduleRanker; diff --git a/server.log b/server.log index 9014859..56c17b2 100644 --- a/server.log +++ b/server.log @@ -1,6 +1,22 @@ -127.0.0.1 - - [10/Jan/2026 14:12:32] "GET /index.html HTTP/1.1" 200 - -127.0.0.1 - - [10/Jan/2026 14:12:32] "GET /static/vue.global.js HTTP/1.1" 200 - -127.0.0.1 - - [10/Jan/2026 14:12:32] "GET /static/html2canvas.min.js HTTP/1.1" 200 - -127.0.0.1 - - [10/Jan/2026 14:12:32] "GET /static/js/solver.js HTTP/1.1" 200 - -127.0.0.1 - - [10/Jan/2026 14:12:32] "GET /static/js/app.js HTTP/1.1" 200 - -127.0.0.1 - - [10/Jan/2026 14:12:33] "GET /functions/search?name=%E6%95%B0%E5%AD%A6&code=&campus=1&semester=2025-2026-1&match_mode=OR HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:36:31] "GET / HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:36:31] "GET /static/js/app.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:36:31] "GET /static/js/solver.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:36:31] "GET /static/vue.global.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:36:31] "GET /static/html2canvas.min.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:36:31] code 404, message File not found +127.0.0.1 - - [11/Jan/2026 04:36:31] "GET /search?name=&code=&campus=1&semester=2025-2026-2&match_mode=OR HTTP/1.1" 404 - +127.0.0.1 - - [11/Jan/2026 04:37:37] "GET / HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:37:37] "GET /static/vue.global.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:37:37] "GET /static/js/app.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:37:37] "GET /static/js/solver.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:37:37] "GET /static/html2canvas.min.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:42:04] "GET / HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:42:04] "GET /static/vue.global.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:42:04] "GET /static/js/solver.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:42:04] "GET /static/js/app.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:42:04] "GET /static/html2canvas.min.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:43:33] "GET / HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:43:33] "GET /static/vue.global.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:43:33] "GET /static/html2canvas.min.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:43:33] "GET /static/js/app.js HTTP/1.1" 200 - +127.0.0.1 - - [11/Jan/2026 04:43:33] "GET /static/js/solver.js HTTP/1.1" 200 - diff --git a/verification/spa_failed.png b/verification/spa_failed.png new file mode 100644 index 0000000000000000000000000000000000000000..166a50c3c47a196a6fa5caefbb5698d068b7ab7b GIT binary patch literal 29408 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 aBp|Fn!RSQ7=ObMI74NCt&A(&v;(r0!ntvq# literal 0 HcmV?d00001 diff --git a/verification/spa_missing_courses.png b/verification/spa_missing_courses.png new file mode 100644 index 0000000000000000000000000000000000000000..815928b452d5c78c1865619bf351b8269861195a GIT binary patch literal 36025 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 { + // weekBits: array of 26 ints. + // We'll just set week 1 + const b = Array(30).fill(0n); + b[1] = BigInt(weekBits); + return { + name, + schedule_bitmaps: b.map(x=>x.toString()), // Solver expects strings often or BigInts + selected: true, + bitmaps: b // Mock what solver expects inside (wait, solver parses it from schedule_bitmaps) + }; + }; + + // Case 1: No Conflicts + { + console.log("Test Case 1: No Conflicts"); + const g1 = { id: 1, candidates: [createCandidate('A', 1)] }; + const g2 = { id: 2, candidates: [createCandidate('B', 2)] }; + const res = ScheduleSolver.generateSchedules([g1, g2], {}); + + if (res.error) { + console.error("FAIL: Unexpected error", res.error); + failed = true; + } else if (res.schedules.length === 0) { + console.error("FAIL: No schedules found"); + failed = true; + } else if (res.schedules[0].courses.length !== 2) { + console.error("FAIL: Expected 2 courses, got", res.schedules[0].courses.length); + failed = true; + } else if (res.schedules[0].missing_course_names && res.schedules[0].missing_course_names.length > 0) { + console.error("FAIL: Unexpected missing courses"); + failed = true; + } else { + console.log("PASS"); + } + } + + // Case 2: Absolute Conflict + { + console.log("Test Case 2: Absolute Conflict"); + const g1 = { id: 1, candidates: [createCandidate('A', 1)] }; + const g2 = { id: 2, candidates: [createCandidate('B', 1)] }; + const res = ScheduleSolver.generateSchedules([g1, g2], {}); + + if (res.error && res.error.includes("绝对冲突")) { + console.log("PASS: Caught absolute conflict:", res.error); + } else { + console.error("FAIL: Expected absolute conflict error, got:", res); + failed = true; + } + } + + // Case 3: Partial Conflict (Maximization) + { + console.log("Test Case 3: Partial Conflict (Maximization)"); + const g1 = { id: 1, candidates: [createCandidate('A1', 1), createCandidate('A2', 2)], name: "GroupA" }; + const g2 = { id: 2, candidates: [createCandidate('B', 1)], name: "GroupB" }; + const g3 = { id: 3, candidates: [createCandidate('C', 2)], name: "GroupC" }; + + // Ensure candidates have selected=true + g1.candidates.forEach(c => c.selected = true); + g2.candidates.forEach(c => c.selected = true); + g3.candidates.forEach(c => c.selected = true); + + const res = ScheduleSolver.generateSchedules([g1, g2, g3], {}); + + if (res.error) { + console.error("FAIL: Unexpected error in partial case", res.error); + failed = true; + } else if (res.schedules.length === 0) { + console.error("FAIL: No schedules found"); + failed = true; + } else { + const top = res.schedules[0]; + console.log("Top Schedule Score:", top.score); + console.log("Top Schedule Courses:", top.courses.length); + console.log("Missing:", top.missing_course_names); + + if (top.courses.length !== 2) { + console.error("FAIL: Expected 2 courses, got", top.courses.length); + failed = true; + } + if (!top.missing_course_names || top.missing_course_names.length !== 1) { + console.error("FAIL: Expected 1 missing course"); + failed = true; + } + // Check penalty + // Score starts at 100. + // Missing 1 course = -10. + // Expected score <= 90. + if (top.score > 90.0001) { + console.error("FAIL: Score too high, penalty not applied?"); + failed = true; + } else { + console.log("PASS"); + } + } + } + + if (failed) process.exit(1); +} + +runTest(); diff --git a/verification/verify_spa_frontend.py b/verification/verify_spa_frontend.py new file mode 100644 index 0000000..28834fc --- /dev/null +++ b/verification/verify_spa_frontend.py @@ -0,0 +1,95 @@ +from playwright.sync_api import sync_playwright + +def verify_missing_courses_display(page): + page.on("console", lambda msg: print(f"Browser Console: {msg.text}")) + page.goto("http://localhost:8080") + + # Mock Search + # Group A: 2 Options. Mon 1-2 (conflicts with B), Tue 1-2 (conflicts with C) + page.route("**/search?*name=A*", lambda route: route.fulfill( + status=200, + content_type="application/json", + body='[' + + '{"name": "A", "code": "001", "teacher": "T1a", "location_text": "Mon 1-2", "checked": false, "schedule_bitmaps": ["3","3"]},' + + '{"name": "A", "code": "001", "teacher": "T1b", "location_text": "Tue 1-2", "checked": false, "schedule_bitmaps": ["12","12"]}' + + ']' + )) + # Group B: Mon 1-2 (conflicts with A1) + page.route("**/search?*name=B*", lambda route: route.fulfill( + status=200, + content_type="application/json", + body='[{"name": "B", "code": "002", "teacher": "T2", "location_text": "Mon 1-2", "checked": false, "schedule_bitmaps": ["3","3"]}]' + )) + # Group C: Tue 1-2 (conflicts with A2) + page.route("**/search?*name=C*", lambda route: route.fulfill( + status=200, + content_type="application/json", + body='[{"name": "C", "code": "003", "teacher": "T3", "location_text": "Tue 1-2", "checked": false, "schedule_bitmaps": ["12","12"]}]' + )) + + # Add A (Both options) + page.fill('input[placeholder*="课程名"]', 'A') + page.click("button:has-text('搜索')") + page.wait_for_selector("text=结果 (2)") + # Select all + page.click("text=全选/反选") + page.click("button:has-text('将选中项存为一组')") + + # Add B + page.fill('input[placeholder*="课程名"]', 'B') + page.click("button:has-text('搜索')") + page.wait_for_selector("text=结果 (1)") + page.click("input[type='checkbox']") + page.click("button:has-text('将选中项存为一组')") + + # Add C + page.fill('input[placeholder*="课程名"]', 'C') + page.click("button:has-text('搜索')") + page.wait_for_selector("text=结果 (1)") + page.click("input[type='checkbox']") + page.click("button:has-text('将选中项存为一组')") + + # Go to Planning + page.click("text=2. 规划 & 策略") + page.wait_for_selector("text=我的课程组 (3)") + + # Generate + print("Clicking Generate...") + page.click("button:has-text('生成课表方案')") + + # Check for toast + try: + toast = page.wait_for_selector(".toast", state="visible", timeout=2000) + if toast: + print(f"Toast detected: {toast.inner_text()}") + except: + pass + + # Wait for result + try: + page.wait_for_selector("text=推荐方案", timeout=5000) + print("Results generated successfully.") + except: + print("Failed to reach results view.") + page.screenshot(path="verification/spa_failed.png") + return + + # Check warning + # We expect some course to be missing (either A, B, or C) + warning_locator = page.locator("text=未排课程 (冲突):") + if warning_locator.is_visible(): + print("PASS: Warning is visible") + print(warning_locator.text_content()) + else: + print("FAIL: Warning not found") + + page.screenshot(path="verification/spa_missing_courses.png") + +if __name__ == "__main__": + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page() + try: + verify_missing_courses_display(page) + finally: + browser.close()