test: verify workbench scroll and Markdown help

Host commander review: merged PR #170 after checking the diff and runner validation. Scope is limited to Cloud Workbench scroll lock, Markdown help route smoke coverage, and CSS/static checks. No PROD/runtime mutation, no hardware write APIs, and no M3/M4/M5 acceptance claim.
This commit is contained in:
Lyon
2026-05-23 02:03:53 +08:00
committed by GitHub
3 changed files with 156 additions and 0 deletions
@@ -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, "\\$&");
}
+2
View File
@@ -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 [
"左侧资源与功能导航",
+5
View File
@@ -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);
}