Files
next-ai-draw-io/tests/unit/mcp-shell-config.test.ts
T
dayuan.jiang 3ef14318f9 fix(mcp-server): review fixes for the preview URL and the theme menu
The stalled-tab note names the page start_session opened (the shell
with DRAWIO_PREVIEW_UI=shell) instead of the classic page. With the
shell not built, start_session opens the classic page and the server
says so at startup, as it does for a missing dist/drawio. BROWSER=none
skips the system browser, as Vite and CRA dev servers do: the e2e tests
set it. Without a fixed DRAWIO_UI, the shell's draw.io URL carries
themes=1 so Extras > Theme is offered, as on the classic page.
2026-10-11 20:56:07 +09:00

144 lines
5.3 KiB
TypeScript

/**
* The MCP's canvas shell takes its settings from the config the server
* writes into the page, and picks its language and theme in the browser.
*/
import { readFileSync } from "node:fs"
import { join } from "node:path"
import { describe, expect, it } from "vitest"
import { getDrawioSrc } from "@/lib/drawio/drawio-config"
import { i18n } from "@/lib/i18n/config"
import {
getShellDictionary,
matchLocale,
pickLocale,
} from "@/packages/mcp-server/shell/dictionary"
import { readShellConfig } from "@/packages/mcp-server/shell/runtime-config"
import {
isDarkPreferred,
THEME_STORAGE_KEY,
} from "@/packages/mcp-server/shell/theme"
const shellDir = join(process.cwd(), "packages/mcp-server/shell")
describe("shell runtime config", () => {
it("takes the server's values", () => {
const config = readShellConfig({
sessionId: "mcp-abc",
token: "0123",
apiBase: "/api",
drawioBaseUrl: "https://embed.diagrams.net/",
drawioUi: "sketch",
drawioLang: "ja",
drawioDark: "dark",
lang: "ja",
})
expect(config.sessionId).toBe("mcp-abc")
expect(config.drawioBaseUrl).toBe("https://embed.diagrams.net/")
expect(config.drawioUi).toBe("sketch")
expect(config.drawioDark).toBe("dark")
})
it("fills in defaults for what is missing or malformed", () => {
expect(readShellConfig(undefined)).toEqual({
sessionId: "",
token: "",
apiBase: "/api",
drawioBaseUrl: "/drawio/index.html",
drawioUi: "",
drawioLang: "",
drawioDark: "auto",
lang: "",
})
const odd = readShellConfig({
sessionId: 42,
drawioDark: "sometimes",
token: "t",
})
expect(odd.sessionId).toBe("")
expect(odd.drawioDark).toBe("auto")
expect(odd.token).toBe("t")
})
})
describe("shell draw.io URL", () => {
it("offers draw.io's theme menu unless a theme is fixed", () => {
// Embedded draw.io shows Extras > Theme only with themes=1 and no ui
const free = new URL(
getDrawioSrc({ editorUrl: "/drawio/index.html" }, false),
)
expect(free.searchParams.get("themes")).toBe("1")
expect(free.searchParams.get("ui")).toBeNull()
const fixed = new URL(
getDrawioSrc(
{ editorUrl: "/drawio/index.html", ui: "kennedy" },
false,
),
)
expect(fixed.searchParams.get("ui")).toBe("kennedy")
expect(fixed.searchParams.get("themes")).toBeNull()
})
})
describe("shell language", () => {
it("maps language tags to the dictionaries we have", () => {
expect(matchLocale("en-US")).toBe("en")
expect(matchLocale("zh")).toBe("zh")
expect(matchLocale("zh-CN")).toBe("zh")
expect(matchLocale("zh-Hans-CN")).toBe("zh")
expect(matchLocale("zh-TW")).toBe("zh-Hant")
expect(matchLocale("zh-Hant")).toBe("zh-Hant")
expect(matchLocale("ZH-HK")).toBe("zh-Hant")
expect(matchLocale("ja-JP")).toBe("ja")
expect(matchLocale("de")).toBeNull()
expect(matchLocale("")).toBeNull()
})
it("prefers the server's language, then the browser's, then English", () => {
expect(pickLocale("zh-tw", ["en-US"])).toBe("zh-Hant")
expect(pickLocale("", ["de-DE", "ja-JP", "en"])).toBe("ja")
expect(pickLocale("de", ["fr"])).toBe("en")
expect(pickLocale("", [])).toBe("en")
})
it("has every dictionary, with the shell's own texts", () => {
for (const locale of i18n.locales) {
const dict = getShellDictionary(locale)
expect(Object.keys(dict.shell).sort(), locale).toEqual([
"aiChanged",
"connected",
"historyChanged",
"offline",
"restoreFailed",
"restoredFromFile",
"toggleTheme",
"tooLarge",
"unreachable",
"waiting",
])
expect(dict.shell.waiting, locale).toBeTruthy()
expect(dict.workspace.canvasLoading, locale).toBeTruthy()
}
})
})
describe("shell theme", () => {
it("follows the stored choice, then the server, then the system", () => {
expect(isDarkPreferred("true", "light", false)).toBe(true)
expect(isDarkPreferred("false", "dark", true)).toBe(false)
expect(isDarkPreferred(null, "dark", false)).toBe(true)
expect(isDarkPreferred(null, "light", true)).toBe(false)
expect(isDarkPreferred(null, "auto", true)).toBe(true)
expect(isDarkPreferred(null, "auto", false)).toBe(false)
expect(isDarkPreferred("maybe", "auto", true)).toBe(true)
})
it("is applied before the first paint by the page's own script", () => {
const html = readFileSync(join(shellDir, "index.html"), "utf8")
expect(THEME_STORAGE_KEY.startsWith("mcp:")).toBe(true)
expect(html).toContain(`localStorage.getItem("${THEME_STORAGE_KEY}")`)
expect(html).toContain("window.__MCP_CONFIG__ = {{CONFIG_JSON}};")
expect(html).toContain('src="/shell/shell.js"')
expect(html).toContain('href="/shell/shell.css"')
})
})