"""Hermes Jobs - Frontend Integration Test (TDD: RED → GREEN) Simulates what a browser does: fetch HTML → extract JS → run logic → verify. """ import sys, os, json, re sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import requests BASE = "http://127.0.0.1:5000" FAILURES = [] def check(name, condition, detail=""): if condition: print(f" PASS {name}") else: print(f" FAIL {name}: {detail}") FAILURES.append(f"{name}: {detail}") # ============================================================ # RED Phase: These capture what the BROWSER actually does # ============================================================ def test_api_jobs_returns_valid_json(): """GET /api/jobs should return {ok: true, data: [...]}""" r = requests.get(f"{BASE}/api/jobs") check("HTTP 200", r.status_code == 200, f"got {r.status_code}") j = r.json() check("ok is true", j.get("ok") == True, f"got {j.get('ok')}") check("data is list", isinstance(j.get("data"), list), f"got {type(j.get('data'))}") return j["data"] def test_job_data_has_required_fields(data): """Each job must have task_id, job_type, job_name, status, started_at""" if not data: print(" SKIP (no jobs) test_job_data_fields") return j = data[0] for field in ["task_id", "job_type", "job_name", "status", "started_at"]: check(f"field '{field}' present", field in j, str(list(j.keys()))) check("task_id is string", isinstance(j.get("task_id"), str)) check("job_type is string", isinstance(j.get("job_type"), str)) def test_job_log_endpoint(task_id): """GET /api/job//log should return log content""" r = requests.get(f"{BASE}/api/job/{task_id}/log") check("log HTTP 200", r.status_code == 200, f"got {r.status_code}") j = r.json() check("log ok is true", j.get("ok") == True, f"got {j.get('ok')}") if j.get("ok"): d = j["data"] check("log has content", "content" in d) check("log has status", "status" in d) if d.get("content"): check("log content not empty", len(d["content"]) > 0, f"size={len(d['content'])}") print(f" log status={d.get('status')}, size={len(d.get('content',''))}") def test_jobs_page_html(): """The jobs page HTML should be usable by a browser""" r = requests.get(f"{BASE}/jobs") check("page HTTP 200", r.status_code == 200, f"got {r.status_code}") html = r.text # Check key elements exist check("has '任务监控' in title or body", "任务监控" in html) check("has list div", 'id="list"' in html) check("has refresh function", "refresh" in html and "function refresh" in html) # ============================================================ # RED TEST: Check if the JS has syntax issues # ============================================================ # Extract the jobs ", start) js_block = html[max(0, start-50):end] # Check for problematic Jinja2-escaping in JS # Template source uses: class=\\\" which renders as class=\\\" in HTML # In JS single-quoted strings, \" should just be " (or at worst literal \"") # Let's check if the rendered JS has weird escaping check("JS has no double-backslash in class attr", 'class=\\\\\"' not in js_block, "Found double-escaped quotes in JS — Jinja2 issue") # Check if the innerHTML assignment has correct quoting inner_html_line = [l for l in js_block.split('\n') if 'innerHTML' in l] if inner_html_line: line = inner_html_line[0].strip() # Should look like: el.innerHTML='
...' check("innerHTML uses single quotes for JS string", "innerHTML='" in line or 'innerHTML="' in line, f"Line: {line[:80]}") print(f"\n JS block length: {len(js_block)} chars") def test_js_can_parse_job_data(): """Simulate what the browser's JS engine does with the API response""" r = requests.get(f"{BASE}/api/jobs") data = r.json()["data"] if not data: print(" SKIP (no jobs)") return # Simulate: var t = data[i] t = data[0] # Simulate: TYPE_NAMES[t.job_type] || t.job_type TYPE_NAMES = { "daily_report": "日报生成", "report_image": "日报图片", "article_summary": "文章摘要", "article_learn": "文章学习", "article_image": "文章图片" } type_name = TYPE_NAMES.get(t["job_type"], t["job_type"]) check("type_name resolved", type_name != t["job_type"] or t["job_type"] not in TYPE_NAMES, f"Unrecognized type: {t['job_type']}") # Simulate: STATUS_NAMES[t.status] || t.status STATUS_NAMES = {"running": "运行中", "completed": "已完成", "failed": "失败"} status_name = STATUS_NAMES.get(t["status"], t["status"]) check("status_name resolved", isinstance(status_name, str)) # Simulate: esc(t.job_name) # The esc function uses textContent for safe HTML insertion name = t.get("job_name", "未命名任务") check("job_name is valid string", isinstance(name, str) and len(name) > 0) # Simulate building the href href = f'/job/{t["task_id"]}' check("task_id in href", t["task_id"] in href) def test_trigger_and_track(): """Integration: trigger a task, verify it appears in job list""" import time unique_id = str(int(time.time() * 1000))[-8:] # Trigger r = requests.post(f"{BASE}/api/articles/summarize", json={ "url": f"https://mp.weixin.qq.com/s/tdd_test_{unique_id}", "title": f"TDD Test {unique_id}" }) check("trigger HTTP 200", r.status_code == 200) j = r.json() check("trigger returns ok", j.get("ok") == True) task_id = j.get("data", {}).get("task_id", "") check("got task_id", len(task_id) > 0, f"got: {task_id}") if task_id: # Wait briefly for DB write import time; time.sleep(0.5) # Check job list includes it r2 = requests.get(f"{BASE}/api/jobs") jobs = r2.json()["data"] found = [x for x in jobs if x["task_id"] == task_id] check("task appears in job list", len(found) > 0, f"task_id={task_id} not in {[j['task_id'] for j in jobs[:5]]}") if found: job = found[0] check("job status is running", job["status"] == "running", f"got {job['status']}") check("job_name matches", unique_id in job.get("job_name", ""), f"got {job.get('job_name')}") # Check log endpoint r3 = requests.get(f"{BASE}/api/job/{task_id}/log") check("log API HTTP 200", r3.status_code == 200) if r3.status_code == 200: log_data = r3.json() # Log file may be empty if task just started; that's OK log_content = log_data.get("data", {}).get("content", "") check("log content present", isinstance(log_content, str), f"type={type(log_content)}") # ============================================================ print("=" * 60) print("Hermes Jobs — Frontend Integration Tests") print("=" * 60) data = test_api_jobs_returns_valid_json() test_job_data_has_required_fields(data) if data: test_job_log_endpoint(data[0]["task_id"]) test_jobs_page_html() test_js_can_parse_job_data() print("\n--- Integration Test ---") test_trigger_and_track() print("\n" + "=" * 60) if FAILURES: print(f"FAILED: {len(FAILURES)} test(s)") for f in FAILURES: print(f" ❌ {f}") sys.exit(1) else: print("ALL TESTS PASSED ✅")