diff --git a/scripts/dev-cloud-workbench-smoke.test.mjs b/scripts/dev-cloud-workbench-smoke.test.mjs index da296519..ff27e6a7 100644 --- a/scripts/dev-cloud-workbench-smoke.test.mjs +++ b/scripts/dev-cloud-workbench-smoke.test.mjs @@ -221,7 +221,10 @@ test("dedicated layout smoke CLI supports static build and DEV live shorthand", test("source/default smoke covers #352 resource explorer removal contract", () => { const report = runDevCloudWorkbenchStaticSmoke(); - assert.equal(report.checks.find((check) => check.id === "feedback-352-resource-explorer-removed")?.status, "pass"); + const check = report.checks.find((item) => item.id === "feedback-352-resource-explorer-removed"); + assert.equal(check?.status, "pass"); + assert.equal(check.evidence.includes("aside#resource-explorer absent"), true); + assert.equal(check.evidence.includes("#explorer-resize absent"), true); }); test("source/default smoke covers #276 two-column wiring long-table contract", () => { @@ -723,6 +726,7 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou "layout-desktop-default", "layout-narrow-desktop-default", "layout-mobile-default", + "layout-feedback-352-resource-explorer-removed", "layout-gate-desktop", "layout-gate-narrow-desktop", "layout-gate-mobile" @@ -742,10 +746,18 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou assert.equal(report.safety.hitTestMethod.includes("elementsFromPoint"), true); assert.match(report.safety.statement, /does not send Code Agent chat, call M3 IO/u); assert.equal(report.checks.find((check) => check.id === "layout-issue-287-future-hardware-status-tabs")?.status, "pass"); + assert.equal(report.checks.find((check) => check.id === "layout-feedback-352-resource-explorer-removed")?.status, "pass"); assert.equal(report.checks.find((check) => check.id === "layout-issue-288-future-single-table-gate")?.status, "skip"); const desktopDefault = report.checks.find((check) => check.id === "layout-desktop-default")?.observations; + const resourceExplorerRemoval = report.checks.find((check) => check.id === "layout-feedback-352-resource-explorer-removed")?.observations; + assert.equal(resourceExplorerRemoval.forbiddenSelectors.includes("#explorer-resize"), true); + assert.equal(resourceExplorerRemoval.forbiddenCopy.includes("常用能力"), true); + assert.equal(resourceExplorerRemoval.coverage.desktop.covered, true); + assert.equal(resourceExplorerRemoval.coverage.mobile.removedVisibleCopyAbsent, true); assert.equal(desktopDefault.removedSelectorsAbsent, true); + assert.equal(desktopDefault.removedVisibleCopyAbsent, true); + assert.equal(desktopDefault.resourceExplorerRemovalGuard, true); assert.equal(desktopDefault.keyTargetsReachable, true); assert.equal(desktopDefault.wiring.longTableOk, true); assert.equal(desktopDefault.wiring.noHorizontalScroll, true); @@ -766,6 +778,8 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou const mobileDefault = report.checks.find((check) => check.id === "layout-mobile-default")?.observations; assert.equal(mobileDefault.removedSelectorsAbsent, true); + assert.equal(mobileDefault.removedVisibleCopyAbsent, true); + assert.equal(mobileDefault.resourceExplorerRemovalGuard, true); assert.equal(mobileDefault.keyTargetsReachable, true); assert.equal(mobileDefault.wiring.longTableOk, true); assert.equal(mobileDefault.wiring.noHorizontalScroll, true); @@ -808,5 +822,6 @@ test("repo-owned web checks expose source build and DEV live layout smoke gates" assert.equal(cloudWebPackage.scripts["layout:live"].includes("--live --url http://74.48.78.17:16666/"), true); assert.match(cloudWebCheckSource, /runDevCloudWorkbenchLayoutSmoke/u); assert.match(cloudWebCheckSource, /tmp\/dev-cloud-workbench-layout-web-check\.json/u); + assert.match(cloudWebCheckSource, /layout-feedback-352-resource-explorer-removed/u); assert.match(cloudWebCheckSource, /compactLayoutSmokeFailures/u); }); diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index 7e37c583..b9602ffc 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -157,6 +157,55 @@ const helpRouteAliases = Object.freeze(["/help"]); const incompletePrimaryNavLabels = Object.freeze(["台", "证", "诊", "帮"]); +const removedResourceExplorerSourceTerms = Object.freeze([ + "resource-explorer", + "explorer-toggle", + "explorer-resize", + "resource-tree", + "explorer-capabilities", + "capability-count", + "tree-count", + "data-focus-command", + "quick-actions", + "tree-panel", + "explorer-collapsed", + "--explorer-width", + "--explorer-min-width", + "--explorer-max-width" +]); + +const removedResourceExplorerDomSelectors = Object.freeze([ + "aside#resource-explorer", + "#resource-explorer", + "#explorer-toggle", + "#explorer-resize", + "#resource-tree", + ".resource-tree", + "#explorer-capabilities", + ".explorer-capabilities", + "#capability-count", + "#tree-count", + "[data-focus-command]", + ".quick-actions", + ".tree-panel", + ".explorer-collapsed" +]); + +const removedResourceExplorerCopyTerms = Object.freeze([ + "资源浏览与硬件资源树", + "资源浏览", + "硬件资源树", + "只读工作区", + "常用能力", + "收起资源树", + "展开资源树", + "拖拽调整左侧资源树宽度", + "资源查看", + "查看接线", + "查看状态", + "编写任务" +]); + const requiredWiringLongTableHeaders = Object.freeze(["res_boxsimu_1", "res_boxsimu_2"]); const forbiddenLegacyWiringColumns = Object.freeze([ "源设备", @@ -507,7 +556,7 @@ function runStaticSmoke() { "aside#resource-explorer absent", "#explorer-resize absent", "resource tree / quick actions / capability cards absent", - "right side keeps a 560-740px M3 status workspace boundary through --right-width" + "right side keeps a 560-760px M3 status workspace boundary through --right-width" ] }); @@ -1052,6 +1101,12 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) { summary: "Mobile 390x844 default layout has no resource drawer and preserves Agent input plus M3 tabs/controls.", observations: mobile.default }, + { + id: "layout-feedback-352-resource-explorer-removed", + status: allViewportCoverage(viewportResults, "resourceExplorerRemovalGuard") ? "pass" : "blocked", + summary: "#352 default workbench has no aside#resource-explorer, #explorer-resize, resource tree/capability copy, or reclaimed left panel across desktop and mobile.", + observations: summarizeResourceExplorerRemovalCoverage(viewportResults) + }, { id: "layout-right-sidebar-resize-desktop", status: desktop.rightResize.pass ? "pass" : "blocked", @@ -1177,7 +1232,7 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) { summary: useLiveUrl ? "Live browser layout smoke verifies workbench layout, hit targets, overflow, /gate usability, and the two-column wiring panel only; it is not M3 hardware acceptance." : "Static local browser layout smoke verifies workbench layout, hit targets, overflow, /gate usability, and the two-column wiring panel only; deployment still requires DEV live verification.", - refs: ["pikasTech/HWLAB#273", "pikasTech/HWLAB#276", "pikasTech/HWLAB#278", "pikasTech/HWLAB#287", "pikasTech/HWLAB#288", "pikasTech/HWLAB#99", "pikasTech/HWLAB#227"], + refs: ["pikasTech/HWLAB#273", "pikasTech/HWLAB#276", "pikasTech/HWLAB#278", "pikasTech/HWLAB#287", "pikasTech/HWLAB#288", "pikasTech/HWLAB#352", "pikasTech/HWLAB#99", "pikasTech/HWLAB#227"], viewports: layoutViewports.map(({ id, width, height }) => ({ id, width, height })), checks, blockers, @@ -1351,7 +1406,7 @@ function layoutSourceContract() { "docs/reference/cloud-workbench.md", "docs/reference/code-agent-chat-readiness.md" ], - summary: "Cloud Workbench layout smoke protects the #99 workbench route, #227 visible M3 control area, Code Agent input, right hardware/trusted-record containers, /gate current route, and outer-scroll lock without claiming hardware acceptance." + summary: "Cloud Workbench layout smoke protects the #99 workbench route, #227 visible M3 control area, #352 resource-explorer removal, Code Agent input, right hardware/trusted-record containers, /gate current route, and outer-scroll lock without claiming hardware acceptance." }; } @@ -2157,34 +2212,8 @@ function hasMobileWorkbenchLayoutContract({ html, app, styles }) { function hasResourceExplorerRemovalContract({ html, app, styles }) { const source = `${html}\n${app}\n${styles}`; - const removedSelectorsAbsent = [ - "resource-explorer", - "explorer-toggle", - "explorer-resize", - "resource-tree", - "explorer-capabilities", - "capability-count", - "tree-count", - "data-focus-command", - "quick-actions", - "tree-panel", - "explorer-collapsed", - "--explorer-width", - "--explorer-min-width", - "--explorer-max-width" - ].every((term) => !source.includes(term)); - const removedTextAbsent = [ - "资源浏览与硬件资源树", - "只读工作区", - "常用能力", - "收起资源树", - "展开资源树", - "拖拽调整左侧资源树宽度", - "资源查看", - "查看接线", - "查看状态", - "编写任务" - ].every((term) => !source.includes(term)); + const removedSelectorsAbsent = removedResourceExplorerSourceTerms.every((term) => !source.includes(term)); + const removedTextAbsent = removedResourceExplorerCopyTerms.every((term) => !source.includes(term)); return ( removedSelectorsAbsent && removedTextAbsent && @@ -5778,7 +5807,7 @@ async function inspectRightSidebarResizeDisabled(page, { viewport }) { } async function inspectLayoutState(page, { mode, viewport, compareTo }) { - return page.evaluate(async ({ mode, viewport, compareTo }) => { + return page.evaluate(async ({ mode, viewport, compareTo, resourceExplorerGuard }) => { const failures = []; const skip = []; const selectorLabels = [ @@ -5968,6 +5997,21 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) { scrolls: element.scrollHeight > element.clientHeight ? element.scrollTop > before : true }; }; + const visibleTextForElement = (root) => { + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + const parts = []; + for (let node = walker.nextNode(); node; node = walker.nextNode()) { + const parent = node.parentElement; + if (!parent || parent.closest("[hidden], script, style, noscript")) continue; + const style = getComputedStyle(parent); + if (style.display === "none" || style.visibility === "hidden") continue; + const box = parent.getBoundingClientRect(); + if (box.width <= 0 || box.height <= 0) continue; + const text = node.textContent?.replace(/\s+/gu, " ").trim(); + if (text) parts.push(text); + } + return parts.join(" ").replace(/\s+/gu, " ").trim(); + }; window.scrollTo(0, 240); document.documentElement.scrollTop = 240; @@ -6252,23 +6296,22 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) { await new Promise((resolve) => requestAnimationFrame(resolve)); } const removedSelectorStates = Object.fromEntries( - [ - ["#resource-explorer", "resourceExplorer"], - ["#explorer-resize", "explorerResize"], - ["#explorer-toggle", "explorerToggle"], - ["#resource-tree", "resourceTree"], - [".quick-actions", "quickActions"] - ].map(([selector, key]) => [key, document.querySelector(selector) === null]) + resourceExplorerGuard.domSelectors.map((selector) => [selector, document.querySelector(selector) === null]) ); const removedSelectorsAbsent = Object.values(removedSelectorStates).every(Boolean); + const visibleWorkbenchText = visibleTextForElement(document.body); + const removedVisibleCopyHits = resourceExplorerGuard.copyTerms.filter((term) => visibleWorkbenchText.includes(term)); + const removedVisibleCopyAbsent = removedVisibleCopyHits.length === 0; + const resourceExplorerRemovalGuard = removedSelectorsAbsent && removedVisibleCopyAbsent; const keyTargetsReachable = blockedKeyTargets.length === 0; - if (!removedSelectorsAbsent) { + if (!resourceExplorerRemovalGuard) { failures.push({ failureType: "blocked/skip", - selector: "#resource-explorer", + selector: "#resource-explorer,#explorer-resize", viewport, - summary: "Removed resource explorer selectors are still present in the default workbench DOM.", - removedSelectorStates + summary: "Removed resource explorer DOM or legacy copy returned in the default workbench.", + removedSelectorStates, + removedVisibleCopyHits }); } if (!rootScrollLocked) { @@ -6324,7 +6367,7 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) { Object.values(noHorizontalOverflow).every(Boolean) && rightWidthTarget && keyTargetsReachable && - removedSelectorsAbsent && + resourceExplorerRemovalGuard && sidePanelOverflowUsable; const passWithWiring = pass && @@ -6339,6 +6382,9 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) { pass: passWithWiring, removedSelectorStates, removedSelectorsAbsent, + removedVisibleCopyAbsent, + removedVisibleCopyHits, + resourceExplorerRemovalGuard, rootScrollLocked, rootAfterScrollAttempt, boxes, @@ -6383,7 +6429,15 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) { skip, failures }; - }, { mode, viewport, compareTo }); + }, { + mode, + viewport, + compareTo, + resourceExplorerGuard: { + domSelectors: [...removedResourceExplorerDomSelectors], + copyTerms: [...removedResourceExplorerCopyTerms] + } + }); } async function inspectGateLayoutState(page, { mode, viewport }) { @@ -6644,6 +6698,44 @@ function allViewportCoverage(viewportResults, fieldName) { ); } +function summarizeResourceExplorerRemovalCoverage(viewportResults) { + const coverageEntries = Object.entries(viewportResults).map(([id, result]) => { + const observation = result.default; + return [ + id, + { + viewport: observation?.viewport ?? null, + covered: Boolean(observation?.resourceExplorerRemovalGuard), + removedSelectorsAbsent: observation?.removedSelectorsAbsent === true, + removedVisibleCopyAbsent: observation?.removedVisibleCopyAbsent === true, + removedVisibleCopyHits: observation?.removedVisibleCopyHits ?? [], + removedSelectorStates: observation?.removedSelectorStates ?? {} + } + ]; + }); + const coverage = Object.fromEntries(coverageEntries); + const failures = coverageEntries + .filter(([, item]) => !item.covered) + .map(([id, item]) => ({ + checkId: "layout-feedback-352-resource-explorer-removed", + viewport: item.viewport, + selector: "#resource-explorer,#explorer-resize", + failureType: "blocked/skip", + summary: `#352 resource explorer removal guard failed in ${id} viewport.`, + removedSelectorsAbsent: item.removedSelectorsAbsent, + removedVisibleCopyAbsent: item.removedVisibleCopyAbsent, + removedVisibleCopyHits: item.removedVisibleCopyHits, + removedSelectorStates: item.removedSelectorStates + })); + return { + status: failures.length === 0 ? "pass" : "blocked", + forbiddenSelectors: [...removedResourceExplorerDomSelectors], + forbiddenCopy: [...removedResourceExplorerCopyTerms], + coverage, + failures + }; +} + function summarizeCoverageGap(viewportResults, issueKey) { const fieldName = { issue288: "issue288SingleTableReady", diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index fc28c224..5b791203 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -140,6 +140,7 @@ const workbenchSmoke = runDevCloudWorkbenchStaticSmoke(); const helpSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "help-md-contract"); const outerScrollSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "outer-scroll-contract"); const mobileLayoutSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "mobile-workbench-layout-contract"); +const resourceExplorerRemovalSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "feedback-352-resource-explorer-removed"); const conversationUxSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "code-agent-conversation-ux-states"); const codeAgentTimeoutSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "code-agent-long-timeout-contract"); const codeAgentQuickPromptsSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "code-agent-quick-prompts-contract"); @@ -183,6 +184,7 @@ assert.equal(workbenchSmoke.devLive, false); assert.equal(helpSmokeCheck?.status, "pass", "PR #114 help Markdown route must be present and ready"); assert.equal(outerScrollSmokeCheck?.status, "pass", "desktop workbench shell must lock outer page scrolling"); assert.equal(mobileLayoutSmokeCheck?.status, "pass", "mobile 390x844 workbench layout contract must be present"); +assert.equal(resourceExplorerRemovalSmokeCheck?.status, "pass", "#352 resource explorer removal source contract must be present"); assert.equal(conversationUxSmokeCheck?.status, "pass", "Code Agent conversation UX states must remain distinct"); assert.equal(codeAgentTimeoutSmokeCheck?.status, "pass", "Code Agent chat must not reuse the 4500ms light API timeout"); assert.equal(codeAgentQuickPromptsSmokeCheck?.status, "pass", "Code Agent quick prompts must remain fill-only with explicit send semantics for writes"); @@ -239,6 +241,11 @@ assert.equal( "pass", "#276 wiring table must stay readable, non-overlapping, and horizontally contained" ); +assert.equal( + layoutSmoke.checks.find((check) => check.id === "layout-feedback-352-resource-explorer-removed")?.status, + "pass", + "#352 resource explorer removal must remain covered by desktop and 390x844 layout smoke" +); assert.equal( layoutSmoke.checks.find((check) => check.id === "layout-issue-288-future-single-table-gate")?.status, "skip", @@ -248,6 +255,7 @@ for (const layoutCheckId of [ "layout-desktop-default", "layout-narrow-desktop-default", "layout-mobile-default", + "layout-feedback-352-resource-explorer-removed", "layout-right-sidebar-resize-desktop", "layout-right-sidebar-resize-narrow-desktop", "layout-right-sidebar-resize-mobile", @@ -413,6 +421,8 @@ for (const removedSelectorTerm of [ } for (const removedUiText of [ "资源浏览与硬件资源树", + "资源浏览", + "硬件资源树", "只读工作区", "常用能力", "收起资源树",