第 {{ 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()
|