From 7e7354a809e3df0b0c7f075601e4c5777ce11d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=80=BB=E5=B7=A5?= Date: Thu, 27 Aug 2026 12:56:20 +0000 Subject: [PATCH] =?UTF-8?q?HEL-174:=20=E5=85=AC=E5=8F=B8=E7=AB=AF=E5=BE=80?= =?UTF-8?q?=E6=9D=A5=E7=A1=AE=E8=AE=A4=E5=AE=8C=E6=88=90=E6=80=81=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=20success=20=E7=BB=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 确认全部完成后,账期流程第 3 步从 doing(warn 黄)切到 done(success 绿), 本月待办 pill 与阻断提示同步用 success;待确认仍保留 warn。补前端契约与 DOM 色值测试。 Co-authored-by: Cursor Co-authored-by: multica-agent --- scripts/shot_hel174.py | 186 ++++++++++++++++++ tests/test_company_confirm_status_color.py | 210 +++++++++++++++++++++ web/app.js | 12 ++ web/company.html | 4 +- 4 files changed, 410 insertions(+), 2 deletions(-) create mode 100644 scripts/shot_hel174.py create mode 100644 tests/test_company_confirm_status_color.py diff --git a/scripts/shot_hel174.py b/scripts/shot_hel174.py new file mode 100644 index 0000000..ba13085 --- /dev/null +++ b/scripts/shot_hel174.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +"""HEL-174: 360 / 820 / 1440 截图——待确认黄 vs 已完成绿。""" + +from __future__ import annotations + +import re +import threading +from functools import partial +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +from playwright.sync_api import sync_playwright + +ROOT = Path(__file__).resolve().parents[1] +WEB = ROOT / "web" +OUT = Path(__file__).resolve().parent.parent / "hel174-shots" +OUT.mkdir(parents=True, exist_ok=True) + +VIEWPORTS = [ + (1440, 900, "1440"), + (820, 900, "820"), + (360, 800, "360"), +] + + +def extract_fn(source: str, name: str) -> str: + marker = f"function {name}(" + start = source.index(marker) + depth = 0 + for i, ch in enumerate(source[start:], start): + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + return source[start : i + 1] + raise RuntimeError(name) + + +def main() -> None: + app_js = (WEB / "app.js").read_text(encoding="utf-8") + inject = "\n".join( + [ + "const state = {};", + "function $(sel, root) { return (root || document).querySelector(sel); }", + "function $$(sel, root) { return Array.from((root || document).querySelectorAll(sel)); }", + extract_fn(app_js, "formatWorkspaceAmount"), + extract_fn(app_js, "applyCompanyWorkspace"), + ] + ) + + handler = partial(SimpleHTTPRequestHandler, directory=str(WEB)) + httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler) + port = httpd.server_address[1] + thread = threading.Thread(target=httpd.serve_forever, daemon=True) + thread.start() + base = f"http://127.0.0.1:{port}" + + html = f""" + + + + +HEL-174 往来确认状态色 + + + +
+

状态预览

+ +
+
+ 本月待办 + 加载中 +
+
+
+
+
+
+ 账期流程 · 2026-07 +
+ +
+
+
+
+
+
+
+
+ 待确认 + 已确认 +
+ + +
+""" + + with sync_playwright() as p: + browser = p.chromium.launch(headless=True, args=["--no-sandbox"]) + page = browser.new_page() + for width, height, tag in VIEWPORTS: + page.set_viewport_size({"width": width, "height": height}) + page.set_content(html, wait_until="domcontentloaded") + page.add_script_tag(content=inject) + + page.evaluate( + """() => { + document.getElementById('shotLabel').textContent = '待确认(应黄)'; + applyCompanyWorkspace({ + pending_unilateral: 3, pending_total: 3, + unilateral_events: [ + {event_id:1, amount:'100000.00', currency:'CNY', + counterparty_company_name:'金牛贸易', effective_at:'2026-07-03'}, + {event_id:2, amount:'200000.00', currency:'CNY', + counterparty_company_name:'金牛物流', effective_at:'2026-07-11'}, + {event_id:3, amount:'300000.00', currency:'CNY', + counterparty_company_name:'金牛置业', effective_at:'2026-07-24'} + ] + }); + }""" + ) + page.wait_for_timeout(200) + page.screenshot(path=str(OUT / f"hel174-pending-{tag}.png"), full_page=True) + + page.evaluate( + """() => { + document.getElementById('shotLabel').textContent = '已完成(应绿)'; + applyCompanyWorkspace({ + pending_unilateral: 0, pending_total: 0, unilateral_events: [] + }); + }""" + ) + page.wait_for_timeout(200) + page.screenshot(path=str(OUT / f"hel174-done-{tag}.png"), full_page=True) + + colors = page.evaluate( + """() => { + const step = document.querySelector('.flow-step[data-view-link="reconcile"]'); + const state = document.getElementById('workspaceConfirmState'); + const pill = document.getElementById('workspacePendingStatus'); + return { + stepClass: step.className, + stateText: state.textContent, + stateColor: getComputedStyle(state).color, + pillClass: pill.className, + overflowX: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1, + }; + }""" + ) + assert "done" in colors["stepClass"], colors + assert "pill-success" in colors["pillClass"], colors + assert colors["stateText"] == "已完成", colors + assert not colors["overflowX"], colors + print(tag, colors) + + browser.close() + httpd.shutdown() + print("shots:", sorted(p.name for p in OUT.glob("*.png"))) + + +if __name__ == "__main__": + main() diff --git a/tests/test_company_confirm_status_color.py b/tests/test_company_confirm_status_color.py new file mode 100644 index 0000000..1165a56 --- /dev/null +++ b/tests/test_company_confirm_status_color.py @@ -0,0 +1,210 @@ +"""HEL-174: 公司端往来确认完成态用 success 绿,待确认保留 warn 黄。""" + +from __future__ import annotations + +import re +import threading +import unittest +from functools import partial +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +WEB = ROOT / "web" + +try: + from playwright.sync_api import sync_playwright +except ImportError: # pragma: no cover - 默认测试环境无浏览器依赖 + sync_playwright = None + + +class ConfirmStatusSourceContractTests(unittest.TestCase): + """不依赖浏览器:锁住完成态切绿 / 待确认仍黄的实现契约。""" + + def test_app_js_toggles_success_green_when_pending_zero(self) -> None: + js = (WEB / "app.js").read_text(encoding="utf-8") + self.assertIn('pill ${total ? "pill-warn" : "pill-success"}', js) + self.assertIn('flowStep.classList.toggle("doing", pending > 0)', js) + self.assertIn('flowStep.classList.toggle("done", pending === 0)', js) + self.assertIn('notice.classList.toggle("warn", pending > 0)', js) + self.assertIn('notice.classList.toggle("success", pending === 0)', js) + # 文案:完成=已完成,不把待确认一并改绿 + self.assertRegex(js, r'status\.textContent = total \? `\$\{total\} 项待处理` : "已完成"') + self.assertRegex(js, r'flowState\.textContent = pending \? `待处理 \$\{pending\} 笔` : "已完成"') + + def test_design_tokens_map_done_to_success_doing_to_warn(self) -> None: + css = (WEB / "design-system.css").read_text(encoding="utf-8") + self.assertIn("--success:", css) + self.assertIn("--warn:", css) + self.assertIn(".flow-step.done .fs-dot { background: var(--success); }", css) + self.assertIn(".flow-step.done .fs-state { color: var(--success); }", css) + self.assertIn(".flow-step.doing .fs-dot { background: var(--warn);", css) + self.assertIn(".pill-success { background: var(--success-soft); color: var(--success); }", css) + self.assertIn(".pill-warn { background: var(--warn-soft);", css) + self.assertIn(".notice.success { background: var(--success-soft);", css) + self.assertIn(".notice.warn { background: var(--warn-soft);", css) + + def test_company_html_exposes_flow_step_and_cache_bust(self) -> None: + html = (WEB / "company.html").read_text(encoding="utf-8") + self.assertIn('id="workspaceConfirmState"', html) + self.assertIn('id="workspacePendingStatus"', html) + self.assertIn('id="workspaceFlowSub"', html) + self.assertIn('data-view-link="reconcile"', html) + self.assertIn("app.js?v=11", html) + # 静态初值仍为进行中(黄),由 JS 在 pending=0 时切 done + self.assertRegex(html, r'class="flow-step doing"[^>]*data-view-link="reconcile"') + + +def _extract_fn(source: str, name: str) -> str: + marker = f"function {name}(" + start = source.index(marker) + depth = 0 + for i, ch in enumerate(source[start:], start): + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + return source[start : i + 1] + raise AssertionError(f"未能截取 function {name}") + + +@unittest.skipUnless(sync_playwright, "playwright 未安装,跳过真实 DOM 色值校验") +class ConfirmStatusDomTests(unittest.TestCase): + """真实浏览器:pending>0 为黄,pending=0 为绿。""" + + @classmethod + def setUpClass(cls) -> None: + handler = partial(SimpleHTTPRequestHandler, directory=str(WEB)) + cls.httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler) + cls.port = cls.httpd.server_address[1] + cls.thread = threading.Thread(target=cls.httpd.serve_forever, daemon=True) + cls.thread.start() + cls.base = f"http://127.0.0.1:{cls.port}" + app_js = (WEB / "app.js").read_text(encoding="utf-8") + cls.inject_js = "\n".join( + [ + "const state = {};", + "function $(sel, root) { return (root || document).querySelector(sel); }", + "function $$(sel, root) { return Array.from((root || document).querySelectorAll(sel)); }", + _extract_fn(app_js, "formatWorkspaceAmount"), + _extract_fn(app_js, "applyCompanyWorkspace"), + ] + ) + + @classmethod + def tearDownClass(cls) -> None: + cls.httpd.shutdown() + cls.httpd.server_close() + + def _open_fixture(self, page): + page.goto(f"{self.base}/design-system.css", wait_until="domcontentloaded") + page.set_content( + f""" + + + + + + +
+
+ 本月待办 + 加载中 +
+
+
+
+
+
+ 账期流程 +
+ +
+
+
+
+
+ 0 + +""", + wait_until="domcontentloaded", + ) + page.add_script_tag(content=self.inject_js) + page.wait_for_function("() => typeof applyCompanyWorkspace === 'function'") + + @staticmethod + def _colors(page) -> dict: + return page.evaluate( + """() => { + const step = document.querySelector('.flow-step[data-view-link="reconcile"]'); + const state = document.getElementById('workspaceConfirmState'); + const pill = document.getElementById('workspacePendingStatus'); + const notice = document.getElementById('blocking-notice'); + const cs = (el) => getComputedStyle(el); + return { + stepClass: step.className, + stateText: state.textContent, + stateColor: cs(state).color, + pillClass: pill.className, + pillColor: cs(pill).color, + pillText: pill.textContent, + noticeClass: notice.className, + }; + }""" + ) + + def test_pending_stays_warn_completed_turns_success(self) -> None: + with sync_playwright() as p: + browser = p.chromium.launch(headless=True, args=["--no-sandbox"]) + page = browser.new_page(viewport={"width": 1440, "height": 900}) + self._open_fixture(page) + + page.evaluate( + """() => applyCompanyWorkspace({ + pending_unilateral: 2, + pending_total: 2, + unilateral_events: [ + {event_id: 1, amount: '100.00', currency: 'CNY', + counterparty_company_name: '乙', effective_at: '2026-07-01'} + ] + })""" + ) + pending = self._colors(page) + self.assertIn("doing", pending["stepClass"]) + self.assertNotIn("done", pending["stepClass"].split()) + self.assertIn("pill-warn", pending["pillClass"]) + self.assertNotIn("pill-success", pending["pillClass"]) + self.assertIn("warn", pending["noticeClass"].split()) + self.assertIn("待处理", pending["stateText"]) + self.assertIn("待处理", pending["pillText"]) + + page.evaluate( + """() => applyCompanyWorkspace({ + pending_unilateral: 0, + pending_total: 0, + unilateral_events: [] + })""" + ) + done = self._colors(page) + self.assertIn("done", done["stepClass"]) + self.assertNotIn("doing", done["stepClass"].split()) + self.assertIn("pill-success", done["pillClass"]) + self.assertNotIn("pill-warn", done["pillClass"]) + self.assertIn("success", done["noticeClass"].split()) + self.assertEqual("已完成", done["stateText"]) + self.assertEqual("已完成", done["pillText"]) + + self.assertNotEqual(pending["stateColor"], done["stateColor"]) + self.assertNotEqual(pending["pillColor"], done["pillColor"]) + browser.close() + + +if __name__ == "__main__": + unittest.main() diff --git a/web/app.js b/web/app.js index 0e04cf5..8e9561e 100644 --- a/web/app.js +++ b/web/app.js @@ -2619,6 +2619,18 @@ function applyCompanyWorkspace(payload) { ? `单边流水 ${pending} 笔待确认` : "已全部确认,等待集团结账"; } + // 完成态必须切到 success 绿(.flow-step.done),待确认保留 warn 黄(.doing) + const flowStep = flowState?.closest(".flow-step"); + if (flowStep) { + flowStep.classList.toggle("doing", pending > 0); + flowStep.classList.toggle("done", pending === 0); + } + const flowSub = $("#workspaceFlowSub"); + if (flowSub) { + flowSub.textContent = pending + ? "当前停在第 3 步「往来确认」,完成后即可等待集团结账" + : "第 3 步「往来确认」已完成,等待集团复核与结账"; + } const countMatch = $("#count-match"); if (countMatch) countMatch.textContent = String(pending); diff --git a/web/company.html b/web/company.html index 85bf2cf..a52069f 100644 --- a/web/company.html +++ b/web/company.html @@ -63,7 +63,7 @@
- 账期流程 · 2026-07当前停在第 3 步「往来确认」,完成后即可等待集团结账 + 账期流程 · 2026-07当前停在第 3 步「往来确认」,完成后即可等待集团结账 结账日顺延至 08-29
@@ -814,6 +814,6 @@
- +