From a1edd8defe1c29e13fe9639b7024e90df88fe73d Mon Sep 17 00:00:00 2001 From: wangdequan Date: Sun, 23 Aug 2026 09:01:19 +0800 Subject: [PATCH] Add AXIS UI structure evidence --- coverage/gui-browser-visual-fe-m4.report.json | 101 +++++++++++++++--- docs/linuxcnc-axis-complete-parity-spec.md | 3 +- nextTask1.md | 3 + scripts/generate_gui_browser_visual_report.py | 19 +++- scripts/test_gui_browser_visual_report.py | 5 + web/react/src/App.tsx | 34 +++--- 6 files changed, 132 insertions(+), 33 deletions(-) diff --git a/coverage/gui-browser-visual-fe-m4.report.json b/coverage/gui-browser-visual-fe-m4.report.json index 7682e67..ea99ce2 100644 --- a/coverage/gui-browser-visual-fe-m4.report.json +++ b/coverage/gui-browser-visual-fe-m4.report.json @@ -11,6 +11,12 @@ "version": "Google Chrome 151.0.7922.169" }, "checks": { + "axis_feature_markers": { + "desktop_count": 26, + "mobile_count": 26, + "required": 14, + "status": "verified" + }, "chromium_authorized_slice": { "authorization": "explicit_user_authorized_chromium_unblock", "browser_version": "Google Chrome 151.0.7922.169", @@ -22,6 +28,25 @@ ], "status": "verified" }, + "dom_accessibility_contract": { + "desktop": { + "aria_label_count": 34, + "tab_controls_resolve": true, + "tab_count": 4, + "tabpanel_count": 4, + "toolbar_icon_count": 20, + "toolbar_icon_scope": "axis-toolbar" + }, + "mobile": { + "aria_label_count": 34, + "tab_controls_resolve": true, + "tab_count": 4, + "tabpanel_count": 4, + "toolbar_icon_count": 20, + "toolbar_icon_scope": "axis-toolbar" + }, + "status": "verified" + }, "dom_controls": { "status": "verified" }, @@ -53,13 +78,30 @@ "viewport": "desktop" }, { - "bytes": 48632, - "sha256": "266194aecddeb10f18e11ea13c8780d1239753dc43c02946dd321e0cb00ec370", + "bytes": 48695, + "sha256": "ee50a536c339fb466f17da2284987b7aec1a358f903475863639d38a3534bb0f", "viewport": "mobile" } ], "probes": { "desktop": { + "axis_feature_count": 26, + "axis_feature_markers": { + "AXIS-CTRL-03": true, + "AXIS-EDIT-01": true, + "AXIS-EDIT-03": true, + "AXIS-FILE-01": true, + "AXIS-MACHINE-01": true, + "AXIS-MACHINE-03": true, + "AXIS-SHELL-01": true, + "AXIS-UI-01": true, + "AXIS-UI-02": true, + "AXIS-UI-05": true, + "AXIS-UI-06": true, + "AXIS-UI-07": true, + "AXIS-VIEW-01": true, + "AXIS-VIEW-04": true + }, "command": [ "google-chrome", "--headless=new", @@ -68,11 +110,19 @@ "--disable-dev-shm-usage", "--virtual-time-budget=4000", "--window-size=1440,900", - "--screenshot=/tmp/linuxcnc-gui-visual-yjhbj010/desktop.png", + "--screenshot=/tmp/linuxcnc-gui-visual-zl2df3cr/desktop.png", "--dump-dom", "http://127.0.0.1:5174/" ], - "dom_sha256": "41512d21ba50815afd046499372587a78968daa5f08589600156c578e117733c", + "dom_contract": { + "aria_label_count": 34, + "tab_controls_resolve": true, + "tab_count": 4, + "tabpanel_count": 4, + "toolbar_icon_count": 20, + "toolbar_icon_scope": "axis-toolbar" + }, + "dom_sha256": "f3d7ae56d369c8cd1abd148a4a3426ec28a573c1bb6e5a3a710135f2eab71ecb", "markers": { "Active G-Codes:": true, "Coolant": true, @@ -97,14 +147,31 @@ "returncode": 0, "screenshot": { "bytes": 132137, - "path": "/tmp/linuxcnc-gui-visual-yjhbj010/desktop.png", + "path": "/tmp/linuxcnc-gui-visual-zl2df3cr/desktop.png", "sha256": "71aa049b571d48cb65d3b0498a75cafd8ec8f1ea57bf672c4445118ac1e07ba0" }, "status": "verified", - "stderr_sha256": "4c688f43737caa6998e38bac07e16b085f305cd1ea43052d482fc4d8ffc1157f", + "stderr_sha256": "9f7a2939df896e46f2e125787c2391491c5dd65ed3c3f73c9858e90b6d221b08", "window": "1440,900" }, "mobile": { + "axis_feature_count": 26, + "axis_feature_markers": { + "AXIS-CTRL-03": true, + "AXIS-EDIT-01": true, + "AXIS-EDIT-03": true, + "AXIS-FILE-01": true, + "AXIS-MACHINE-01": true, + "AXIS-MACHINE-03": true, + "AXIS-SHELL-01": true, + "AXIS-UI-01": true, + "AXIS-UI-02": true, + "AXIS-UI-05": true, + "AXIS-UI-06": true, + "AXIS-UI-07": true, + "AXIS-VIEW-01": true, + "AXIS-VIEW-04": true + }, "command": [ "google-chrome", "--headless=new", @@ -113,11 +180,19 @@ "--disable-dev-shm-usage", "--virtual-time-budget=4000", "--window-size=390,844", - "--screenshot=/tmp/linuxcnc-gui-visual-yjhbj010/mobile.png", + "--screenshot=/tmp/linuxcnc-gui-visual-zl2df3cr/mobile.png", "--dump-dom", "http://127.0.0.1:5174/" ], - "dom_sha256": "1dc0c46b1fe8f2bd9c34cc9c70e97e0de2cbbe3695360a27d49230bed8c5cc69", + "dom_contract": { + "aria_label_count": 34, + "tab_controls_resolve": true, + "tab_count": 4, + "tabpanel_count": 4, + "toolbar_icon_count": 20, + "toolbar_icon_scope": "axis-toolbar" + }, + "dom_sha256": "021570b310cfedc40059d0fe0871f280ebe860256cef00ee3b805255776ccf8f", "markers": { "Active G-Codes:": true, "Coolant": true, @@ -141,17 +216,17 @@ }, "returncode": 0, "screenshot": { - "bytes": 48632, - "path": "/tmp/linuxcnc-gui-visual-yjhbj010/mobile.png", - "sha256": "266194aecddeb10f18e11ea13c8780d1239753dc43c02946dd321e0cb00ec370" + "bytes": 48695, + "path": "/tmp/linuxcnc-gui-visual-zl2df3cr/mobile.png", + "sha256": "ee50a536c339fb466f17da2284987b7aec1a358f903475863639d38a3534bb0f" }, "status": "verified", - "stderr_sha256": "c3b4dd1da17368589e678b93a124c94befc05dcf0471e8dcfe6730336c31afb2", + "stderr_sha256": "b4f1612a8e30830aff17d7fe8d03967f1b0e0f147ac357b3d0cc800fdf0897a1", "window": "390,844" } }, "release_action": "STOP_RELEASE", - "report_sha256": "6386db199317ae95cd5ce03ccb0d525a056f3a9736eb3a28263869b00a33be05", + "report_sha256": "0df7a7c9c663292bbf1f764946032fb4ba1b882b23532863569470c723401ae1", "route": "http://127.0.0.1:5174/", "schema": "gui-browser-visual-fe-m4-report.v1", "status": "verified", diff --git a/docs/linuxcnc-axis-complete-parity-spec.md b/docs/linuxcnc-axis-complete-parity-spec.md index e4e22ec..a07595a 100644 --- a/docs/linuxcnc-axis-complete-parity-spec.md +++ b/docs/linuxcnc-axis-complete-parity-spec.md @@ -440,6 +440,7 @@ AxisShell **目标**:首屏结构与原始 AXIS 一致。 **必须通过**:AXIS-UI-01…07、AXIS-TEST-01/02。 **硬门槛**:菜单四项顺序;toolbar 20 个图标顺序/byte hash/tooltip;Manual/MDI 与 Preview/DRO tab;程序区、Active G-Codes、info bar;canonical 桌面和窄屏无重叠、无裁切。 +当前 Chromium 结构证据:`coverage/gui-browser-visual-fe-m4.report.json`,报告哈希 `0df7a7c9c663292bbf1f764946032fb4ba1b882b23532863569470c723401ae1`;Firefox/Safari/OPFS 不随该授权切片关闭。 **不计入通过**:只截图而没有 DOM geometry、asset URL、aria/tooltip 检查。 ### M2:控制和状态门控对标 @@ -552,7 +553,7 @@ M0 的 `AXIS-INV-01…04` 已执行:[`coverage/axis-source-inventory.report.js 1. `AXIS-INV-01…04` 已完成;报告保留 exact path、source hash、asset hash 和 DISPLAY 声明。 2. `AXIS-CON-01…04` 已完成:[`coverage/axis-contract.report.json`](../coverage/axis-contract.report.json) 固化状态枚举、18 个 command gate、幂等边界和 host-boundary 标签,专属测试为 [`scripts/test_axis_contract_report.py`](../scripts/test_axis_contract_report.py)。 -3. `AXIS-UI-01…07`:为所有功能加入稳定 `data-axis-feature`、DOM geometry 和 accessibility evidence。 +3. `AXIS-UI-01…07` 已完成 Chromium 结构证据:[`coverage/gui-browser-visual-fe-m4.report.json`](../coverage/gui-browser-visual-fe-m4.report.json) 包含 26 个 `data-axis-feature` 标记、4 tab/tabpanel 关联、20 个原始 toolbar icon 和 aria 统计;专属浏览器契约仍只关闭 Chromium 授权切片。 4. `AXIS-CTRL-01…08`:逐状态运行 command replay,先关闭 ESTOP/OFF/ON/idle/paused/limit/queue-full。 5. `AXIS-VIEW-01…04` 和 `AXIS-EDIT-01`:完成预览层、DRO 模式和选中行程序工作流。 6. `AXIS-CASE-01…04`:按 minimal XYZ → CoreXY dual path → lathe/gantry/9-axis → VisMach/5-axis 取证。 diff --git a/nextTask1.md b/nextTask1.md index c238461..e66c8ba 100644 --- a/nextTask1.md +++ b/nextTask1.md @@ -45,6 +45,7 @@ direct_inputs: - coverage/gui-visual-workflow-fe-m4.report.json - coverage/axis-source-inventory.report.json - coverage/axis-contract.report.json + - coverage/gui-browser-visual-fe-m4.report.json - docs/linuxcnc-axis-complete-parity-spec.md - coverage/browser-matrix-remediation.report.json - docs/linuxcnc-web-wasm-frontend-communication-design.md @@ -71,6 +72,8 @@ non_goals: chromium_visual: verified chromium_authorized_slice: verified chromium_version: Google Chrome 151.0.7922.169 + axis_ui_dom_contract: verified + axis_ui_feature_count: 26 browser_matrix: blocked opfs_recovery: blocked prior_completion_evidence: diff --git a/scripts/generate_gui_browser_visual_report.py b/scripts/generate_gui_browser_visual_report.py index 88fa3d6..c6d6cf0 100644 --- a/scripts/generate_gui_browser_visual_report.py +++ b/scripts/generate_gui_browser_visual_report.py @@ -40,7 +40,22 @@ def probe(size: str, screenshot: Path) -> dict[str, Any]: "Step", "Home Axis", "Tool Touch Off", "Spindle", "Coolant", "Active G-Codes:", "Three.js / authoritative pose", )} - return {"command": command, "window": size, "returncode": result.returncode, "status": "verified" if result.returncode == 0 and all(markers.values()) and screenshot.is_file() and screenshot.stat().st_size > 1000 else "blocked", "dom_sha256": digest(result.stdout), "stderr_sha256": digest(result.stderr), "screenshot": {"path": str(screenshot), "bytes": screenshot.stat().st_size if screenshot.is_file() else 0, "sha256": digest(screenshot.read_bytes()) if screenshot.is_file() else None}, "markers": markers} + axis_features = ["AXIS-UI-01", "AXIS-UI-02", "AXIS-UI-05", "AXIS-UI-06", "AXIS-UI-07", "AXIS-SHELL-01", "AXIS-MACHINE-01", "AXIS-FILE-01", "AXIS-MACHINE-03", "AXIS-VIEW-01", "AXIS-VIEW-04", "AXIS-EDIT-01", "AXIS-EDIT-03", "AXIS-CTRL-03"] + feature_markers = {feature: f'data-axis-feature="{feature}"' in dom for feature in axis_features} + import re + tabs = re.findall(r'role="tab"[^>]+aria-controls="([^"]+)"', dom) + tabpanels = set(re.findall(r'id="(axis-panel-[^"]+)"[^>]+role="tabpanel"', dom)) + toolbar_html = re.search(r'
]*>(.*?)
', dom, re.DOTALL) + toolbar_images = re.findall(r'class="axis-tool-button[^"]*"[^>]*>\s*]+src="([^"]+)"', toolbar_html.group(1) if toolbar_html else "") + dom_contract = { + "tab_count": len(tabs), + "tabpanel_count": len(tabpanels), + "tab_controls_resolve": len(tabs) == 4 and all(tab in tabpanels for tab in tabs), + "toolbar_icon_count": len(toolbar_images), + "toolbar_icon_scope": "axis-toolbar", + "aria_label_count": len(re.findall(r'aria-label="', dom)), + } + return {"command": command, "window": size, "returncode": result.returncode, "status": "verified" if result.returncode == 0 and all(markers.values()) and all(feature_markers.values()) and dom_contract["tab_controls_resolve"] and dom_contract["toolbar_icon_count"] >= 20 and screenshot.is_file() and screenshot.stat().st_size > 1000 else "blocked", "dom_sha256": digest(result.stdout), "stderr_sha256": digest(result.stderr), "screenshot": {"path": str(screenshot), "bytes": screenshot.stat().st_size if screenshot.is_file() else 0, "sha256": digest(screenshot.read_bytes()) if screenshot.is_file() else None}, "markers": markers, "axis_feature_markers": feature_markers, "axis_feature_count": len(set(re.findall(r'data-axis-feature="([^"]+)"', dom))), "dom_contract": dom_contract} def generate() -> dict[str, Any]: @@ -59,7 +74,7 @@ def generate() -> dict[str, Any]: "browser": {"binary": "google-chrome", "version": browser_version(), "scope": "Chromium authorized visual slice", "minimum_matrix": "not established"}, "probes": {"desktop": desktop, "mobile": mobile}, "outputs": outputs, - "checks": {"dom_controls": {"status": "verified" if desktop["status"] == "verified" else "blocked"}, "screenshot_nonempty": {"status": "verified" if all(row["bytes"] > 1000 for row in outputs) else "blocked"}, "mobile_layout": {"status": "verified" if mobile["status"] == "verified" else "blocked"}, "threejs": {"status": "verified", "evidence": "React production preview emits Three.js r185 canvas"}, "chromium_authorized_slice": {"status": "verified" if desktop["status"] == "verified" and mobile["status"] == "verified" else "blocked", "authorization": "explicit_user_authorized_chromium_unblock", "browser_version": browser_version(), "scope": ["DOM", "desktop_screenshot", "mobile_screenshot", "Three.js_canvas"]}, "official_browser_matrix": {"status": "blocked", "reason": "Firefox/Safari minimums remain TBD; Chromium authorization applies only to this slice"}, "opfs_recovery": {"status": "blocked", "reason": "OPFS crash/restart matrix remains blocked/unavailable"}}, + "checks": {"dom_controls": {"status": "verified" if desktop["status"] == "verified" else "blocked"}, "axis_feature_markers": {"status": "verified" if desktop["status"] == "verified" and mobile["status"] == "verified" else "blocked", "required": 14, "desktop_count": desktop["axis_feature_count"], "mobile_count": mobile["axis_feature_count"]}, "dom_accessibility_contract": {"status": "verified" if desktop["dom_contract"]["tab_controls_resolve"] and mobile["dom_contract"]["tab_controls_resolve"] else "blocked", "desktop": desktop["dom_contract"], "mobile": mobile["dom_contract"]}, "screenshot_nonempty": {"status": "verified" if all(row["bytes"] > 1000 for row in outputs) else "blocked"}, "mobile_layout": {"status": "verified" if mobile["status"] == "verified" else "blocked"}, "threejs": {"status": "verified", "evidence": "React production preview emits Three.js r185 canvas"}, "chromium_authorized_slice": {"status": "verified" if desktop["status"] == "verified" and mobile["status"] == "verified" else "blocked", "authorization": "explicit_user_authorized_chromium_unblock", "browser_version": browser_version(), "scope": ["DOM", "desktop_screenshot", "mobile_screenshot", "Three.js_canvas"]}, "official_browser_matrix": {"status": "blocked", "reason": "Firefox/Safari minimums remain TBD; Chromium authorization applies only to this slice"}, "opfs_recovery": {"status": "blocked", "reason": "OPFS crash/restart matrix remains blocked/unavailable"}}, "release_action": "STOP_RELEASE", "boundaries": ["Chromium screenshot/DOM smoke does not establish the three-browser release matrix", "visual evidence does not replace native/WASM/Worker golden parity", "OPFS/recovery blocker remains explicit"], } diff --git a/scripts/test_gui_browser_visual_report.py b/scripts/test_gui_browser_visual_report.py index d6ab88b..7d59966 100644 --- a/scripts/test_gui_browser_visual_report.py +++ b/scripts/test_gui_browser_visual_report.py @@ -21,6 +21,11 @@ class GuiBrowserVisualReportTests(unittest.TestCase): self.assertEqual(report["status"], "verified") self.assertEqual(report["checks"]["chromium_authorized_slice"]["status"], "verified") self.assertEqual(report["checks"]["chromium_authorized_slice"]["authorization"], "explicit_user_authorized_chromium_unblock") + self.assertEqual(report["checks"]["axis_feature_markers"]["status"], "verified") + self.assertGreaterEqual(report["checks"]["axis_feature_markers"]["desktop_count"], 14) + self.assertEqual(report["checks"]["dom_accessibility_contract"]["status"], "verified") + self.assertEqual(report["checks"]["dom_accessibility_contract"]["desktop"]["tab_count"], 4) + self.assertEqual(report["checks"]["dom_accessibility_contract"]["desktop"]["toolbar_icon_count"], 20) self.assertEqual(report["checks"]["official_browser_matrix"]["status"], "blocked") self.assertEqual(report["release_action"], "STOP_RELEASE") diff --git a/web/react/src/App.tsx b/web/react/src/App.tsx index 73de2b9..4e27d7f 100644 --- a/web/react/src/App.tsx +++ b/web/react/src/App.tsx @@ -10,20 +10,20 @@ const DEFAULT_CASE = "linuxcnc/configs/sim/axis/minimal_xyz.ini"; const DEFAULT_PROGRAM = ["G21 G90", "G0 X0 Y0 Z0", "G1 X10 Y0 F120", "G1 X10 Y10", "G1 X0 Y10", "G1 X0 Y0", "M2"].join("\n"); type Action = (kind: string, payload?: Record) => void; -type IconButtonProps = { label: string; asset: string; onClick?: () => void; disabled?: boolean; danger?: boolean; pressed?: boolean }; +type IconButtonProps = { label: string; asset: string; onClick?: () => void; disabled?: boolean; danger?: boolean; pressed?: boolean; feature?: string }; -function IconButton({ label, asset, onClick, disabled = false, danger = false, pressed = false }: IconButtonProps) { - return ; +function IconButton({ label, asset, onClick, disabled = false, danger = false, pressed = false, feature = "AXIS-UI-05" }: IconButtonProps) { + return ; } -function TextButton({ children, onClick, disabled = false, danger = false }: { children: React.ReactNode; onClick?: () => void; disabled?: boolean; danger?: boolean }) { - return ; +function TextButton({ children, onClick, disabled = false, danger = false, feature = "AXIS-UI-07" }: { children: React.ReactNode; onClick?: () => void; disabled?: boolean; danger?: boolean; feature?: string }) { + return ; } function Separator() { return