diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index 0ed04fb7..1127a7e7 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -942,6 +942,12 @@ export async function runDevCloudWorkbenchMobileSmoke() { }); await page.goto(server.url, { waitUntil: "networkidle", timeout: 15000 }); const closed = await inspectMobileWorkbench(page, { explorerOpen: false }); + const workspaceScroll = await inspectWorkbenchScrollContract(page, { panelSelector: "#conversation-list" }); + await page.click('[data-route="help"]'); + await page.waitForFunction(() => document.querySelector("#help-content")?.dataset.helpState === "ready", null, { timeout: 8000 }); + const help = await inspectHelpMarkdownRoute(page); + await page.click('[data-route="workspace"]'); + await page.waitForFunction(() => document.querySelector('[data-view="workspace"]')?.hidden === false, null, { timeout: 8000 }); await page.click("#explorer-toggle"); const open = await inspectMobileWorkbench(page, { explorerOpen: true }); await page.click('[data-side-tab-jump="wiring"]'); @@ -960,6 +966,25 @@ export async function runDevCloudWorkbenchMobileSmoke() { visibleText: closed.visibleText } }, + { + id: "mobile-outer-scroll-lock", + status: workspaceScroll.outerScrollLocked && help.rootStillLocked && help.helpPanelScrolls ? "pass" : "blocked", + summary: "390x844 page locks outer scrolling while the internal Markdown help pane owns overflow scrolling.", + observations: { + workspace: workspaceScroll, + help: { + rootStillLocked: help.rootStillLocked, + helpPanelScrolls: help.helpPanelScrolls, + helpScroll: help.helpScroll + } + } + }, + { + id: "mobile-help-markdown-route", + status: help.nonDefaultMarkdownHelp && help.termsPresent ? "pass" : "blocked", + summary: "Help opens as a non-default internal route rendered from help.md by the vendored Markdown renderer.", + observations: help + }, { id: "mobile-default-hit-test", status: closed.primaryControlsReachable ? "pass" : "blocked", @@ -1197,6 +1222,130 @@ async function inspectMobileWorkbench(page, { explorerOpen }) { }, { explorerOpen }); } +async function inspectWorkbenchScrollContract(page, { panelSelector }) { + return page.evaluate(async ({ panelSelector }) => { + function metrics(selector) { + const element = selector === "html" ? document.documentElement : selector === "body" ? document.body : document.querySelector(selector); + if (!element) return null; + const style = getComputedStyle(element); + return { + selector, + overflow: style.overflow, + overflowY: style.overflowY, + height: element.getBoundingClientRect().height, + clientHeight: element.clientHeight, + scrollHeight: element.scrollHeight, + scrollTop: element.scrollTop + }; + } + + window.scrollTo(0, 240); + document.documentElement.scrollTop = 240; + document.body.scrollTop = 240; + await new Promise((resolve) => requestAnimationFrame(resolve)); + const rootAfterScrollAttempt = { + windowScrollY: window.scrollY, + htmlScrollTop: document.documentElement.scrollTop, + bodyScrollTop: document.body.scrollTop + }; + + const panel = document.querySelector(panelSelector); + const beforePanelScroll = panel?.scrollTop ?? null; + if (panel) { + panel.scrollTop = 0; + await new Promise((resolve) => requestAnimationFrame(resolve)); + panel.scrollTop = panel.scrollHeight; + await new Promise((resolve) => requestAnimationFrame(resolve)); + } + const afterPanelScroll = panel?.scrollTop ?? null; + const html = metrics("html"); + const body = metrics("body"); + const shell = metrics("[data-app-shell]"); + const panelMetrics = metrics(panelSelector); + const rootScrollLocked = + html?.overflow === "hidden" && + body?.overflow === "hidden" && + Math.abs((shell?.height ?? 0) - window.innerHeight) <= 2 && + document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 && + document.body.scrollHeight <= document.body.clientHeight + 2 && + window.scrollY === 0 && + document.documentElement.scrollTop === 0 && + document.body.scrollTop === 0; + return { + viewport: { width: window.innerWidth, height: window.innerHeight }, + outerScrollLocked: rootScrollLocked, + panelScrolls: Boolean(panelMetrics && panelMetrics.scrollHeight > panelMetrics.clientHeight && afterPanelScroll > beforePanelScroll), + rootAfterScrollAttempt, + html, + body, + shell, + panel: panelMetrics + }; + }, { panelSelector }); +} + +async function inspectHelpMarkdownRoute(page) { + return page.evaluate(async () => { + const workspace = document.querySelector('[data-view="workspace"]'); + const gate = document.querySelector('[data-view="gate"]'); + const help = document.querySelector('[data-view="help"]'); + const helpContent = document.querySelector("#help-content"); + const heading = helpContent?.querySelector("h1"); + const renderedLists = helpContent?.querySelectorAll("li").length ?? 0; + const codeCount = helpContent?.querySelectorAll("code").length ?? 0; + const text = helpContent?.textContent ?? ""; + const helpScroll = await (async () => { + if (!helpContent) return null; + helpContent.scrollTop = 0; + await new Promise((resolve) => requestAnimationFrame(resolve)); + const before = helpContent.scrollTop; + helpContent.scrollTop = helpContent.scrollHeight; + await new Promise((resolve) => requestAnimationFrame(resolve)); + return { + before, + after: helpContent.scrollTop, + clientHeight: helpContent.clientHeight, + scrollHeight: helpContent.scrollHeight, + overflowY: getComputedStyle(helpContent).overflowY + }; + })(); + const rootStillLocked = + getComputedStyle(document.documentElement).overflow === "hidden" && + getComputedStyle(document.body).overflow === "hidden" && + document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2 && + document.body.scrollHeight <= document.body.clientHeight + 2; + return { + hash: window.location.hash, + helpState: helpContent?.dataset.helpState ?? null, + workspaceHidden: workspace ? workspace.hidden : null, + gateHidden: gate ? gate.hidden : null, + helpHidden: help ? help.hidden : null, + heading: heading?.textContent?.trim() ?? null, + renderedLists, + codeCount, + hasRawMarkdownHeading: Boolean(helpContent?.textContent?.includes("# 云工作台内部使用说明")), + termsPresent: + ["左侧资源与功能导航", "Agent 对话与工作清单", "same-origin", "/v1", ":16666"].every((term) => text.includes(term)) && + codeCount >= 8, + helpPanelScrolls: Boolean(helpScroll && helpScroll.scrollHeight > helpScroll.clientHeight && helpScroll.after > helpScroll.before), + rootStillLocked, + helpScroll, + nonDefaultMarkdownHelp: + window.location.hash === "#help" && + workspace?.hidden === true && + gate?.hidden === true && + help?.hidden === false && + helpContent?.dataset.helpState === "ready" && + heading?.textContent?.trim() === "云工作台内部使用说明" && + renderedLists >= 8 && + codeCount >= 8 && + !helpContent?.textContent?.includes("# 云工作台内部使用说明") && + rootStillLocked && + Boolean(helpScroll && helpScroll.scrollHeight > helpScroll.clientHeight && helpScroll.after > helpScroll.before) + }; + }); +} + function escapeRegExp(value) { return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); } diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index b4f8379a..543ecd37 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -199,6 +199,8 @@ assert.match(app, /return "workspace";/); assert.match(styles, /\.help-view\s*{[^}]*overflow:\s*hidden;/s); assert.match(styles, /\.help-panel\s*{[^}]*overflow:\s*hidden;/s); assert.match(styles, /\.help-content\s*{[^}]*overflow:\s*auto;/s); +assert.match(styles, /body\s*>\s*\[data-app-shell\]\s*{[^}]*height:\s*100%;[^}]*overflow:\s*hidden;[^}]*overscroll-behavior:\s*contain;/s); +assert.match(styles, /\.workbench-shell\s*{[^}]*height:\s*100dvh;[^}]*max-height:\s*100dvh;[^}]*overflow:\s*hidden;[^}]*overscroll-behavior:\s*contain;/s); assert.match(helpMarkdown, /^# 云工作台内部使用说明/m); for (const helpTerm of [ "左侧资源与功能导航", diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css index 153f6e5d..3e581917 100644 --- a/web/hwlab-cloud-web/styles.css +++ b/web/hwlab-cloud-web/styles.css @@ -42,7 +42,10 @@ body { } body > [data-app-shell] { + height: 100%; min-height: 0; + overflow: hidden; + overscroll-behavior: contain; } button, @@ -66,11 +69,13 @@ ul { .workbench-shell { height: 100vh; height: 100dvh; + max-height: 100dvh; min-height: 0; display: grid; grid-template-columns: 92px 292px minmax(460px, 1fr) 366px; grid-template-rows: 1fr; overflow: hidden; + overscroll-behavior: contain; background: rgba(15, 17, 16, 0.88); }