diff --git a/backend/ranker.py b/backend/ranker.py index 8647202..e2a926c 100644 --- a/backend/ranker.py +++ b/backend/ranker.py @@ -2,18 +2,14 @@ class ScheduleRanker: @staticmethod - def score_schedule(schedule, preferences): + def evaluate_schedule(schedule, preferences): """ - Scores a schedule based on preferences. - Lower score is better? Or Higher? Let's say Higher is better. - - Preferences: - - avoid_early_morning (bool): 1-2节 - - avoid_weekend (bool): Sat/Sun - - compactness (str): "high" (bunch together), "low" (spread out), "none" - - max_daily_load (int): Penalty if > X + Evaluates a schedule based on preferences and returns score + breakdown. """ - score = 100.0 + base_score = 100.0 + details = {} + total_penalty = 0.0 + total_bonus = 0.0 # Merge bitmaps full_bitmap = [0] * 30 # Assume max weeks 25 @@ -29,14 +25,12 @@ def score_schedule(schedule, preferences): val = 0 full_bitmap[w] |= val - # 1. Avoid Early Morning (Nodes 1-2 -> Bits 0-1, 13-14, etc.) + # 1. Avoid Early Morning if preferences.get('avoid_early_morning'): penalty = 0 - for w in range(1, 26): # Check typical weeks + for w in range(1, 26): mask = full_bitmap[w] if mask == 0: continue - # Early morning mask: Day 0..6, Nodes 0..1 - # Bit pos = day*13 + node early_mask = 0 for d in range(7): early_mask |= (1 << (d * 13 + 0)) @@ -44,14 +38,15 @@ def score_schedule(schedule, preferences): if (mask & early_mask): penalty += 1 - score -= penalty * 2 # 严格惩罚早八 - # 2. Avoid Weekend (Days 5, 6) + p_val = penalty * 2 + total_penalty += p_val + details['早八回避'] = -p_val + + # 2. Avoid Weekend if preferences.get('avoid_weekend'): penalty = 0 weekend_mask = 0 - # Day 5 (Sat), Day 6 (Sun). All 13 nodes. - # Bits 65-77 (Sat), 78-90 (Sun) ... wait 5*13=65. for node in range(13): weekend_mask |= (1 << (5 * 13 + node)) weekend_mask |= (1 << (6 * 13 + node)) @@ -59,14 +54,13 @@ def score_schedule(schedule, preferences): for w in range(1, 26): if (full_bitmap[w] & weekend_mask): penalty += 1 - score -= penalty * 2.0 # Higher penalty for weekends - # 3. Compactness (Variance of start times? Or density?) - # Let's use daily variance. - # Compact -> High density, few gaps. - # Spread -> Evenly distributed. + p_val = penalty * 2.0 + total_penalty += p_val + details['周末回避'] = -p_val + + # 3. Compactness if preferences.get('compactness') in ['high', 'low']: - # Calculate gaps total_gaps = 0 for w in range(1, 26): mask = full_bitmap[w] @@ -75,10 +69,6 @@ def score_schedule(schedule, preferences): day_bits = (mask >> (d * 13)) & 0x1FFF if day_bits == 0: continue - # Convert to string binary to find 101 patterns (gap) - bin_str = bin(day_bits)[2:].zfill(13) # LSB is 1st class? - # Actually LSB is node 0 (1st class). `bin` output is MSB left. - # Let's just iterate has_started = False gap_count = 0 current_gap = 0 @@ -96,9 +86,13 @@ def score_schedule(schedule, preferences): total_gaps += gap_count if preferences['compactness'] == 'high': - score -= total_gaps * 0.2 # Penalty for gaps + p_val = total_gaps * 0.2 + total_penalty += p_val + details['课程紧凑'] = -p_val else: - score += total_gaps * 0.2 # Bonus for gaps (spread) + b_val = total_gaps * 0.2 + total_bonus += b_val + details['课程分散'] = +b_val # 4. Max Daily Load limit = preferences.get('max_daily_load') @@ -109,40 +103,43 @@ def score_schedule(schedule, preferences): if mask == 0: continue for d in range(7): day_bits = (mask >> (d * 13)) & 0x1FFF - # Count set bits count = bin(day_bits).count('1') if count > limit: overload += (count - limit) - score -= overload * 5.0 # Heavy penalty + p_val = overload * 5.0 + total_penalty += p_val + details['每日负载'] = -p_val - # 5. Day Max Limit (Specific Days) + # 5. Day Max Limit if preferences.get('day_max_limit_enabled'): limit = preferences.get('day_max_limit_value', 4) - target_days = preferences.get('day_max_limit_days', []) # List of bools, idx 0=Mon - - penalty = 0 - # Ensure target_days has 7 elements + target_days = preferences.get('day_max_limit_days', []) if len(target_days) < 7: target_days = target_days + [False] * (7 - len(target_days)) + penalty = 0 for w in range(1, 26): mask = full_bitmap[w] if mask == 0: continue - for d in range(7): - # Check if this day is selected for limiting - if not target_days[d]: - continue - + if not target_days[d]: continue day_bits = (mask >> (d * 13)) & 0x1FFF count = bin(day_bits).count('1') - if count > limit: - # Penalty calculation - # If limit is 0 (day off), any class is bad. diff = count - limit - penalty += diff * 50.0 # Very heavy penalty + penalty += diff * 50.0 - score -= penalty + p_val = penalty + total_penalty += p_val + details['特定日限制'] = -p_val - return score + final_score = base_score + total_bonus - total_penalty + return { + 'score': final_score, + 'details': details + } + + @staticmethod + def score_schedule(schedule, preferences): + result = ScheduleRanker.evaluate_schedule(schedule, preferences) + return result['score'] diff --git a/backend/solver.py b/backend/solver.py index ddea52d..517238d 100644 --- a/backend/solver.py +++ b/backend/solver.py @@ -198,6 +198,7 @@ def generate_schedules(groups, max_results=20, preferences=None): current_bitmap = [0] * 30 counter = itertools.count() + total_found_container = [0] # Pre-calculate group order? # Heuristic: Process groups with FEWEST options first (Fail Fast). @@ -216,6 +217,8 @@ def generate_schedules(groups, max_results=20, preferences=None): def backtrack(group_idx, current_schedule_meta): if group_idx == len(meta_groups): # Found a valid schedule + total_found_container[0] += 1 + # Reconstruct final schedule but include alternatives info final_schedule = [] for m in current_schedule_meta: @@ -281,7 +284,7 @@ def backtrack(group_idx, current_schedule_meta): backtrack(0, []) sorted_results = sorted(top_n_heap, key=lambda x: x[0], reverse=True) - return [item[2] for item in sorted_results] + return [item[2] for item in sorted_results], total_found_container[0] @staticmethod def is_valid_combination(courses): diff --git a/jwFetcher.py b/jwFetcher.py index 35e7795..bbde736 100644 --- a/jwFetcher.py +++ b/jwFetcher.py @@ -338,10 +338,23 @@ def search(self, course_name=None, course_code=None, campus="1", semester="2025- # 调用正则解析器 bitmap, sessions = ScheduleBitmapper.generate_bitmap(raw_loc) + # Try to extract credit (XF) and hours (XS) + try: + credit = float(row.get("XF", 0)) + except: + credit = 0.0 + + try: + hours = float(row.get("XS", 0)) + except: + hours = 0.0 + item = { "name": row.get("KCM"), "code": row.get("KCH"), "teacher": teacher, + "credit": credit, + "hours": hours, "location_text": raw_loc, "school": row.get("PKDWDM_DISPLAY") or row.get("KKDWDM_DISPLAY"), # 输出二进制列表 (核心) diff --git a/main.py b/main.py index 743e858..3232ef7 100644 --- a/main.py +++ b/main.py @@ -56,20 +56,126 @@ def generate_schedules(self, groups, preferences): # 2. Generate # Pass preferences to solver for DFS pruning/ordering - raw_schedules = ScheduleSolver.generate_schedules(groups, preferences=preferences) - print(f"[Api] Found {len(raw_schedules)} valid schedules") + raw_schedules, total_count = ScheduleSolver.generate_schedules(groups, preferences=preferences) + print(f"[Api] Found {len(raw_schedules)} top schedules (from {total_count} total explored)") - # 3. Rank + # 3. Rank and Enrich ranker = ScheduleRanker() ranked = [] for s in raw_schedules: - score = ranker.score_schedule(s, preferences) - ranked.append({'courses': s, 'score': score}) + eval_result = ranker.evaluate_schedule(s, preferences) + score = eval_result['score'] + details = eval_result['details'] + + # Calculate stats + total_credits = 0.0 + total_hours = 0 + + # Find week span + min_week = 999 + max_week = -1 + has_classes = False + + for course in s: + total_credits += course.get('credit', 0) + + # Track active weeks for this course + course_weeks = set() + + # Try getting from sessions first + sessions = course.get('sessions', []) + if sessions: + for sess in sessions: + if sess['weeks']: + course_weeks.update(sess['weeks']) + else: + # Fallback: scan schedule_bitmaps + # index 1..len + bitmaps = course.get('schedule_bitmaps', []) + for w_idx in range(1, len(bitmaps)): + val = bitmaps[w_idx] + if isinstance(val, str): + try: + val = int(val) + except: val = 0 + if val > 0: + course_weeks.add(w_idx) + + # Update global stats + if course_weeks: + has_classes = True + min_week = min(min_week, min(course_weeks)) + max_week = max(max_week, max(course_weeks)) + + # Use official hours if available, else calculate + if course.get('hours', 0) > 0: + total_hours += course.get('hours') + else: + # Calculate hours from sessions if available + if sessions: + for sess in sessions: + p_len = sess['end'] - sess['start'] + 1 + w_len = len(sess['weeks']) + total_hours += (p_len * w_len) + else: + # Fallback: Count bits in bitmaps for active weeks + bitmaps = course.get('schedule_bitmaps', []) + for w in course_weeks: + if w < len(bitmaps): + val = bitmaps[w] + if isinstance(val, str): + val = int(val) + total_hours += bin(val).count('1') + + avg_weekly = 0.0 + if has_classes and max_week >= min_week: + span = max_week - min_week + 1 + if span > 0: + avg_weekly = total_hours / span + + ranked.append({ + 'courses': s, + 'score': score, + 'score_details': details, + 'stats': { + 'total_credits': round(total_credits, 1), + 'total_hours': total_hours, + 'avg_weekly_hours': round(avg_weekly, 1), + 'week_span': f"{min_week}-{max_week}" if has_classes else "N/A" + } + }) # Sort desc ranked.sort(key=lambda x: x['score'], reverse=True) - return {'schedules': ranked} + return {'schedules': ranked, 'total_found': total_count} + + def save_image_dialog(self, base64_data): + import base64 + try: + active_window = webview.windows[0] + file_path = active_window.create_file_dialog( + webview.SAVE_DIALOG, + directory='', + save_filename='schedule.png', + file_types=('PNG Image (*.png)', 'All files (*.*)') + ) + + if file_path: + if isinstance(file_path, (list, tuple)): + file_path = file_path[0] + + # Remove header if present + if ',' in base64_data: + base64_data = base64_data.split(',')[1] + + with open(file_path, "wb") as f: + f.write(base64.b64decode(base64_data)) + return True + return False + except Exception as e: + print(f"[Api] Save Image Error: {e}") + return False def save_session(self, groups_json, prefs_json): try: diff --git a/reproduce.py b/reproduce.py index 45c3007..0483609 100644 --- a/reproduce.py +++ b/reproduce.py @@ -23,8 +23,8 @@ def test_duplication_repro(): print(f"Loaded {len(groups)} groups.") # Run solver - schedules = ScheduleSolver.generate_schedules(groups, max_results=10) - print(f"Generated {len(schedules)} schedules.") + schedules, total_count = ScheduleSolver.generate_schedules(groups, max_results=10) + print(f"Generated {len(schedules)} schedules (Total found: {total_count}).") if not schedules: print("No schedules found! Checking if candidates are selected...") diff --git a/server.log b/server.log new file mode 100644 index 0000000..03a5997 --- /dev/null +++ b/server.log @@ -0,0 +1,24 @@ +127.0.0.1 - - [10/Jan/2026 07:22:19] "GET /static/index.html HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:22:19] "GET /static/vue.global.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:22:19] "GET /static/app.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:22:19] "GET /static/html2canvas.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:23:49] "GET /static/index.html HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:23:49] "GET /static/app.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:23:49] "GET /static/html2canvas.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:23:49] "GET /static/vue.global.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:24:21] "GET /static/index.html HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:24:21] "GET /static/app.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:24:21] "GET /static/vue.global.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:24:21] "GET /static/html2canvas.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:24:50] "GET /static/index.html HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:24:50] "GET /static/vue.global.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:24:50] "GET /static/app.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:24:50] "GET /static/html2canvas.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:32:44] "GET /static/index.html HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:32:44] "GET /static/vue.global.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:32:44] "GET /static/app.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:32:44] "GET /static/html2canvas.min.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:41:07] "GET /static/index.html HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:41:07] "GET /static/app.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:41:07] "GET /static/vue.global.js HTTP/1.1" 200 - +127.0.0.1 - - [10/Jan/2026 07:41:07] "GET /static/html2canvas.min.js HTTP/1.1" 200 - diff --git a/static/app.js b/static/app.js index a70bb23..9cda4a2 100644 --- a/static/app.js +++ b/static/app.js @@ -21,6 +21,7 @@ createApp({ const hasSearched = ref(false); const schedules = ref([]); + const totalCount = ref(0); const currentScheduleIdx = ref(0); const currentWeek = ref(1); const toastRef = ref(null); @@ -246,14 +247,26 @@ createApp({ showToast("错误: " + res.error); } else { schedules.value = res.schedules; + totalCount.value = res.total_found; currentView.value = 'results'; currentScheduleIdx.value = 0; } } else { // Mock schedules.value = [ - { score: 95, courses: [groups.value[0].candidates[0]] } + { + score: 95, + score_details: {'早八回避': -2, '课程紧凑': 5}, + stats: { + 'total_credits': 22.5, + 'total_hours': 400, + 'avg_weekly_hours': 25.0, + 'week_span': '1-16' + }, + courses: [groups.value[0].candidates[0]] + } ]; + totalCount.value = 1; currentView.value = 'results'; } } catch (e) { @@ -330,11 +343,23 @@ createApp({ const downloadImage = () => { const el = document.getElementById('capture-area'); if(window.html2canvas) { - window.html2canvas(el).then(canvas => { - const link = document.createElement('a'); - link.download = 'schedule.png'; - link.href = canvas.toDataURL(); - link.click(); + window.html2canvas(el).then(async (canvas) => { + const dataUrl = canvas.toDataURL(); + if (window.pywebview) { + // Use backend dialog + const success = await window.pywebview.api.save_image_dialog(dataUrl); + if (success) { + showToast("图片已保存"); + } else { + showToast("保存取消或失败"); + } + } else { + // Fallback for browser testing + const link = document.createElement('a'); + link.download = 'schedule.png'; + link.href = dataUrl; + link.click(); + } }); } }; @@ -433,7 +458,7 @@ createApp({ return { currentView, loading, searchParams, searchResults, - groups, preferences, schedules, currentScheduleIdx, currentWeek, + groups, preferences, schedules, totalCount, currentScheduleIdx, currentWeek, filterText, hasSearched, filteredSearchResults, doSearch, createGroup, getGroupName, getActiveCount, removeGroup, generateSchedules, getCell, downloadImage, saveSession, newSession, toastRef, diff --git a/static/index.html b/static/index.html index fc4a236..8ca760c 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ body { font-family: "Microsoft YaHei", sans-serif; margin: 0; padding: 0; background: #f5f5f5; color: #333; } .container { max-width: 1200px; margin: 0 auto; padding: 20px; } header { background: #6200ea; color: white; padding: 15px 20px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } - h1 { margin: 0; font-size: 1.2rem; } + h1 { margin: 0; font-size: 1.2rem; display: flex; align-items: baseline; } button { background: #03dac6; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-weight: bold; transition: 0.2s; } button:hover { opacity: 0.9; } button.secondary { background: #ccc; } @@ -55,7 +55,11 @@
-

🐋南哪选课助手 当前组数: {{ groups.length }}

+

+ 🐋南哪选课助手 + 当前组数: {{ groups.length }} + 本app所有内容在本地处理,仅获取必要的cookie,不存储用户密码.所有内容只是算法生成,不作为完全参考. +

@@ -228,13 +232,14 @@

偏好设置

请先生成方案。
-

推荐方案 (共 {{ schedules.length }} 个)

+

推荐方案 (显示 {{ schedules.length }} 个 / 共 {{ totalCount }} 个可能方案)

-
@@ -246,6 +251,24 @@

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

+ +
+
+ 总学分: {{ schedules[currentScheduleIdx].stats?.total_credits }} + 总学时: {{ schedules[currentScheduleIdx].stats?.total_hours }} + 周平均学时: {{ schedules[currentScheduleIdx].stats?.avg_weekly_hours }} + 活跃周次: {{ schedules[currentScheduleIdx].stats?.week_span }} +
+
+ 评分详情: + 基准分: 100 + + {{ key }}: {{ val > 0 ? '+' : ''}}{{ val }} + + = {{ parseInt(schedules[currentScheduleIdx].score) }} +
+
+ diff --git a/tests/saved_sessions/test_session.json b/tests/saved_sessions/test_session.json index ce9924b..a6f0c9c 100644 --- a/tests/saved_sessions/test_session.json +++ b/tests/saved_sessions/test_session.json @@ -1,5 +1,5 @@ { - "timestamp": "2026-01-09T12:26:45.250661", + "timestamp": "2026-01-10T07:40:37.879341", "groups": [ { "id": 1, diff --git a/verification/verification.png b/verification/verification.png new file mode 100644 index 0000000..d9f00d0 Binary files /dev/null and b/verification/verification.png differ diff --git a/verification/verification_count.png b/verification/verification_count.png new file mode 100644 index 0000000..7d17236 Binary files /dev/null and b/verification/verification_count.png differ diff --git a/verification/verify_frontend.py b/verification/verify_frontend.py new file mode 100644 index 0000000..59ff5ba --- /dev/null +++ b/verification/verify_frontend.py @@ -0,0 +1,72 @@ +from playwright.sync_api import sync_playwright + +def verify_frontend(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page() + + # Access the index.html directly from the static server + page.goto("http://localhost:8080/static/index.html") + + # 1. Verify Privacy Text is present + # It's in the header h1 span with specific text + privacy_text = page.locator("header h1 span").nth(1) + if privacy_text.is_visible() and "本app所有内容在本地处理" in privacy_text.inner_text(): + print("[SUCCESS] Privacy text found.") + else: + print("[FAIL] Privacy text not found or incorrect.") + + # 2. Verify Stats Display logic + # We need to simulate having schedules. + # Inject mock data into the Vue app + page.evaluate(""" + const app = document.getElementById('app').__vue_app__._instance; + app.setupState.schedules = [ + { + score: 95, + score_details: {'早八回避': -2, '课程紧凑': 5}, + stats: { + 'total_credits': 22.5, + 'total_hours': 400, + 'avg_weekly_hours': 25.0, + 'week_span': '1-16' + }, + courses: [] + }, + { score: 90, courses: [] } + ]; + app.setupState.currentView = 'results'; + app.setupState.currentScheduleIdx = 0; + """) + + # Wait for render + page.wait_for_timeout(500) + + # Check Stats Section + stats_section = page.locator("#capture-area div").first + content = stats_section.inner_text() + print(f"Stats Content: {content}") + + if "总学分: 22.5" in content and "早八回避: -2" in content: + print("[SUCCESS] Stats and score details displayed.") + else: + print("[FAIL] Stats incorrect.") + + # 3. Verify Schedule Buttons container styles + # Check if flex-shrink: 0 is applied to buttons + # We can check CSS property + buttons_container = page.locator(".card .secondary").first.locator("..") # parent div + # Actually checking if buttons have flex-shrink: 0 + btn = page.locator(".card button").nth(0) # First scheme button? + # The scheme buttons are inside a div with overflow-x: auto + # Selector: div with padding-bottom: 10px > button + scheme_btn = page.locator("button", has_text="方案 1") + + # Take screenshot + page.screenshot(path="verification/verification.png") + print("Screenshot saved.") + + browser.close() + +if __name__ == "__main__": + verify_frontend() diff --git a/verification/verify_frontend_count.py b/verification/verify_frontend_count.py new file mode 100644 index 0000000..9a50fa3 --- /dev/null +++ b/verification/verify_frontend_count.py @@ -0,0 +1,46 @@ +from playwright.sync_api import sync_playwright + +def verify_frontend_count(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page() + + # Access the index.html directly from the static server + page.goto("http://localhost:8080/static/index.html") + page.wait_for_load_state("networkidle") + + # Inject mock data with total_found + page.evaluate(""" + const app = document.getElementById('app').__vue_app__._instance; + app.setupState.schedules = [ + { + score: 95, + score_details: {}, + stats: {}, + courses: [] + } + ]; + app.setupState.totalCount = 1234; + app.setupState.currentView = 'results'; + """) + + page.wait_for_timeout(500) + + # Check Header + # Selector: h3 in .card containing "推荐方案" + header = page.get_by_text("推荐方案 (显示 1 个 / 共 1234 个可能方案)") + + if header.is_visible(): + print("[SUCCESS] Total count displayed in header.") + else: + print("[FAIL] Header text not found.") + # debug + print(page.locator(".card h3").inner_text()) + + page.screenshot(path="verification/verification_count.png") + print("Screenshot saved.") + + browser.close() + +if __name__ == "__main__": + verify_frontend_count() diff --git a/verification/verify_frontend_robust.py b/verification/verify_frontend_robust.py new file mode 100644 index 0000000..844e2c2 --- /dev/null +++ b/verification/verify_frontend_robust.py @@ -0,0 +1,71 @@ +from playwright.sync_api import sync_playwright + +def verify_frontend(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page() + + # Access the index.html directly from the static server + page.goto("http://localhost:8080/static/index.html") + page.wait_for_load_state("networkidle") + + # 1. Verify Privacy Text is present + # Check all spans in h1 + spans = page.locator("header h1 span") + count = spans.count() + print(f"Found {count} spans in header.") + + privacy_found = False + for i in range(count): + txt = spans.nth(i).inner_text() + print(f"Span {i}: {txt}") + if "本app所有内容在本地处理" in txt: + privacy_found = True + + if privacy_found: + print("[SUCCESS] Privacy text found.") + else: + print("[FAIL] Privacy text not found.") + + # 2. Trigger Mock Flow to get to Results + # Search + page.get_by_role("button", name="搜索").click() + page.wait_for_timeout(500) + + # Select first result + page.locator(".result-item input[type='checkbox']").first.click() + + # Create Group + page.get_by_text("将选中项存为一组").click() + + # Go to Planning + page.get_by_text("2. 规划 & 策略").click() + + # Generate + page.get_by_role("button", name="生成课表方案").click() + page.wait_for_timeout(500) + + # Check Stats Section + # It's inside #capture-area + # Look for "总学分" + if page.get_by_text("总学分: 22.5").is_visible(): + print("[SUCCESS] Stats displayed correctly.") + else: + print("[FAIL] Stats not found.") + # Debug content + print(page.locator("#capture-area").inner_text()) + + # Check scrollable container + # We need 20 schedules to test scroll? + # Mock only returns 1. + # But we can verify the CSS class or style. + # The container is the parent of the scheme buttons. + + # Take screenshot + page.screenshot(path="verification/verification.png") + print("Screenshot saved.") + + browser.close() + +if __name__ == "__main__": + verify_frontend()