Merge pull request #163 from pikasTech/fix/mobile-cloud-workbench-reachability
fix: improve mobile cloud workbench reachability
This commit is contained in:
@@ -1,14 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { parseSmokeArgs, printSmokeHelp, runDevCloudWorkbenchSmoke } from "./src/dev-cloud-workbench-smoke-lib.mjs";
|
||||
import {
|
||||
parseSmokeArgs,
|
||||
printSmokeHelp,
|
||||
runDevCloudWorkbenchMobileSmoke,
|
||||
runDevCloudWorkbenchSmoke
|
||||
} from "./src/dev-cloud-workbench-smoke-lib.mjs";
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
try {
|
||||
const args = parseSmokeArgs(process.argv.slice(2));
|
||||
const report = args.help ? printSmokeHelp() : await runDevCloudWorkbenchSmoke(process.argv.slice(2));
|
||||
const report = args.help
|
||||
? printSmokeHelp()
|
||||
: args.mobile
|
||||
? await runDevCloudWorkbenchMobileSmoke()
|
||||
: await runDevCloudWorkbenchSmoke(process.argv.slice(2));
|
||||
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
process.exitCode = report.status === "pass" || report.status === "usage" ? 0 : 2;
|
||||
process.exitCode = report.status === "pass" || report.status === "usage" || report.status === "skip" ? 0 : 2;
|
||||
} catch (error) {
|
||||
process.stderr.write(`[dev-cloud-workbench-smoke] ${error instanceof Error ? error.stack : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import http from "node:http";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
@@ -157,6 +158,8 @@ export function parseSmokeArgs(argv) {
|
||||
args.mode = "static";
|
||||
} else if (arg === "--live") {
|
||||
args.mode = "live";
|
||||
} else if (arg === "--mobile") {
|
||||
args.mobile = true;
|
||||
} else if (arg === "--url") {
|
||||
index += 1;
|
||||
if (!argv[index]) throw new Error("--url requires a value");
|
||||
@@ -243,6 +246,16 @@ function runStaticSmoke() {
|
||||
evidence: ["html/body overflow hidden", "workbench-shell 100vh/100dvh overflow hidden", ".view overflow auto"]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "mobile-workbench-layout-contract", hasMobileWorkbenchLayoutContract(files), "390x844 mobile layout keeps the default workbench reachable and moves the resource tree into a controlled drawer.", {
|
||||
blocker: "runtime_blocker",
|
||||
evidence: [
|
||||
"syncMobileExplorer collapses resource tree at <=860px",
|
||||
"mobile rail spans full height",
|
||||
"center workspace and right sidebar share the content column",
|
||||
"resource explorer opens as a full-height drawer"
|
||||
]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "same-origin-readonly-boundary", hasSameOriginReadOnlyBoundary(files.app), "Workbench data access stays same-origin and JSON-RPC diagnostics stay read-only.", {
|
||||
blocker: "safety_blocker",
|
||||
evidence: ["/health/live", "/v1", "/v1/agent/chat", "/json-rpc", ...readOnlyRpcMethods]
|
||||
@@ -584,6 +597,26 @@ function hasScrollLockContract(styles) {
|
||||
);
|
||||
}
|
||||
|
||||
function hasMobileWorkbenchLayoutContract({ html, app, styles }) {
|
||||
return (
|
||||
/id=["']resource-explorer["']/u.test(html) &&
|
||||
/aria-controls=["']resource-explorer["']/u.test(html) &&
|
||||
/aria-expanded=["']true["']/u.test(html) &&
|
||||
/function\s+syncMobileExplorer\s*\(/u.test(app) &&
|
||||
/window\.matchMedia\(["']\(max-width:\s*860px\)["']\)/u.test(app) &&
|
||||
/setExplorerCollapsed\(true\)/u.test(app) &&
|
||||
/setExplorerCollapsed\(false\)/u.test(app) &&
|
||||
/function\s+collapseExplorerAfterMobileAction\s*\(/u.test(app) &&
|
||||
/aria-expanded/u.test(functionBody(app, "setExplorerCollapsed")) &&
|
||||
/\.explorer-collapsed\s+\.explorer\s*\{[^\}]*pointer-events:\s*none;/su.test(styles) &&
|
||||
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.activity-rail\s*\{[\s\S]*?grid-row:\s*1\s*\/\s*3;/u.test(styles) &&
|
||||
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.explorer\s*\{[\s\S]*?grid-row:\s*1\s*\/\s*3;[\s\S]*?z-index:\s*4;/u.test(styles) &&
|
||||
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.center-workspace\s*\{[\s\S]*?grid-row:\s*1;/u.test(styles) &&
|
||||
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.right-sidebar\s*\{[\s\S]*?grid-row:\s*2;/u.test(styles) &&
|
||||
/@media\s*\(max-width:\s*520px\)[\s\S]*?\.command-bar\s*\{[\s\S]*?grid-template-columns:\s*minmax\(0,\s*1fr\)\s+auto\s+auto;/u.test(styles)
|
||||
);
|
||||
}
|
||||
|
||||
function hasSameOriginReadOnlyBoundary(app) {
|
||||
const rpcCalls = [...app.matchAll(/callRpc\(\s*["']([^"']+)["']/gu)].map((match) => match[1]).sort();
|
||||
return (
|
||||
@@ -879,6 +912,291 @@ async function inspectLiveDom(url) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function runDevCloudWorkbenchMobileSmoke() {
|
||||
let chromium;
|
||||
try {
|
||||
({ chromium } = await import("playwright"));
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "skip",
|
||||
task: "DC-DCSN-P0-2026-003",
|
||||
mode: "static-mobile-browser",
|
||||
viewport: { width: 390, height: 844 },
|
||||
evidenceLevel: "SOURCE",
|
||||
devLive: false,
|
||||
summary: `Mobile browser smoke skipped because Playwright is unavailable: ${error.message}`,
|
||||
checks: [],
|
||||
blockers: [],
|
||||
safety: staticSafety()
|
||||
};
|
||||
}
|
||||
|
||||
const server = await startStaticWebServer();
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 390, height: 844 },
|
||||
deviceScaleFactor: 1,
|
||||
isMobile: true
|
||||
});
|
||||
await page.goto(server.url, { waitUntil: "networkidle", timeout: 15000 });
|
||||
const closed = await inspectMobileWorkbench(page, { explorerOpen: false });
|
||||
await page.click("#explorer-toggle");
|
||||
const open = await inspectMobileWorkbench(page, { explorerOpen: true });
|
||||
await page.click('[data-side-tab-jump="wiring"]');
|
||||
const afterQuickAction = await inspectMobileWorkbench(page, { explorerOpen: false });
|
||||
const checks = [
|
||||
{
|
||||
id: "mobile-default-workbench",
|
||||
status: closed.defaultWorkbenchReachable ? "pass" : "blocked",
|
||||
summary: "390x844 default route remains the user workbench with Gate/help as secondary entries.",
|
||||
observations: {
|
||||
title: closed.title,
|
||||
workspaceHidden: closed.workspaceHidden,
|
||||
gateHidden: closed.gateHidden,
|
||||
helpHidden: closed.helpHidden,
|
||||
explorerCollapsed: closed.explorerCollapsed,
|
||||
visibleText: closed.visibleText
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "mobile-default-hit-test",
|
||||
status: closed.primaryControlsReachable ? "pass" : "blocked",
|
||||
summary: "Primary workbench controls are center-click reachable with the resource tree closed.",
|
||||
observations: {
|
||||
reachable: closed.reachable,
|
||||
blocked: closed.blocked
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "mobile-resource-drawer-hit-test",
|
||||
status: open.resourceControlsReachable ? "pass" : "blocked",
|
||||
summary: "Opened resource drawer keeps quick actions and resource rows center-click reachable.",
|
||||
observations: {
|
||||
drawer: open.drawer,
|
||||
reachable: open.reachable,
|
||||
blocked: open.blocked
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "mobile-resource-action-collapse",
|
||||
status: afterQuickAction.resourceActionCollapsed ? "pass" : "blocked",
|
||||
summary: "Mobile resource quick actions collapse the drawer so the target workspace panel remains reachable.",
|
||||
observations: {
|
||||
explorerCollapsed: afterQuickAction.explorerCollapsed,
|
||||
wiringSelected: afterQuickAction.wiringSelected,
|
||||
tabHit: afterQuickAction.reachable.find((target) => target.label === "接线")
|
||||
}
|
||||
}
|
||||
];
|
||||
const blockers = checks
|
||||
.filter((check) => check.status !== "pass")
|
||||
.map((check) => ({
|
||||
type: "runtime_blocker",
|
||||
scope: check.id,
|
||||
status: "open",
|
||||
summary: check.summary
|
||||
}));
|
||||
return {
|
||||
status: blockers.length === 0 ? "pass" : "blocked",
|
||||
task: "DC-DCSN-P0-2026-003",
|
||||
mode: "static-mobile-browser",
|
||||
viewport: { width: 390, height: 844 },
|
||||
url: server.url,
|
||||
generatedAt: new Date().toISOString(),
|
||||
evidenceLevel: "SOURCE",
|
||||
devLive: false,
|
||||
summary: "Static local browser smoke only; deployment still requires post-deploy live smoke.",
|
||||
checks,
|
||||
blockers,
|
||||
safety: staticSafety()
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "blocked",
|
||||
task: "DC-DCSN-P0-2026-003",
|
||||
mode: "static-mobile-browser",
|
||||
viewport: { width: 390, height: 844 },
|
||||
url: server.url,
|
||||
generatedAt: new Date().toISOString(),
|
||||
evidenceLevel: "SOURCE",
|
||||
devLive: false,
|
||||
summary: `Mobile browser smoke failed: ${error.message}`,
|
||||
checks: [],
|
||||
blockers: [
|
||||
{
|
||||
type: "runtime_blocker",
|
||||
scope: "mobile-browser-smoke",
|
||||
status: "open",
|
||||
summary: error.message
|
||||
}
|
||||
],
|
||||
safety: staticSafety()
|
||||
};
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
await server.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function startStaticWebServer() {
|
||||
const server = http.createServer(async (request, response) => {
|
||||
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
||||
let pathname = decodeURIComponent(url.pathname);
|
||||
if (pathname === "/" || pathname === "/workbench" || pathname === "/gate" || pathname === "/diagnostics/gate") {
|
||||
pathname = "/index.html";
|
||||
}
|
||||
const filePath = path.resolve(webRoot, pathname.replace(/^\/+/, ""));
|
||||
if (!filePath.startsWith(webRoot)) {
|
||||
response.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
|
||||
response.end("forbidden");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const body = await fs.promises.readFile(filePath);
|
||||
response.writeHead(200, { "content-type": contentTypeFor(filePath) });
|
||||
response.end(body);
|
||||
} catch {
|
||||
response.writeHead(404, { "content-type": "application/json; charset=utf-8" });
|
||||
response.end(JSON.stringify({ ok: false, error: "not_found" }));
|
||||
}
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
return {
|
||||
url: `http://127.0.0.1:${address.port}/`,
|
||||
close: () => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()))
|
||||
};
|
||||
}
|
||||
|
||||
function contentTypeFor(filePath) {
|
||||
if (filePath.endsWith(".html")) return "text/html; charset=utf-8";
|
||||
if (filePath.endsWith(".css")) return "text/css; charset=utf-8";
|
||||
if (filePath.endsWith(".mjs") || filePath.endsWith(".js")) return "text/javascript; charset=utf-8";
|
||||
if (filePath.endsWith(".md")) return "text/markdown; charset=utf-8";
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
async function inspectMobileWorkbench(page, { explorerOpen }) {
|
||||
return page.evaluate(async ({ explorerOpen }) => {
|
||||
function visibleTextFor(selector) {
|
||||
const element = document.querySelector(selector);
|
||||
return element?.textContent?.replace(/\s+/gu, " ").trim() ?? "";
|
||||
}
|
||||
function boxFor(selector) {
|
||||
const element = document.querySelector(selector);
|
||||
if (!element) return null;
|
||||
const box = element.getBoundingClientRect();
|
||||
return {
|
||||
left: box.left,
|
||||
top: box.top,
|
||||
right: box.right,
|
||||
bottom: box.bottom,
|
||||
width: box.width,
|
||||
height: box.height,
|
||||
display: getComputedStyle(element).display
|
||||
};
|
||||
}
|
||||
function ownsHit(element, hit) {
|
||||
return hit === element || element.contains(hit);
|
||||
}
|
||||
function inspectTarget(element, label = element.id || element.textContent?.trim() || element.tagName) {
|
||||
const box = element.getBoundingClientRect();
|
||||
const cx = box.left + box.width / 2;
|
||||
const cy = box.top + box.height / 2;
|
||||
const hit = document.elementFromPoint(cx, cy);
|
||||
const hitLabel = hit ? `${hit.tagName.toLowerCase()}${hit.id ? `#${hit.id}` : ""}${hit.className ? `.${String(hit.className).trim().replace(/\s+/gu, ".")}` : ""}` : "none";
|
||||
return {
|
||||
label: label.replace(/\s+/gu, " ").trim(),
|
||||
ok: box.width > 0 && box.height > 0 && cx >= 0 && cx <= window.innerWidth && cy >= 0 && cy <= window.innerHeight && ownsHit(element, hit),
|
||||
center: { x: cx, y: cy },
|
||||
box: { left: box.left, top: box.top, right: box.right, bottom: box.bottom, width: box.width, height: box.height },
|
||||
hit: hitLabel
|
||||
};
|
||||
}
|
||||
function inspectSelector(selector, label) {
|
||||
const element = document.querySelector(selector);
|
||||
if (!element) {
|
||||
return { label, ok: false, missing: true };
|
||||
}
|
||||
return inspectTarget(element, label);
|
||||
}
|
||||
async function inspectScrollableTargets(selector) {
|
||||
const targets = [...document.querySelectorAll(selector)];
|
||||
const results = [];
|
||||
for (const target of targets) {
|
||||
target.scrollIntoView({ block: "center", inline: "nearest" });
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
results.push(inspectTarget(target));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const defaultTargets = [
|
||||
inspectSelector('[data-route="workspace"]', "工作台"),
|
||||
inspectSelector('[data-route="gate"]', "内部复核"),
|
||||
inspectSelector('[data-route="help"]', "使用说明"),
|
||||
inspectSelector("#explorer-toggle", "资源树"),
|
||||
inspectSelector("#command-input", "Agent 输入"),
|
||||
inspectSelector("#command-send", "发送"),
|
||||
inspectSelector("#command-clear", "清空"),
|
||||
inspectSelector("#tab-control", "控制"),
|
||||
inspectSelector("#tab-wiring", "接线"),
|
||||
inspectSelector("#tab-records", "可信记录")
|
||||
];
|
||||
const drawerTargets = explorerOpen
|
||||
? [
|
||||
...[...document.querySelectorAll(".quick-actions button")].map((element) => inspectTarget(element)),
|
||||
...(await inspectScrollableTargets("#resource-tree summary, #resource-tree .tree-row"))
|
||||
]
|
||||
: [];
|
||||
const reachable = explorerOpen ? drawerTargets : defaultTargets;
|
||||
const blocked = reachable.filter((target) => !target.ok);
|
||||
const text = document.body.textContent ?? "";
|
||||
const workspace = document.querySelector('[data-view="workspace"]');
|
||||
const gate = document.querySelector('[data-view="gate"]');
|
||||
const help = document.querySelector('[data-view="help"]');
|
||||
const shell = document.querySelector("[data-app-shell]");
|
||||
const drawer = boxFor("#resource-explorer");
|
||||
return {
|
||||
title: document.title,
|
||||
workspaceHidden: workspace ? workspace.hidden : null,
|
||||
gateHidden: gate ? gate.hidden : null,
|
||||
helpHidden: help ? help.hidden : null,
|
||||
explorerCollapsed: shell?.classList.contains("explorer-collapsed") ?? null,
|
||||
visibleText: {
|
||||
bodyHasWorkbench: text.includes("用户工作台") && text.includes("Agent 对话"),
|
||||
gateIsSecondaryLabel: text.includes("内部复核"),
|
||||
helpIsSecondaryLabel: text.includes("使用说明"),
|
||||
visibleWorkspaceText: visibleTextFor(".center-workspace").slice(0, 160)
|
||||
},
|
||||
drawer,
|
||||
defaultWorkbenchReachable:
|
||||
document.title === "HWLAB 云工作台" &&
|
||||
workspace?.hidden === false &&
|
||||
gate?.hidden === true &&
|
||||
help?.hidden === true &&
|
||||
text.includes("用户工作台") &&
|
||||
text.includes("Agent 对话") &&
|
||||
text.includes("内部复核") &&
|
||||
text.includes("使用说明"),
|
||||
primaryControlsReachable: defaultTargets.every((target) => target.ok),
|
||||
resourceControlsReachable: drawerTargets.length >= 8 && drawerTargets.every((target) => target.ok),
|
||||
resourceActionCollapsed:
|
||||
!explorerOpen &&
|
||||
shell?.classList.contains("explorer-collapsed") === true &&
|
||||
document.querySelector("#tab-wiring")?.getAttribute("aria-selected") === "true",
|
||||
wiringSelected: document.querySelector("#tab-wiring")?.getAttribute("aria-selected") === "true",
|
||||
reachable,
|
||||
blocked
|
||||
};
|
||||
}, { explorerOpen });
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
}
|
||||
@@ -889,6 +1207,7 @@ export function printSmokeHelp() {
|
||||
command: "node scripts/dev-cloud-workbench-smoke.mjs --static | --live --url http://74.48.78.17:16666/",
|
||||
notes: [
|
||||
"Static mode reads repository files and emits SOURCE-level evidence only.",
|
||||
"--mobile runs a local static 390x844 browser hit-test and still emits SOURCE-level evidence only.",
|
||||
"Live mode is read-only and reports blocked/skip when optional browser DOM evidence is unavailable."
|
||||
]
|
||||
};
|
||||
|
||||
@@ -98,6 +98,7 @@ const state = {
|
||||
|
||||
initRoutes();
|
||||
initExplorerToggle();
|
||||
syncMobileExplorer();
|
||||
initSideTabs();
|
||||
initQuickActions();
|
||||
initCommandBar();
|
||||
@@ -154,11 +155,29 @@ function showView(route) {
|
||||
function initExplorerToggle() {
|
||||
el.explorerToggle.addEventListener("click", () => {
|
||||
const collapsed = !el.shell.classList.contains("explorer-collapsed");
|
||||
el.shell.classList.toggle("explorer-collapsed", collapsed);
|
||||
el.explorerToggle.setAttribute("aria-pressed", collapsed ? "true" : "false");
|
||||
setExplorerCollapsed(collapsed);
|
||||
});
|
||||
}
|
||||
|
||||
function setExplorerCollapsed(collapsed) {
|
||||
el.shell.classList.toggle("explorer-collapsed", collapsed);
|
||||
el.explorerToggle.setAttribute("aria-pressed", collapsed ? "true" : "false");
|
||||
el.explorerToggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
||||
}
|
||||
|
||||
function syncMobileExplorer() {
|
||||
const media = window.matchMedia("(max-width: 860px)");
|
||||
const apply = () => {
|
||||
if (media.matches) {
|
||||
setExplorerCollapsed(true);
|
||||
} else {
|
||||
setExplorerCollapsed(false);
|
||||
}
|
||||
};
|
||||
apply();
|
||||
media.addEventListener("change", apply);
|
||||
}
|
||||
|
||||
async function loadHelpSurface() {
|
||||
el.helpStatus.textContent = "加载中";
|
||||
el.helpStatus.className = "state-tag tone-dry-run";
|
||||
@@ -191,7 +210,10 @@ function initSideTabs() {
|
||||
tab.addEventListener("click", () => selectSideTab(tab.dataset.sideTab));
|
||||
}
|
||||
for (const jump of document.querySelectorAll("[data-side-tab-jump]")) {
|
||||
jump.addEventListener("click", () => selectSideTab(jump.dataset.sideTabJump));
|
||||
jump.addEventListener("click", () => {
|
||||
selectSideTab(jump.dataset.sideTabJump);
|
||||
collapseExplorerAfterMobileAction();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,10 +221,17 @@ function initQuickActions() {
|
||||
for (const button of document.querySelectorAll("[data-focus-command]")) {
|
||||
button.addEventListener("click", () => {
|
||||
el.commandInput.focus();
|
||||
collapseExplorerAfterMobileAction();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function collapseExplorerAfterMobileAction() {
|
||||
if (window.matchMedia("(max-width: 860px)").matches) {
|
||||
setExplorerCollapsed(true);
|
||||
}
|
||||
}
|
||||
|
||||
function selectSideTab(tabId) {
|
||||
for (const tab of document.querySelectorAll("[data-side-tab]")) {
|
||||
const active = tab.dataset.sideTab === tabId;
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
<button class="rail-button" type="button" data-route="gate" title="内部复核" aria-label="内部复核">内部复核</button>
|
||||
<button class="rail-button" type="button" data-route="help" title="使用说明" aria-label="使用说明">使用说明</button>
|
||||
<span class="rail-spacer" aria-hidden="true"></span>
|
||||
<button class="rail-button" id="explorer-toggle" type="button" title="展开或收起资源树" aria-label="展开或收起资源树">资源树</button>
|
||||
<button class="rail-button" id="explorer-toggle" type="button" title="展开或收起资源树" aria-label="展开或收起资源树" aria-controls="resource-explorer" aria-expanded="true">资源树</button>
|
||||
</aside>
|
||||
|
||||
<aside class="explorer" aria-label="资源浏览与硬件资源树">
|
||||
<aside class="explorer" id="resource-explorer" aria-label="资源浏览与硬件资源树">
|
||||
<header class="explorer-head">
|
||||
<div>
|
||||
<p class="eyebrow">HWLAB Cloud</p>
|
||||
|
||||
@@ -3,7 +3,10 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { loadMvpGateSummary } from "../../../internal/mvp-gate/summary.mjs";
|
||||
import { runDevCloudWorkbenchStaticSmoke } from "../../../scripts/src/dev-cloud-workbench-smoke-lib.mjs";
|
||||
import {
|
||||
runDevCloudWorkbenchMobileSmoke,
|
||||
runDevCloudWorkbenchStaticSmoke
|
||||
} from "../../../scripts/src/dev-cloud-workbench-smoke-lib.mjs";
|
||||
import { gateSummary } from "../gate-summary.mjs";
|
||||
import { runCloudWebM3ReadonlyContract } from "./m3-readonly-contract.mjs";
|
||||
import { runWorkbenchHardwarePanelContract } from "./workbench-hardware-panel-contract.mjs";
|
||||
@@ -54,12 +57,23 @@ const requiredTrustedRecordTerms = Object.freeze([
|
||||
]);
|
||||
const workbenchSmoke = runDevCloudWorkbenchStaticSmoke();
|
||||
const helpSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "help-md-contract");
|
||||
const mobileLayoutSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "mobile-workbench-layout-contract");
|
||||
const mobileWorkbenchSmoke = await runDevCloudWorkbenchMobileSmoke();
|
||||
|
||||
assert.equal(workbenchSmoke.status, "pass", JSON.stringify(workbenchSmoke.blockers, null, 2));
|
||||
assert.equal(workbenchSmoke.evidenceLevel, "SOURCE");
|
||||
assert.equal(workbenchSmoke.devLive, false);
|
||||
assert.equal(helpSmokeCheck?.status, "pass", "PR #114 help Markdown route must be present and ready");
|
||||
assert.equal(mobileLayoutSmokeCheck?.status, "pass", "mobile 390x844 workbench layout contract must be present");
|
||||
assert.equal(workbenchSmoke.help.status, "pass", "smoke report must not leave #114 help contract pending");
|
||||
assert.notEqual(mobileWorkbenchSmoke.status, "blocked", JSON.stringify(mobileWorkbenchSmoke.blockers, null, 2));
|
||||
assert.equal(mobileWorkbenchSmoke.evidenceLevel, "SOURCE");
|
||||
assert.equal(mobileWorkbenchSmoke.devLive, false);
|
||||
if (mobileWorkbenchSmoke.status === "pass") {
|
||||
assert.equal(mobileWorkbenchSmoke.viewport.width, 390);
|
||||
assert.equal(mobileWorkbenchSmoke.viewport.height, 844);
|
||||
assert.equal(mobileWorkbenchSmoke.checks.every((check) => check.status === "pass"), true);
|
||||
}
|
||||
|
||||
assert.match(html, /HWLAB 云工作台/);
|
||||
for (const chineseWorkbenchText of [
|
||||
@@ -150,6 +164,7 @@ assert.match(html, /src="\/app\.mjs"/);
|
||||
for (const workbenchElement of [
|
||||
"activity-rail",
|
||||
"explorer",
|
||||
"resource-explorer",
|
||||
"resource-tree",
|
||||
"conversation-list",
|
||||
"agent-chat-status",
|
||||
@@ -165,6 +180,8 @@ for (const workbenchElement of [
|
||||
]) {
|
||||
assert.match(html, new RegExp(workbenchElement));
|
||||
}
|
||||
assert.match(html, /aria-controls="resource-explorer"/);
|
||||
assert.match(html, /aria-expanded="true"/);
|
||||
for (const viewId of ["workspace", "gate"]) {
|
||||
assert.match(html, new RegExp(`data-view="${viewId}"`));
|
||||
}
|
||||
@@ -286,6 +303,17 @@ assert.match(styles, /body\s*>\s*\[data-app-shell\]\s*{[^}]*min-height:\s*0;/s);
|
||||
assert.match(styles, /\.workbench-shell\s*{[^}]*height:\s*100(?:d)?vh;[^}]*overflow:\s*hidden;/s);
|
||||
assert.match(styles, /\.view\s*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s);
|
||||
assert.match(styles, /\.(?:resource-tree|compact-list|conversation-list|trace-list|task-list|hardware-list)[^{]*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s);
|
||||
assert.match(app, /function syncMobileExplorer/);
|
||||
assert.match(app, /window\.matchMedia\("\(max-width: 860px\)"\)/);
|
||||
assert.match(app, /setExplorerCollapsed\(true\)/);
|
||||
assert.match(app, /function collapseExplorerAfterMobileAction/);
|
||||
assert.match(functionBody(app, "setExplorerCollapsed"), /aria-expanded/);
|
||||
assert.match(styles, /\.explorer-collapsed \.explorer\s*{[^}]*pointer-events:\s*none;/s);
|
||||
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.activity-rail\s*{[\s\S]*?grid-row:\s*1 \/ 3;/);
|
||||
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.explorer\s*{[\s\S]*?grid-row:\s*1 \/ 3;[\s\S]*?z-index:\s*4;/);
|
||||
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.center-workspace\s*{[\s\S]*?grid-row:\s*1;/);
|
||||
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.right-sidebar\s*{[\s\S]*?grid-row:\s*2;/);
|
||||
assert.match(styles, /@media \(max-width: 520px\)[\s\S]*?\.command-bar\s*{[\s\S]*?grid-template-columns:\s*minmax\(0, 1fr\) auto auto;/);
|
||||
assert.match(app, /fetchJson\("\/v1"\)/);
|
||||
assert.match(app, /fetchJson\("\/health\/live"\)/);
|
||||
assert.match(app, /callRpc\("system\.health"\)/);
|
||||
|
||||
@@ -147,6 +147,10 @@ ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.explorer-collapsed .explorer {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.explorer-head,
|
||||
.topbar,
|
||||
.hardware-status,
|
||||
@@ -1129,34 +1133,89 @@ tbody tr:last-child td {
|
||||
@media (max-width: 860px) {
|
||||
.workbench-shell,
|
||||
.explorer-collapsed {
|
||||
grid-template-columns: 76px minmax(0, 1fr);
|
||||
grid-template-rows: clamp(150px, 26dvh, 240px) minmax(0, 1fr) clamp(180px, 32dvh, 280px);
|
||||
grid-template-columns: 68px minmax(0, 1fr);
|
||||
grid-template-rows: minmax(0, 1fr) clamp(214px, 30dvh, 280px);
|
||||
}
|
||||
|
||||
.activity-rail {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
grid-template-rows: repeat(3, minmax(42px, auto)) minmax(12px, 1fr) minmax(42px, auto);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rail-button {
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.explorer {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / 3;
|
||||
z-index: 4;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
box-shadow: -1px 0 0 var(--line), 12px 0 24px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.center-workspace,
|
||||
.right-sidebar {
|
||||
grid-column: 1 / 3;
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.center-workspace {
|
||||
grid-row: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.right-sidebar {
|
||||
grid-row: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.explorer-collapsed .explorer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
min-height: 0;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.topbar-main h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.probe-card {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.workbench-view,
|
||||
.gate-view,
|
||||
.help-view,
|
||||
.right-sidebar {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.conversation-column {
|
||||
grid-template-rows: minmax(210px, 1fr) minmax(150px, 0.45fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.command-bar {
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: stretch;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.input-shell {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.command-button {
|
||||
min-width: 48px;
|
||||
padding: 0 9px;
|
||||
}
|
||||
|
||||
.topbar,
|
||||
.command-bar,
|
||||
.gate-split {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -1173,3 +1232,45 @@ tbody tr:last-child td {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.workbench-shell,
|
||||
.explorer-collapsed {
|
||||
grid-template-columns: 64px minmax(0, 1fr);
|
||||
grid-template-rows: minmax(0, 1fr) clamp(200px, 28dvh, 250px);
|
||||
}
|
||||
|
||||
.activity-rail {
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.rail-button {
|
||||
padding: 6px 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.topbar .probe-card {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.command-bar {
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
}
|
||||
|
||||
.prompt-mark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.input-shell {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.command-button {
|
||||
min-width: 45px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user