141 lines
8.4 KiB
TypeScript
141 lines
8.4 KiB
TypeScript
import { expect } from "vitest";
|
|
import { clickButton, control, createAndSelectWorkspace, evalIn, seedSessions, waitFor } from "@openwork/behaviors";
|
|
import { desktop } from "@openwork/hosts";
|
|
import { needs, test } from "@openwork/testkit";
|
|
|
|
const enabled = process.env.OPENWORK_EVAL_E2E_TESTS === "1";
|
|
const title = enabled
|
|
? "Mermaid renders safely in completed chat and Markdown artifacts"
|
|
: "Mermaid rendering skipped — needs: set OPENWORK_EVAL_E2E_TESTS=1";
|
|
|
|
test.skipIf(!enabled)(title, async () => {
|
|
needs({ optIn: ["OPENWORK_EVAL_E2E_TESTS"] });
|
|
await using app = await desktop({
|
|
name: "mermaid-rendering",
|
|
mode: process.env.OPENWORK_EVAL_CDP_URL?.trim() ? "attach" : "spawn",
|
|
env: { OPENWORK_ELECTRON_START_URL: "", PORT: "0" },
|
|
});
|
|
await createAndSelectWorkspace(app, { path: `/tmp/openwork-mermaid-rendering-${Date.now()}` });
|
|
await seedSessions(app, ["Mermaid rendering proof"]);
|
|
await waitFor(app, `window.__openworkControl.listActions().some((action) => action.id === "eval.markdown_primitive.seed_chat" && !action.disabled)`, {
|
|
timeoutMs: 30_000,
|
|
label: "Mermaid chat fixture available",
|
|
});
|
|
|
|
await control(app, "eval.mermaid.set_theme", { mode: "light" });
|
|
await waitFor(app, `document.documentElement.dataset.theme === "light"`, { timeoutMs: 10_000, label: "light theme applied" });
|
|
await control(app, "eval.markdown_primitive.seed_chat");
|
|
await waitFor(app, `(() => {
|
|
const diagrams = Array.from(document.querySelectorAll("[data-openwork-mermaid]"));
|
|
return diagrams.length === 4 && diagrams.every((diagram) => diagram.getAttribute("aria-busy") === "false");
|
|
})()`, { timeoutMs: 30_000, label: "all chat Mermaid diagrams settled" });
|
|
|
|
const chatContract = await evalIn(app, `(() => {
|
|
const diagrams = Array.from(document.querySelectorAll("[data-openwork-mermaid]"));
|
|
const find = (text) => diagrams.find((diagram) => diagram.querySelector("[data-openwork-mermaid-source]")?.textContent?.includes(text));
|
|
const valid = find("Inline Mermaid Start");
|
|
const remote = find("No remote resources");
|
|
const malformed = find("not-a-mermaid-diagram");
|
|
const guarded = find("Guard node 250");
|
|
const sourceVisible = (diagram) => diagram?.querySelector("[data-openwork-mermaid-source]")?.hidden === false;
|
|
return {
|
|
validRendered: valid?.dataset.openworkMermaidState === "rendered" && Boolean(valid.querySelector("svg")),
|
|
controls: Boolean(valid?.querySelector("[data-openwork-mermaid-view='source']") && valid.querySelector("[data-openwork-mermaid-view='rendered']") && valid.querySelector("[data-openwork-mermaid-download]:not([hidden])")),
|
|
unsafeFallback: remote?.dataset.openworkMermaidReason === "unsafe" && sourceVisible(remote) && !remote.querySelector("svg"),
|
|
malformedFallback: malformed?.dataset.openworkMermaidReason === "invalid" && sourceVisible(malformed) && !malformed.querySelector("svg"),
|
|
guardFallback: guarded?.dataset.openworkMermaidReason === "complexity" && sourceVisible(guarded) && !guarded.querySelector("svg"),
|
|
lightTheme: valid?.dataset.openworkMermaidTheme === "light",
|
|
states: diagrams.map((diagram) => ({
|
|
source: diagram.querySelector("[data-openwork-mermaid-source]")?.textContent?.slice(0, 40),
|
|
state: diagram.getAttribute("data-openwork-mermaid-state"),
|
|
reason: diagram.getAttribute("data-openwork-mermaid-reason"),
|
|
theme: diagram.getAttribute("data-openwork-mermaid-theme"),
|
|
})),
|
|
};
|
|
})()`);
|
|
expect(chatContract, JSON.stringify(chatContract)).toMatchObject({
|
|
validRendered: true,
|
|
controls: true,
|
|
unsafeFallback: true,
|
|
malformedFallback: true,
|
|
guardFallback: true,
|
|
lightTheme: true,
|
|
});
|
|
|
|
const toggleContract = await evalIn(app, `(() => {
|
|
const diagram = Array.from(document.querySelectorAll("[data-openwork-mermaid]")).find((item) => item.textContent?.includes("Inline Mermaid Start"));
|
|
diagram?.querySelector("[data-openwork-mermaid-view='source']")?.click();
|
|
const source = diagram?.dataset.openworkMermaidState === "source" && diagram.querySelector("[data-openwork-mermaid-source]")?.hidden === false;
|
|
diagram?.querySelector("[data-openwork-mermaid-view='rendered']")?.click();
|
|
return source && diagram?.dataset.openworkMermaidState === "rendered";
|
|
})()`);
|
|
expect(toggleContract).toBe(true);
|
|
|
|
const downloadContract = await evalIn(app, `(async () => {
|
|
const diagram = Array.from(document.querySelectorAll("[data-openwork-mermaid]")).find((item) => item.textContent?.includes("Inline Mermaid Start"));
|
|
const button = diagram?.querySelector("[data-openwork-mermaid-download]");
|
|
if (!button) return false;
|
|
return await new Promise((resolve) => {
|
|
const listener = (event) => {
|
|
if (!(event.target instanceof HTMLAnchorElement) || !event.target.download.endsWith(".svg")) return;
|
|
event.preventDefault();
|
|
clearTimeout(timer);
|
|
document.removeEventListener("click", listener, true);
|
|
resolve(event.target.href.startsWith("blob:"));
|
|
};
|
|
const timer = setTimeout(() => {
|
|
document.removeEventListener("click", listener, true);
|
|
resolve(false);
|
|
}, 2_000);
|
|
document.addEventListener("click", listener, true);
|
|
button.click();
|
|
});
|
|
})()`, { awaitPromise: true });
|
|
expect(downloadContract).toBe(true);
|
|
|
|
await control(app, "eval.mermaid.set_theme", { mode: "dark" });
|
|
expect(await evalIn(app, `(() => {
|
|
const diagram = Array.from(document.querySelectorAll("[data-openwork-mermaid]")).find((item) => item.textContent?.includes("Inline Mermaid Start"));
|
|
diagram?.querySelector("[data-openwork-mermaid-view='source']")?.click();
|
|
return diagram?.dataset.openworkMermaidState === "source";
|
|
})()`)).toBe(true);
|
|
await waitFor(app, `(() => {
|
|
const diagram = Array.from(document.querySelectorAll("[data-openwork-mermaid]")).find((item) => item.textContent?.includes("Inline Mermaid Start"));
|
|
return diagram?.getAttribute("data-openwork-mermaid-theme") === "dark" && diagram?.getAttribute("data-openwork-mermaid-state") === "source";
|
|
})()`, {
|
|
timeoutMs: 30_000,
|
|
label: "dark theme rerender preserved the selected source view",
|
|
});
|
|
expect(await evalIn(app, `document.documentElement.dataset.theme === "dark" && Boolean(document.querySelector("[data-openwork-mermaid-theme='dark'] svg")) && document.querySelector("[data-openwork-mermaid-theme='dark'] [data-openwork-mermaid-source]")?.hidden === false`)).toBe(true);
|
|
|
|
await control(app, "browser.open_url", { url: "about:blank" }).catch(() => undefined);
|
|
await waitFor(app, `window.__openworkControl.listActions().some((action) => action.id === "eval.markdown_primitive.seed_artifact" && !action.disabled)`, {
|
|
timeoutMs: 30_000,
|
|
label: "Mermaid artifact fixture available",
|
|
});
|
|
await control(app, "eval.markdown_primitive.seed_artifact");
|
|
await clickButton(app, "markdown-primitive-proof.md");
|
|
await waitFor(app, `Boolean(document.querySelector("[data-openwork-markdown-preview] [data-openwork-mermaid-state='rendered'] svg"))`, {
|
|
timeoutMs: 30_000,
|
|
label: "fenced Mermaid rendered in Markdown artifact preview",
|
|
});
|
|
expect(await evalIn(app, `document.querySelector("[data-openwork-markdown-preview] [data-openwork-mermaid-source]")?.textContent?.includes("Artifact Mermaid Start")`)).toBe(true);
|
|
|
|
await control(app, "eval.markdown_primitive.seed_artifact", { standalone: true });
|
|
await clickButton(app, "standalone-mermaid-proof.mmd");
|
|
await waitFor(app, `Boolean(document.querySelector("[data-openwork-mermaid-artifact] [data-openwork-mermaid-state='rendered'] svg"))`, {
|
|
timeoutMs: 30_000,
|
|
label: "standalone mmd artifact rendered",
|
|
});
|
|
expect(await evalIn(app, `document.body.innerText.includes("standalone-mermaid-proof.mmd") && Array.from(document.querySelectorAll("button")).some((button) => button.textContent?.trim() === "Edit")`)).toBe(true);
|
|
expect(await evalIn(app, `(() => {
|
|
const button = document.querySelector("button[aria-label='Artifact actions']");
|
|
button?.click();
|
|
return Boolean(button);
|
|
})()`)).toBe(true);
|
|
const rawDownloadSelector = `[role="menuitem"]`;
|
|
await waitFor(app, `Array.from(document.querySelectorAll(${JSON.stringify(rawDownloadSelector)})).some((item) => item.textContent?.trim() === "Download")`, { timeoutMs: 10_000, label: "raw Mermaid artifact download action" });
|
|
expect(await evalIn(app, `Array.from(document.querySelectorAll(${JSON.stringify(rawDownloadSelector)})).some((item) => item.textContent?.trim() === "Download")`)).toBe(true);
|
|
|
|
});
|