diff --git a/dist/index.html b/dist/index.html index 69f69bc..7cc7db1 100644 --- a/dist/index.html +++ b/dist/index.html @@ -45,7 +45,8 @@ .timetable { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; } .timetable th, .timetable td { border: 1px solid #ddd; padding: 5px; text-align: center; font-size: 0.85rem; height: 40px; } .timetable th { background: #f9f9f9; } - .cell-active { background-color: #bbdefb; color: #0d47a1; font-size: 0.75rem; border-radius: 2px; height: 100%; width: 100%;} + .cell-active { background-color: #bbdefb; color: #0d47a1; font-size: 0.75rem; border-radius: 2px; height: 100%; width: 100%; display: flex; flex-direction: column; justify-content: center; overflow: hidden; } + .cell-active.gray { background-color: #e0e0e0; color: #757575; opacity: 0.8; } .cell-conflict { background-color: #ffcdd2; color: #b71c1c; } /* Toast */ @@ -271,8 +272,13 @@

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

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

-
- 周次: +
+ +
+ 周次: +
@@ -310,7 +316,7 @@

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

{{ node }}
{{ getCell(currentScheduleIdx, currentWeek, day-1, node-1).name }}
diff --git a/dist/static/js/app.js b/dist/static/js/app.js index 693d638..b10eeb7 100644 --- a/dist/static/js/app.js +++ b/dist/static/js/app.js @@ -23,6 +23,7 @@ createApp({ const totalCount = ref(0); const currentScheduleIdx = ref(0); const currentWeek = ref(1); + const showAllWeeks = ref(false); const toastRef = ref(null); // Import Modal State @@ -141,6 +142,27 @@ createApp({ showImportModal.value = false; }; + const checkForDuplicates = (newCandidates) => { + if (!groups.value || groups.value.length === 0) return true; + const existingNames = new Set(groups.value.map(g => { + const active = g.candidates.find(c => c.selected); + return active ? active.name : (g.candidates[0] ? g.candidates[0].name : ""); + })); + + const duplicates = new Set(); + for (const cand of newCandidates) { + if (existingNames.has(cand.name)) { + duplicates.add(cand.name); + } + } + + if (duplicates.size > 0) { + const names = Array.from(duplicates).join(", "); + return confirm(`检测到重复课程: [${names}] 已在现有课程组中。\n\n重复添加可能导致排课结果混乱。\n是否继续添加?`); + } + return true; + }; + const startBatchImport = async () => { if (!importText.value) return showToast("请粘贴内容", 'error'); isImporting.value = true; @@ -218,11 +240,18 @@ createApp({ }); if (res && res.length > 0) { - // Create Group - const candidates = res.map(c => ({ + const candidates = res.map(c => ({ ...c, - selected: true // Auto select all for imported groups + selected: true })); + + // Check dupe for this specific import item + if (!checkForDuplicates(candidates)) { + failCount++; // User skipped + continue; + } + + // Create Group groups.value.push({ id: Date.now() + i, // unique-ish id open: false, @@ -248,6 +277,11 @@ createApp({ const selectedInSearch = searchResults.value.filter(c => c.checked); if (selectedInSearch.length === 0) return showToast("未选择任何课程", 'error'); + // Pre-check duplicates + if (!checkForDuplicates(selectedInSearch)) { + return; + } + // Copy all search results, map checked to selected const candidates = searchResults.value.map(c => ({ ...c, @@ -309,18 +343,14 @@ createApp({ const bitPos = BigInt(day * 13 + node); const mask = 1n << bitPos; + // 1. Try to find match in CURRENT week for (let c of courses) { - // schedule_bitmaps are strings now let weekMapStr = c.schedule_bitmaps ? c.schedule_bitmaps[week] : "0"; if (!weekMapStr) weekMapStr = "0"; - let weekMap = 0n; - try { - weekMap = BigInt(weekMapStr); - } catch (e) {} + try { weekMap = BigInt(weekMapStr); } catch (e) {} if ((weekMap & mask) !== 0n) { - // Found the course occupying this cell let loc = "未知地点"; if (c.sessions) { const currentPeriod = node + 1; @@ -330,29 +360,71 @@ createApp({ currentPeriod <= s.end && s.weeks.includes(week) ); - if (sess) { - loc = sess.location; - } + if (sess) loc = sess.location; } else { - let text = c.location_text || ""; - text = text.replace(/周[一二三四五六日].+?周(\((单|双)\))?/g, '').trim(); - text = text.replace(/^周[一二三四五六日]\s+/, '').trim(); - - if (!text || text === ',') { - loc = (c.location_text || "").split(' ').pop(); - } else { - loc = text; - } + // Fallback parsing + let text = c.location_text || ""; + text = text.replace(/周[一二三四五六日].+?周(\((单|双)\))?/g, '').trim(); + text = text.replace(/^周[一二三四五六日]\s+/, '').trim(); + if (!text || text === ',') { + loc = (c.location_text || "").split(' ').pop(); + } else { + loc = text; + } } - return { name: c.name, teacher: c.teacher, location: loc, - alternatives: c.alternatives + alternatives: c.alternatives, + isCurrent: true }; } } + + // 2. If not found, and showAllWeeks is ON, check other weeks + if (showAllWeeks.value) { + for (let c of courses) { + // Check sessions first (more accurate) + if (c.sessions) { + const currentPeriod = node + 1; + // Find any session that covers this Day/Node, regardless of week + const sess = c.sessions.find(s => + s.day === day && + currentPeriod >= s.start && + currentPeriod <= s.end + ); + if (sess) { + return { + name: c.name, + teacher: c.teacher, + location: sess.location, + alternatives: c.alternatives, + isCurrent: false // Gray out + }; + } + } + + // Fallback: scan all bitmaps (heavy but necessary if no sessions) + // Skip if sessions existed but didn't match (already handled above) + if (!c.sessions && c.schedule_bitmaps) { + for(let w=1; w { - totalCredits += (c.credit || 0); - totalHours += (c.hours || 0); + if (!countedCourses.has(c.name)) { + totalCredits += (c.credit || 0); + totalHours += (c.hours || 0); + countedCourses.add(c.name); + } if (c.sessions) { c.sessions.forEach(s => s.weeks.forEach(w => weekSet.add(w))); } diff --git a/reproduce_issue2.py b/reproduce_issue2.py new file mode 100644 index 0000000..14bec7a --- /dev/null +++ b/reproduce_issue2.py @@ -0,0 +1,64 @@ + +import re + +WEEKDAY_MAP = {"一": 0, "二": 1, "三": 2, "四": 3, "五": 4, "六": 5, "日": 6, "天": 6} + +class ScheduleBitmapper: + REGEX_PATTERN = re.compile(r"周([一二三四五六日])\s*(\d+)-(\d+)节\s*([0-9,-]+)周") + + @staticmethod + def parse_week_ranges(week_str): + weeks = set() + parts = week_str.split(',') + for part in parts: + if '-' in part: + try: + s, e = map(int, part.split('-')) + weeks.update(range(s, e + 1)) + except: pass + else: + try: + weeks.add(int(part)) + except: pass + return sorted(list(weeks)) + + @staticmethod + def generate_bitmap(location_text, max_weeks=25): + semester_schedule = [0] * (max_weeks + 1) + sessions = [] + + if not location_text: + return [str(x) for x in semester_schedule], sessions + + segments = re.split(r'[,;]', location_text) + + for seg in segments: + print(f"Processing segment: '{seg}'") + matches = list(ScheduleBitmapper.REGEX_PATTERN.finditer(seg)) + if not matches: + print(" No matches found!") + continue + + print(f" Found {len(matches)} matches.") + + location_part = seg + for m in matches: + location_part = location_part.replace(m.group(0), "") + location_part = location_part.replace("(单)", "").replace("(双)", "").strip() + + for match in matches: + day_char, start_node, end_node, week_range_str = match.groups() + print(f" Match: Day={day_char}, Start={start_node}, End={end_node}, Weeks={week_range_str}") + + day_idx = WEEKDAY_MAP.get(day_char, 0) + s_node = int(start_node) + e_node = int(end_node) + active_weeks = ScheduleBitmapper.parse_week_ranges(week_range_str) + + print(f" Active Weeks: {active_weeks}") + + # ... (rest of logic) + +test_string = "周四 9-11节 3-16周 科技馆210、216、301、302、305、308、310、311、313/315/316/318" +print(f"Testing string: {test_string}") +ScheduleBitmapper.generate_bitmap(test_string) diff --git a/verification/planning_view.png b/verification/planning_view.png deleted file mode 100644 index 47a5b6b..0000000 Binary files a/verification/planning_view.png and /dev/null differ diff --git a/verification/search_results.png b/verification/search_results.png deleted file mode 100644 index 1fc51bb..0000000 Binary files a/verification/search_results.png and /dev/null differ diff --git a/verification/search_view.png b/verification/search_view.png deleted file mode 100644 index 942bb5f..0000000 Binary files a/verification/search_view.png and /dev/null differ diff --git a/verification/server.py b/verification/server.py deleted file mode 100644 index 7fb46d0..0000000 --- a/verification/server.py +++ /dev/null @@ -1,89 +0,0 @@ -import http.server -import socketserver -import json -import urllib.parse -import os -import sys - -PORT = 8000 -DIRECTORY = "dist" - -class Handler(http.server.SimpleHTTPRequestHandler): - def __init__(self, *args, **kwargs): - super().__init__(*args, directory=DIRECTORY, **kwargs) - - def do_GET(self): - parsed = urllib.parse.urlparse(self.path) - path = parsed.path - - # Mock Edge Function - if path.startswith("/functions/search"): - self.handle_search(parsed.query) - return - - return super().do_GET() - - def handle_search(self, query_string): - params = urllib.parse.parse_qs(query_string) - campus = params.get('campus', ['1'])[0] - semester = params.get('semester', ['2025-2026-1'])[0] - - # Load JSON data - # Note: In real edge, this is globalThis[key] or fetch - filename = f"nju_courses_{campus}_{semester}.json" - filepath = os.path.join(DIRECTORY, "data", filename) - - if not os.path.exists(filepath): - self.send_error(404, "Data file not found") - return - - try: - with open(filepath, 'r', encoding='utf-8') as f: - data = json.load(f) - except Exception as e: - self.send_error(500, f"Error reading data: {e}") - return - - # Filter Logic - name_param = params.get('name', [''])[0].lower().strip() - code_param = params.get('code', [''])[0].lower().strip() - match_mode = params.get('match_mode', ['OR'])[0] - - results = [] - for item in data: - # Name Filter - if name_param: - keywords = name_param.split() - item_name = (item.get('name') or '').lower() - - if match_mode == 'AND': - if not all(k in item_name for k in keywords): - continue - else: # OR - if not any(k in item_name for k in keywords): - continue - - # Code Filter - if code_param: - if code_param not in (item.get('code') or '').lower(): - continue - - results.append(item) - - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps(results).encode('utf-8')) - -if __name__ == "__main__": - # Ensure dist exists - if not os.path.exists(DIRECTORY): - print(f"Error: {DIRECTORY} directory not found.") - sys.exit(1) - - print(f"Serving {DIRECTORY} at http://localhost:{PORT}") - with socketserver.TCPServer(("", PORT), Handler) as httpd: - try: - httpd.serve_forever() - except KeyboardInterrupt: - print("\nShutting down server.") diff --git a/verification/spa_failed.png b/verification/spa_failed.png deleted file mode 100644 index 166a50c..0000000 Binary files a/verification/spa_failed.png and /dev/null differ diff --git a/verification/spa_missing_courses.png b/verification/spa_missing_courses.png deleted file mode 100644 index 815928b..0000000 Binary files a/verification/spa_missing_courses.png and /dev/null differ diff --git a/verification/test_solver_spa.js b/verification/test_solver_spa.js deleted file mode 100644 index 8990669..0000000 --- a/verification/test_solver_spa.js +++ /dev/null @@ -1,122 +0,0 @@ - -const fs = require('fs'); -const vm = require('vm'); -const path = require('path'); - -// Load Solver -const solverCode = fs.readFileSync(path.join(__dirname, '../dist/static/js/solver.js'), 'utf8'); -const context = { window: {}, module: { exports: {} } }; -vm.runInNewContext(solverCode, context); - -// Try to get from window or module.exports -const ScheduleSolver = context.window.Solver || context.module.exports.ScheduleSolver; -const ScheduleRanker = context.window.Ranker || context.module.exports.ScheduleRanker; - -function runTest() { - console.log("Starting Solver Tests..."); - let failed = false; - - // Helper to create simple course candidate - const createCandidate = (name, weekBits) => { - // 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/timetable_view.png b/verification/timetable_view.png deleted file mode 100644 index c27c31f..0000000 Binary files a/verification/timetable_view.png and /dev/null differ diff --git a/verification/verification.png b/verification/verification.png deleted file mode 100644 index d9f00d0..0000000 Binary files a/verification/verification.png and /dev/null differ diff --git a/verification/verification_count.png b/verification/verification_count.png deleted file mode 100644 index 7d17236..0000000 Binary files a/verification/verification_count.png and /dev/null differ diff --git a/verification/verify_frontend.py b/verification/verify_frontend.py deleted file mode 100644 index 59ff5ba..0000000 --- a/verification/verify_frontend.py +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index 9a50fa3..0000000 --- a/verification/verify_frontend_count.py +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 844e2c2..0000000 --- a/verification/verify_frontend_robust.py +++ /dev/null @@ -1,71 +0,0 @@ -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() diff --git a/verification/verify_spa.py b/verification/verify_spa.py deleted file mode 100644 index d180d3a..0000000 --- a/verification/verify_spa.py +++ /dev/null @@ -1,83 +0,0 @@ -from playwright.sync_api import sync_playwright, expect -import time -import subprocess -import sys -import os - -def verify_frontend(): - # Start the server in background - # We use the python script we created earlier - server_process = subprocess.Popen([sys.executable, "verification/server.py"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - - try: - with sync_playwright() as p: - browser = p.chromium.launch(headless=True) - page = browser.new_page() - - # 1. Navigate to Search View - page.goto("http://localhost:8000/index.html") - - # Wait for Vue to mount - page.wait_for_selector("#app") - - # Verify "Batch Import" button exists on the first page (Search View) - import_btn = page.get_by_role("button", name="批量导入") - expect(import_btn).to_be_visible() - - # Take a screenshot of the search page - page.screenshot(path="verification/search_view.png") - print("Captured search_view.png") - - # 2. Perform a search - name_input = page.get_by_placeholder("课程名 (空格分隔多词)") - name_input.fill("数学") - - search_btn = page.get_by_role("button", name="搜索") - search_btn.click() - - # Wait for results - page.wait_for_selector(".result-item") - - # Take a screenshot of results - page.screenshot(path="verification/search_results.png") - print("Captured search_results.png") - - # 3. Create a Group - # Click "Select All" - select_all_btn = page.get_by_text("全选/反选") - select_all_btn.click() - - # Click "Save as Group" - create_group_btn = page.get_by_text("将选中项存为一组") - create_group_btn.click() - - # Verify Toast - # toast = page.locator(".toast.success") - # expect(toast).to_be_visible() - - # 4. Go to Planning View - plan_tab = page.get_by_text("2. 规划 & 策略") - plan_tab.click() - - # Take a screenshot of planning view - page.screenshot(path="verification/planning_view.png") - print("Captured planning_view.png") - - # 5. Generate Schedule - generate_btn = page.get_by_text("生成课表方案") - generate_btn.click() - - # Wait for results tab - page.wait_for_selector("table.timetable") - - # Screenshot timetable - page.screenshot(path="verification/timetable_view.png") - print("Captured timetable_view.png") - - browser.close() - - finally: - server_process.terminate() - -if __name__ == "__main__": - verify_frontend() diff --git a/verification/verify_spa_frontend.py b/verification/verify_spa_frontend.py deleted file mode 100644 index 28834fc..0000000 --- a/verification/verify_spa_frontend.py +++ /dev/null @@ -1,95 +0,0 @@ -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()