Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions scripts/docs-site/diagram-interactions.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { test } from "node:test";
import { chromium, webkit } from "playwright";
import { fixture } from "./test-helpers/redirect-fixture.mjs";

const source = "flowchart LR\n A[\"Chat apps and plugins\"] --> B[Gateway]\n B --> C[\"OpenClaw agent\"]\n B --> D[\"Web Control UI\"]";
const markdown = "# Diagram interactions\n\n```mermaid\n" + source + "\n```\n\n```mermaid\nflowchart LR\n Broken -->\n```";

for (const [name, engine] of [["Chromium", chromium], ["WebKit", webkit]]) {
test(name + " preserves diagram source, error fallback and expansion controls", async t => {
const f = fixture(t, [], { "guide.md": markdown });
fs.symlinkSync(path.resolve("node_modules"), path.join(f.root, "node_modules"), process.platform === "win32" ? "junction" : "dir");
const built = f.build();
assert.equal(built.status, 0, built.stderr);
const site = path.join(f.root, "dist/docs-site");
const browser = await engine.launch({ headless: true });
t.after(() => browser.close());
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
page.setDefaultTimeout(10000);
await page.route("**/*", route => {
const url = new URL(route.request().url());
if (url.origin !== "https://docs.example") return route.abort();
let file = path.join(site, url.pathname);
if (fs.existsSync(file) && fs.statSync(file).isDirectory()) file = path.join(file, "index.html");
if (!fs.existsSync(file)) return route.fulfill({ status: 404 });
const contentType = ({ ".html": "text/html", ".js": "text/javascript", ".mjs": "text/javascript", ".css": "text/css", ".woff2": "font/woff2", ".svg": "image/svg+xml" })[path.extname(file)] || "application/octet-stream";
return route.fulfill({ contentType, body: fs.readFileSync(file) });
});
await page.goto("https://docs.example/guide");
await page.locator(".oc-mermaid.is-rendered > svg").waitFor();
assert.equal(await page.locator(".oc-mermaid").first().getAttribute("data-mermaid"), source);
assert.ok(fs.readFileSync(path.join(site, "guide.md"), "utf8").includes(source));
assert.ok(JSON.parse(await page.locator("[data-page-markdown]").textContent()).includes(source));
assert.match(await page.locator(".oc-mermaid.is-error code").textContent(), /Broken -->/);
await page.locator("[data-mermaid-expand]").click();
const expanded = page.locator("[data-mermaid-overlay-canvas] > svg");
await expanded.waitFor({ state: "visible" });
const geometry = await expanded.evaluate(svg => ({ width: svg.getBoundingClientRect().width, naturalWidth: svg.viewBox.baseVal.width }));
assert.ok(geometry.width >= geometry.naturalWidth - 1, "expansion must not shrink labels to fit a narrow screen");
const canvas = page.locator("[data-mermaid-overlay-canvas]");
await canvas.evaluate(element => { element.scrollLeft = element.scrollWidth; });
assert.ok(await expanded.evaluate(svg => svg.getBoundingClientRect().right <= svg.parentElement.getBoundingClientRect().right + 1), "the expanded diagram end must remain reachable by scrolling");
await page.keyboard.press("Tab");
assert.ok(await page.locator("[data-mermaid-overlay]").evaluate(overlay => overlay.contains(document.activeElement)));
await page.keyboard.press("Escape");
assert.equal(await page.locator("[data-mermaid-overlay].open").count(), 0);
assert.ok(await page.locator("[data-mermaid-expand]").evaluate(button => button === document.activeElement));
assert.ok(await page.evaluate(() => !document.body.classList.contains("has-mermaid-overlay")));
});
}
3 changes: 3 additions & 0 deletions scripts/docs-site/docs-layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -768,3 +768,6 @@
@media (min-width:821px) {
.docs-layout:dir(rtl) :is(.docs-sidebar-directory, .docs-sidebar-context > .docs-sections) { padding-block-end: 88px; }
}

/* Renderers put responsive max-width inline; expansion must stay readable and scroll. */
.docs-layout .oc-mermaid-overlay-canvas > svg { max-width: none !important; }
2 changes: 1 addition & 1 deletion scripts/docs-site/site-js.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ function mermaidExpandButton(){return '<div class="oc-mermaid-actions"><button t
function ensureMermaidOverlay(){let overlay=document.querySelector("[data-mermaid-overlay]");if(overlay)return overlay;overlay=document.createElement("div");overlay.className="oc-mermaid-overlay";overlay.dataset.mermaidOverlay="";overlay.setAttribute("role","dialog");overlay.setAttribute("aria-modal","true");overlay.setAttribute("aria-label","Expanded diagram");overlay.innerHTML='<div class="oc-mermaid-overlay-head"><button type="button" class="oc-mermaid-overlay-close" data-mermaid-overlay-close aria-label="Close expanded diagram">'+mermaidCloseIcon+'</button></div><div class="oc-mermaid-overlay-canvas" data-mermaid-overlay-canvas></div>';document.body.appendChild(overlay);return overlay}
function closeMermaidOverlay(){const overlay=document.querySelector("[data-mermaid-overlay]");if(!overlay?.classList.contains("open"))return;mermaidOverlayRender+=1;overlay.classList.remove("open");document.body.classList.remove("has-mermaid-overlay");overlay.querySelector("[data-mermaid-overlay-canvas]").innerHTML="";lastMermaidTrigger?.focus?.();lastMermaidTrigger=null}
function handleMermaidOverlayKeydown(e){const overlay=document.querySelector("[data-mermaid-overlay].open");if(!overlay)return false;if(e.key==="Escape"){e.preventDefault();e.stopImmediatePropagation();closeMermaidOverlay();return true}if(isSearchShortcutEvent(e)){e.preventDefault();e.stopImmediatePropagation();return true}if(e.key!=="Tab")return false;const controls=[...overlay.querySelectorAll("button:not([disabled]),[href],[tabindex]:not([tabindex='-1'])")].filter(control=>!control.hidden&&control.getAttribute("aria-hidden")!=="true"&&control.offsetParent!==null);const first=controls[0];const last=controls.at(-1);if(!first){e.preventDefault();return true}if(!overlay.contains(document.activeElement)||e.shiftKey&&document.activeElement===first||!e.shiftKey&&document.activeElement===last){e.preventDefault();(e.shiftKey?last:first).focus();return true}return false}
async function openMermaidOverlay(control){const block=control.closest("[data-mermaid]");const source=block?.dataset.mermaid||"";if(!source)return;const renderToken=++mermaidOverlayRender;lastMermaidTrigger=control;const overlay=ensureMermaidOverlay();const canvas=overlay.querySelector("[data-mermaid-overlay-canvas]");canvas.innerHTML="<pre><code>"+escapeHtml(source)+"</code></pre>";overlay.classList.add("open");document.body.classList.add("has-mermaid-overlay");overlay.querySelector("[data-mermaid-overlay-close]")?.focus();const renderId="oc_mermaid_overlay_"+Date.now();try{const {svg}=await renderMermaidSvg(source,renderId);if(renderToken!==mermaidOverlayRender||!overlay.classList.contains("open")){removeMermaidRenderArtifact(renderId);return}canvas.innerHTML=svg}catch{removeMermaidRenderArtifact(renderId)}}
async function openMermaidOverlay(control){const block=control.closest("[data-mermaid]");const source=block?.dataset.mermaid||"";if(!source)return;const renderToken=++mermaidOverlayRender;lastMermaidTrigger=control;const overlay=ensureMermaidOverlay();const canvas=overlay.querySelector("[data-mermaid-overlay-canvas]");canvas.innerHTML="<pre><code>"+escapeHtml(source)+"</code></pre>";overlay.classList.add("open");document.body.classList.add("has-mermaid-overlay");overlay.querySelector("[data-mermaid-overlay-close]")?.focus();const renderId="oc_mermaid_overlay_"+Date.now();try{const {svg}=await renderMermaidSvg(source,renderId);if(renderToken!==mermaidOverlayRender||!overlay.classList.contains("open")){removeMermaidRenderArtifact(renderId);return}canvas.innerHTML=svg;const diagram=canvas.firstElementChild;diagram.style.width=diagram.viewBox.baseVal.width+"px"}catch{removeMermaidRenderArtifact(renderId)}}
async function initMermaid(force=false){const blocks=[...document.querySelectorAll("[data-mermaid]")].filter(block=>force||!block.dataset.mermaidReady);if(!blocks.length)return;let i=0;for(const block of blocks){const source=block.dataset.mermaid||"";if(force)resetMermaidBlock(block,source);const renderId="oc_mermaid_"+Date.now()+"_"+i++;try{const {svg}=await renderMermaidSvg(source,renderId);block.innerHTML=mermaidExpandButton()+svg;block.dataset.mermaidReady="true";block.classList.remove("is-error");block.classList.add("is-rendered")}catch(err){removeMermaidRenderArtifact(renderId);resetMermaidBlock(block,source);block.dataset.mermaidReady="true";block.classList.add("is-error");block.insertAdjacentHTML("afterbegin",'<div class="oc-mermaid-error"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4M12 17h.01"/></svg><span>Diagram could not be rendered</span></div>')}}}
function setCopyFeedback(control,state){if(!control)return;if(control.matches("[data-code-copy]")||control.matches("[data-prompt-copy]")||control.matches("[data-chat-copy]")||control.matches("[data-copy-page]")||control.matches("[data-heading-anchor]")){const label=control.dataset.copyLabel||control.getAttribute("aria-label")||"Copy";control.dataset.copyLabel=label;const feedback=control.querySelector("[data-copy-feedback]");const old=control.dataset.copyDefaultText||feedback?.textContent||label;control.dataset.copyDefaultText=old;clearTimeout(control._copyStateTimer);if(state==="copying"){control.dataset.copyState="copying";control.setAttribute("aria-label","Copying");if(feedback)feedback.textContent="Copying...";return}const ok=state==="copied";control.dataset.copyState=ok?"copied":"failed";control.setAttribute("aria-label",ok?"Copied":"Copy failed");if(control.hasAttribute("title"))control.title=ok?"Copied":"Copy failed";if(feedback)feedback.textContent=ok?"Copied":"Copy failed";control._copyStateTimer=setTimeout(()=>{delete control.dataset.copyState;control.setAttribute("aria-label",label);if(control.hasAttribute("title"))control.title=label;if(feedback)feedback.textContent=old},1300);return}const old=control.textContent;control.textContent=state==="copied"?"Copied":"Copy failed";setTimeout(()=>control.textContent=old||"Copy",1300)}
function showCopyFeedback(control,ok){setCopyFeedback(control,ok?"copied":"failed")}
Expand Down
Loading