drawio-mcp-server 1.7.0 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +96 -425
  2. package/build/assets/downloader.js +96 -0
  3. package/build/assets/index.js +2 -0
  4. package/build/assets/manager.js +31 -0
  5. package/build/config.js +45 -0
  6. package/build/config.test.js +54 -0
  7. package/build/emitter_bus.js +2 -3
  8. package/build/index.js +305 -337
  9. package/build/plugin/mcp-plugin.js +2618 -0
  10. package/build/prefetch-assets.js +11 -0
  11. package/build/real-environment/add-cell-of-shape.test.js +51 -0
  12. package/build/real-environment/add-edge.test.js +86 -0
  13. package/build/real-environment/assertions.js +18 -0
  14. package/build/real-environment/delete-cell-by-id.test.js +45 -0
  15. package/build/real-environment/edge-editing.test.js +70 -0
  16. package/build/real-environment/edit-cell.test.js +64 -0
  17. package/build/real-environment/harness.js +175 -0
  18. package/build/real-environment/import-export.test.js +82 -0
  19. package/build/real-environment/layers-and-selection.test.js +70 -0
  20. package/build/real-environment/logger.js +25 -0
  21. package/build/real-environment/screenshot.js +46 -0
  22. package/build/real-environment/set-cell-parent.test.js +64 -0
  23. package/build/real-environment/shapes.test.js +153 -0
  24. package/build/real-environment/test-helpers.js +10 -0
  25. package/build/real-environment/tools.js +22 -0
  26. package/build/real-environment/types.js +1 -0
  27. package/build/tool.js +46 -0
  28. package/build/tools/add-cell-of-shape.js +42 -0
  29. package/build/tools/add-edge.js +33 -0
  30. package/build/tools/add-rectangle.js +41 -0
  31. package/build/tools/create-layer.js +8 -0
  32. package/build/tools/delete-cell-by-id.js +10 -0
  33. package/build/tools/edit-cell.js +28 -0
  34. package/build/tools/edit-edge.js +30 -0
  35. package/build/tools/export-diagram.js +96 -0
  36. package/build/tools/get-active-layer.js +5 -0
  37. package/build/tools/get-selected-cell.js +5 -0
  38. package/build/tools/get-shape-by-name.js +10 -0
  39. package/build/tools/get-shape-categories.js +5 -0
  40. package/build/tools/get-shapes-in-category.js +10 -0
  41. package/build/tools/import-diagram.js +22 -0
  42. package/build/tools/index.js +49 -0
  43. package/build/tools/list-layers.js +5 -0
  44. package/build/tools/list-paged-model.js +41 -0
  45. package/build/tools/move-cell-to-layer.js +11 -0
  46. package/build/tools/set-active-layer.js +8 -0
  47. package/build/tools/set-cell-data.js +14 -0
  48. package/build/tools/set-cell-parent.js +9 -0
  49. package/build/tools/set-cell-shape.js +13 -0
  50. package/build/tools/shared.js +7 -0
  51. package/build/tools/types.js +1 -0
  52. package/package.json +29 -22
@@ -0,0 +1,11 @@
1
+ import { ensureAssets } from "./assets/index.js";
2
+ async function main() {
3
+ const { assetRoot } = await ensureAssets({}, (message) => {
4
+ console.log(message);
5
+ });
6
+ console.log(`Assets ready at ${assetRoot}`);
7
+ }
8
+ main().catch((error) => {
9
+ console.error("Failed to prefetch draw.io assets", error);
10
+ process.exit(1);
11
+ });
@@ -0,0 +1,51 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
2
+ import { createRealEnvironmentContext, disposeRealEnvironmentContext, getCells, resetDiagram, } from "./harness.js";
3
+ import { expectNoBrowserErrors, expectNoServerErrors, withVerificationScreenshot, } from "./assertions.js";
4
+ import { callToolJson } from "./tools.js";
5
+ describe("real environment/add-cell-of-shape", () => {
6
+ let context;
7
+ beforeAll(async () => {
8
+ context = await createRealEnvironmentContext();
9
+ }, 180000);
10
+ afterAll(async () => {
11
+ await disposeRealEnvironmentContext(context);
12
+ });
13
+ it("adds a cell via MCP and verifies the live diagram state", async () => {
14
+ await resetDiagram(context);
15
+ context.browserMessages.length = 0;
16
+ const logCountBefore = context.logger.entries.length;
17
+ const beforeCells = await getCells(context.page);
18
+ const { payload } = await callToolJson(context, "add-cell-of-shape", {
19
+ shape_name: "rectangle",
20
+ text: "MCP rectangle",
21
+ x: 180,
22
+ y: 140,
23
+ width: 160,
24
+ height: 90,
25
+ style: "fillColor=#dae8fc;strokeColor=#6c8ebf;",
26
+ });
27
+ expect(payload.success).toBe(true);
28
+ expect(payload.result.id).toBeTruthy();
29
+ await context.page.waitForFunction((id) => {
30
+ const ui = window.ui;
31
+ const graph = ui?.editor?.graph;
32
+ const cell = graph?.getModel?.().getCell?.(id);
33
+ return Boolean(cell);
34
+ }, payload.result.id);
35
+ await withVerificationScreenshot(context, "add-cell-of-shape", "before-live-state-verification", async () => {
36
+ const afterCells = await getCells(context.page);
37
+ expect(afterCells).toHaveLength(beforeCells.length + 1);
38
+ const insertedCell = afterCells.find((cell) => cell.id === payload.result.id);
39
+ expect(insertedCell).toBeDefined();
40
+ expect(insertedCell?.value).toBe("MCP rectangle");
41
+ expect(insertedCell?.x).toBe(180);
42
+ expect(insertedCell?.y).toBe(140);
43
+ expect(insertedCell?.width).toBe(160);
44
+ expect(insertedCell?.height).toBe(90);
45
+ expect(insertedCell?.style).toContain("fillColor=#dae8fc");
46
+ expect(insertedCell?.style).toContain("strokeColor=#6c8ebf");
47
+ });
48
+ await expectNoBrowserErrors(context, "add-cell-of-shape");
49
+ await expectNoServerErrors(context, "add-cell-of-shape", logCountBefore);
50
+ }, 180000);
51
+ });
@@ -0,0 +1,86 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
2
+ import { createRealEnvironmentContext, disposeRealEnvironmentContext, resetDiagram, } from "./harness.js";
3
+ import { expectNoBrowserErrors, expectNoServerErrors, withVerificationScreenshot, } from "./assertions.js";
4
+ import { callToolJson } from "./tools.js";
5
+ describe("real environment/add-edge", () => {
6
+ let context;
7
+ beforeAll(async () => {
8
+ context = await createRealEnvironmentContext();
9
+ }, 180000);
10
+ afterAll(async () => {
11
+ await disposeRealEnvironmentContext(context);
12
+ });
13
+ it("adds an edge through MCP and verifies the live diagram state", async () => {
14
+ await resetDiagram(context);
15
+ context.browserMessages.length = 0;
16
+ const logCountBefore = context.logger.entries.length;
17
+ const { payload: source } = await callToolJson(context, "add-cell-of-shape", {
18
+ shape_name: "rectangle",
19
+ text: "Source",
20
+ x: 100,
21
+ y: 140,
22
+ width: 120,
23
+ height: 70,
24
+ });
25
+ const { payload: target } = await callToolJson(context, "add-cell-of-shape", {
26
+ shape_name: "rectangle",
27
+ text: "Target",
28
+ x: 360,
29
+ y: 140,
30
+ width: 120,
31
+ height: 70,
32
+ });
33
+ expect(source.success).toBe(true);
34
+ expect(target.success).toBe(true);
35
+ const { payload } = await callToolJson(context, "add-edge", {
36
+ source_id: source.result.id,
37
+ target_id: target.result.id,
38
+ text: "connects to",
39
+ style: "endArrow=classic;strokeColor=#b85450;html=1;rounded=0;",
40
+ });
41
+ expect(payload.success).toBe(true);
42
+ expect(payload.result.id).toBeTruthy();
43
+ await context.page.waitForFunction(({ edgeId, sourceId, targetId, }) => {
44
+ const ui = window.ui;
45
+ const graph = ui?.editor?.graph;
46
+ const edge = graph?.getModel?.().getCell?.(edgeId);
47
+ return (edge &&
48
+ edge.edge === true &&
49
+ edge.source?.id === sourceId &&
50
+ edge.target?.id === targetId &&
51
+ edge.value === "connects to" &&
52
+ String(edge.style || "").includes("strokeColor=#b85450"));
53
+ }, {
54
+ edgeId: payload.result.id,
55
+ sourceId: source.result.id,
56
+ targetId: target.result.id,
57
+ });
58
+ await withVerificationScreenshot(context, "add-edge", "before-live-state-verification", async () => {
59
+ const edge = await context.page.evaluate((edgeId) => {
60
+ const ui = window.ui;
61
+ const graph = ui?.editor?.graph;
62
+ const cell = graph?.getModel?.().getCell?.(edgeId);
63
+ if (!cell) {
64
+ return null;
65
+ }
66
+ return {
67
+ id: String(cell.id),
68
+ edge: Boolean(cell.edge),
69
+ value: typeof cell.value === "string" ? cell.value : "",
70
+ style: String(cell.style || ""),
71
+ sourceId: cell.source?.id ? String(cell.source.id) : null,
72
+ targetId: cell.target?.id ? String(cell.target.id) : null,
73
+ };
74
+ }, payload.result.id);
75
+ expect(edge).toBeDefined();
76
+ expect(edge).not.toBeNull();
77
+ expect(edge?.edge).toBe(true);
78
+ expect(edge?.value).toBe("connects to");
79
+ expect(edge?.sourceId).toBe(source.result.id);
80
+ expect(edge?.targetId).toBe(target.result.id);
81
+ expect(edge?.style).toContain("strokeColor=#b85450");
82
+ });
83
+ await expectNoBrowserErrors(context, "add-edge");
84
+ await expectNoServerErrors(context, "add-edge", logCountBefore);
85
+ }, 180000);
86
+ });
@@ -0,0 +1,18 @@
1
+ import { expect } from "@jest/globals";
2
+ import { captureVerificationArtifact } from "./screenshot.js";
3
+ import { browserErrors } from "./harness.js";
4
+ export async function withVerificationScreenshot(context, testName, stepName, verify) {
5
+ await captureVerificationArtifact(context.artifactRunDir, context.page, testName, stepName);
6
+ return await verify();
7
+ }
8
+ export async function expectNoBrowserErrors(context, testName) {
9
+ return withVerificationScreenshot(context, testName, "before-browser-log-verification", () => {
10
+ expect(browserErrors(context)).toEqual([]);
11
+ });
12
+ }
13
+ export async function expectNoServerErrors(context, testName, logCountBefore) {
14
+ return withVerificationScreenshot(context, testName, "before-server-log-verification", () => {
15
+ const serverErrors = context.logger.errors().slice(logCountBefore);
16
+ expect(serverErrors).toEqual([]);
17
+ });
18
+ }
@@ -0,0 +1,45 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
2
+ import { createRealEnvironmentContext, disposeRealEnvironmentContext, getCells, resetDiagram, } from "./harness.js";
3
+ import { expectNoBrowserErrors, expectNoServerErrors, withVerificationScreenshot, } from "./assertions.js";
4
+ import { callToolJson } from "./tools.js";
5
+ describe("real environment/delete-cell-by-id", () => {
6
+ let context;
7
+ beforeAll(async () => {
8
+ context = await createRealEnvironmentContext();
9
+ }, 180000);
10
+ afterAll(async () => {
11
+ await disposeRealEnvironmentContext(context);
12
+ });
13
+ it("deletes a cell through MCP and verifies the live diagram state", async () => {
14
+ await resetDiagram(context);
15
+ context.browserMessages.length = 0;
16
+ const logCountBefore = context.logger.entries.length;
17
+ const { payload: createdPayload } = await callToolJson(context, "add-cell-of-shape", {
18
+ shape_name: "rectangle",
19
+ text: "Delete me",
20
+ x: 260,
21
+ y: 160,
22
+ width: 120,
23
+ height: 70,
24
+ });
25
+ expect(createdPayload.success).toBe(true);
26
+ const beforeDelete = await getCells(context.page);
27
+ expect(beforeDelete.some((cell) => cell.id === createdPayload.result.id)).toBe(true);
28
+ const { payload } = await callToolJson(context, "delete-cell-by-id", {
29
+ cell_id: createdPayload.result.id,
30
+ });
31
+ expect(payload.success).toBe(true);
32
+ await context.page.waitForFunction((id) => {
33
+ const ui = window.ui;
34
+ const graph = ui?.editor?.graph;
35
+ return !graph?.getModel?.().getCell?.(id);
36
+ }, createdPayload.result.id);
37
+ await withVerificationScreenshot(context, "delete-cell-by-id", "before-live-state-verification", async () => {
38
+ const afterDelete = await getCells(context.page);
39
+ expect(afterDelete.some((cell) => cell.id === createdPayload.result.id)).toBe(false);
40
+ expect(afterDelete).toHaveLength(beforeDelete.length - 1);
41
+ });
42
+ await expectNoBrowserErrors(context, "delete-cell-by-id");
43
+ await expectNoServerErrors(context, "delete-cell-by-id", logCountBefore);
44
+ }, 180000);
45
+ });
@@ -0,0 +1,70 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
2
+ import { createRealEnvironmentContext, disposeRealEnvironmentContext, getCellById, resetDiagram, } from "./harness.js";
3
+ import { expectNoBrowserErrors, expectNoServerErrors, withVerificationScreenshot, } from "./assertions.js";
4
+ import { callToolJson } from "./tools.js";
5
+ import { expectToolSuccess } from "./test-helpers.js";
6
+ describe("real environment/edge editing", () => {
7
+ let context;
8
+ beforeAll(async () => {
9
+ context = await createRealEnvironmentContext();
10
+ }, 180000);
11
+ afterAll(async () => {
12
+ await disposeRealEnvironmentContext(context);
13
+ });
14
+ it("covers edge editing with target change, style change, and waypoints", async () => {
15
+ await resetDiagram(context);
16
+ context.browserMessages.length = 0;
17
+ const logCountBefore = context.logger.entries.length;
18
+ const { payload: source } = await callToolJson(context, "add-rectangle", {
19
+ x: 120,
20
+ y: 110,
21
+ width: 150,
22
+ height: 90,
23
+ text: "Source",
24
+ });
25
+ const { payload: target } = await callToolJson(context, "add-rectangle", {
26
+ x: 360,
27
+ y: 110,
28
+ width: 150,
29
+ height: 90,
30
+ text: "Target",
31
+ });
32
+ const { payload: alternateTarget } = await callToolJson(context, "add-rectangle", {
33
+ x: 620,
34
+ y: 110,
35
+ width: 150,
36
+ height: 90,
37
+ text: "Alternate target",
38
+ });
39
+ expectToolSuccess(source);
40
+ expectToolSuccess(target);
41
+ expectToolSuccess(alternateTarget);
42
+ const { payload: initialEdge } = await callToolJson(context, "add-edge", {
43
+ source_id: source.result.id,
44
+ target_id: target.result.id,
45
+ text: "initial edge",
46
+ });
47
+ expectToolSuccess(initialEdge);
48
+ await callToolJson(context, "edit-edge", {
49
+ cell_id: initialEdge.result.id,
50
+ text: "edited edge",
51
+ target_id: alternateTarget.result.id,
52
+ style: "endArrow=classic;strokeColor=#9673a6;html=1;rounded=0;",
53
+ points: [{ x: 500, y: 70 }],
54
+ });
55
+ await withVerificationScreenshot(context, "edge-editing", "before-live-state-verification", async () => {
56
+ const edge = await getCellById(context.page, initialEdge.result.id);
57
+ expect(edge).not.toBeNull();
58
+ expect(edge?.edge).toBe(true);
59
+ expect(edge?.value).toBe("edited edge");
60
+ expect(edge?.sourceId).toBe(source.result.id);
61
+ expect(edge?.targetId).toBe(alternateTarget.result.id);
62
+ expect(edge?.style).toContain("strokeColor=#9673a6");
63
+ expect(edge?.points).toHaveLength(1);
64
+ expect(edge?.points[0]?.x).toBe(500);
65
+ expect(edge?.points[0]?.y).toBe(70);
66
+ });
67
+ await expectNoBrowserErrors(context, "edge-editing");
68
+ await expectNoServerErrors(context, "edge-editing", logCountBefore);
69
+ }, 180000);
70
+ });
@@ -0,0 +1,64 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
2
+ import { createRealEnvironmentContext, disposeRealEnvironmentContext, getCells, resetDiagram, } from "./harness.js";
3
+ import { expectNoBrowserErrors, expectNoServerErrors, withVerificationScreenshot, } from "./assertions.js";
4
+ import { callToolJson } from "./tools.js";
5
+ describe("real environment/edit-cell", () => {
6
+ let context;
7
+ beforeAll(async () => {
8
+ context = await createRealEnvironmentContext();
9
+ }, 180000);
10
+ afterAll(async () => {
11
+ await disposeRealEnvironmentContext(context);
12
+ });
13
+ it("edits an existing cell through MCP and verifies the live diagram state", async () => {
14
+ await resetDiagram(context);
15
+ context.browserMessages.length = 0;
16
+ const logCountBefore = context.logger.entries.length;
17
+ const { payload: created } = await callToolJson(context, "add-cell-of-shape", {
18
+ shape_name: "rectangle",
19
+ text: "Original",
20
+ x: 100,
21
+ y: 120,
22
+ width: 120,
23
+ height: 70,
24
+ style: "fillColor=#ffffff;strokeColor=#000000;",
25
+ });
26
+ expect(created.success).toBe(true);
27
+ const { payload } = await callToolJson(context, "edit-cell", {
28
+ cell_id: created.result.id,
29
+ text: "Edited via MCP",
30
+ x: 260,
31
+ y: 210,
32
+ width: 180,
33
+ height: 95,
34
+ style: "fillColor=#d5e8d4;strokeColor=#82b366;",
35
+ });
36
+ expect(payload.success).toBe(true);
37
+ await context.page.waitForFunction((id) => {
38
+ const ui = window.ui;
39
+ const graph = ui?.editor?.graph;
40
+ const cell = graph?.getModel?.().getCell?.(id);
41
+ return (cell &&
42
+ cell.value === "Edited via MCP" &&
43
+ cell.geometry?.x === 260 &&
44
+ cell.geometry?.y === 210 &&
45
+ cell.geometry?.width === 180 &&
46
+ cell.geometry?.height === 95 &&
47
+ String(cell.style || "").includes("fillColor=#d5e8d4"));
48
+ }, created.result.id);
49
+ await withVerificationScreenshot(context, "edit-cell", "before-live-state-verification", async () => {
50
+ const cells = await getCells(context.page);
51
+ const editedCell = cells.find((cell) => cell.id === created.result.id);
52
+ expect(editedCell).toBeDefined();
53
+ expect(editedCell?.value).toBe("Edited via MCP");
54
+ expect(editedCell?.x).toBe(260);
55
+ expect(editedCell?.y).toBe(210);
56
+ expect(editedCell?.width).toBe(180);
57
+ expect(editedCell?.height).toBe(95);
58
+ expect(editedCell?.style).toContain("fillColor=#d5e8d4");
59
+ expect(editedCell?.style).toContain("strokeColor=#82b366");
60
+ });
61
+ await expectNoBrowserErrors(context, "edit-cell");
62
+ await expectNoServerErrors(context, "edit-cell", logCountBefore);
63
+ }, 180000);
64
+ });
@@ -0,0 +1,175 @@
1
+ import { chromium, } from "@playwright/test";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
4
+ import { ensureAssets } from "../assets/index.js";
5
+ import { getHttpFeatureConfig } from "../config.js";
6
+ import { createDrawioMcpApp } from "../index.js";
7
+ import { MemoryLogger } from "./logger.js";
8
+ import { createArtifactRunDir } from "./screenshot.js";
9
+ export async function createRealEnvironmentContext() {
10
+ const logger = new MemoryLogger();
11
+ const browserMessages = [];
12
+ const artifactRunDir = await createArtifactRunDir();
13
+ await ensureAssets({}, () => undefined);
14
+ const app = createDrawioMcpApp({ log: logger });
15
+ const wsServer = await app.startWebSocketServer(0);
16
+ const wsPort = Number(wsServer.address().port);
17
+ const config = {
18
+ extensionPort: wsPort,
19
+ httpPort: 0,
20
+ transports: ["http"],
21
+ editorEnabled: true,
22
+ };
23
+ const startedHttp = await app.startHttpServer(0, config, getHttpFeatureConfig(config));
24
+ const httpPort = startedHttp.port;
25
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
26
+ const client = new Client({
27
+ name: "real-environment-test",
28
+ version: "1.0.0",
29
+ });
30
+ await Promise.all([
31
+ app.server.connect(serverTransport),
32
+ client.connect(clientTransport),
33
+ ]);
34
+ const browser = await chromium.launch({ headless: true });
35
+ const page = await browser.newPage();
36
+ page.on("console", (message) => {
37
+ browserMessages.push({
38
+ type: message.type(),
39
+ text: message.text(),
40
+ });
41
+ });
42
+ await page.addInitScript((port) => {
43
+ const store = {
44
+ websocketPort: port,
45
+ };
46
+ window.__DRAWIO_MCP_TEST_HOOKS__ = true;
47
+ window.localStorage.setItem("drawio-mcp-plugin-config", JSON.stringify(store));
48
+ window.localStorage.setItem("drawio-mcp-config", JSON.stringify(store));
49
+ }, wsPort);
50
+ await page.goto(`http://localhost:${httpPort}/`, {
51
+ waitUntil: "domcontentloaded",
52
+ });
53
+ await waitForPluginReady(page);
54
+ return {
55
+ browser,
56
+ page,
57
+ client,
58
+ app,
59
+ logger,
60
+ browserMessages,
61
+ artifactRunDir,
62
+ httpPort,
63
+ wsPort,
64
+ };
65
+ }
66
+ export async function disposeRealEnvironmentContext(context) {
67
+ await context.client.close();
68
+ await context.browser.close();
69
+ await context.app.close();
70
+ }
71
+ export async function waitForPluginReady(page) {
72
+ await page.waitForFunction(() => {
73
+ const maybeWindow = window;
74
+ return Boolean(maybeWindow.ui?.editor?.graph);
75
+ });
76
+ await page.waitForFunction(() => {
77
+ const maybeWindow = window;
78
+ const graph = maybeWindow.ui?.editor?.graph;
79
+ return Boolean(graph?.getModel?.()?.cells);
80
+ });
81
+ await page.waitForFunction(() => document.querySelector("svg") !== null);
82
+ await page.waitForTimeout(1500);
83
+ }
84
+ export async function getCells(page) {
85
+ return page.evaluate(() => {
86
+ const maybeWindow = window;
87
+ const graph = maybeWindow.ui?.editor?.graph;
88
+ const model = graph?.getModel?.();
89
+ const cells = Object.values(model?.cells ?? {});
90
+ return cells
91
+ .filter((cell) => cell?.vertex && cell?.geometry)
92
+ .map((cell) => ({
93
+ id: String(cell.id),
94
+ value: typeof cell.value === "string" ? cell.value : "",
95
+ style: String(cell.style ?? ""),
96
+ x: typeof cell.geometry?.x === "number" ? cell.geometry.x : null,
97
+ y: typeof cell.geometry?.y === "number" ? cell.geometry.y : null,
98
+ width: typeof cell.geometry?.width === "number" ? cell.geometry.width : null,
99
+ height: typeof cell.geometry?.height === "number"
100
+ ? cell.geometry.height
101
+ : null,
102
+ parentId: cell.parent?.id ? String(cell.parent.id) : null,
103
+ }))
104
+ .sort((a, b) => a.id.localeCompare(b.id));
105
+ });
106
+ }
107
+ export async function getCellById(page, cellId) {
108
+ return page.evaluate((id) => {
109
+ const maybeWindow = window;
110
+ const graph = maybeWindow.ui?.editor?.graph;
111
+ const cell = graph?.getModel?.().getCell?.(id);
112
+ if (!cell) {
113
+ return null;
114
+ }
115
+ return {
116
+ id: String(cell.id),
117
+ value: typeof cell.value === "string" ? cell.value : "",
118
+ style: String(cell.style ?? ""),
119
+ x: typeof cell.geometry?.x === "number" ? cell.geometry.x : null,
120
+ y: typeof cell.geometry?.y === "number" ? cell.geometry.y : null,
121
+ width: typeof cell.geometry?.width === "number" ? cell.geometry.width : null,
122
+ height: typeof cell.geometry?.height === "number" ? cell.geometry.height : null,
123
+ parentId: cell.parent?.id ? String(cell.parent.id) : null,
124
+ edge: Boolean(cell.edge),
125
+ vertex: Boolean(cell.vertex),
126
+ sourceId: cell.source?.id ? String(cell.source.id) : null,
127
+ targetId: cell.target?.id ? String(cell.target.id) : null,
128
+ points: Array.isArray(cell.geometry?.points)
129
+ ? cell.geometry.points.map((point) => ({
130
+ x: typeof point.x === "number" ? point.x : null,
131
+ y: typeof point.y === "number" ? point.y : null,
132
+ }))
133
+ : [],
134
+ attributes: typeof cell.value === "object" && cell.value?.attributes
135
+ ? Array.from(cell.value.attributes).reduce((acc, attr) => {
136
+ acc[String(attr.name)] = String(attr.value);
137
+ return acc;
138
+ }, {})
139
+ : {},
140
+ };
141
+ }, cellId);
142
+ }
143
+ export async function selectCell(page, cellId) {
144
+ await page.evaluate((id) => {
145
+ const maybeWindow = window;
146
+ const graph = maybeWindow.ui?.editor?.graph;
147
+ const cell = graph?.getModel?.().getCell?.(id);
148
+ if (!cell) {
149
+ throw new Error(`Cell ${id} not found for selection`);
150
+ }
151
+ graph.setSelectionCell(cell);
152
+ }, cellId);
153
+ }
154
+ export function browserErrors(context) {
155
+ return context.browserMessages.filter((entry) => entry.type === "error");
156
+ }
157
+ export async function resetDiagram(context) {
158
+ const emptyDiagram = '<mxGraphModel dx="0" dy="0" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0"><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel>';
159
+ await context.client.callTool({
160
+ name: "import-diagram",
161
+ arguments: {
162
+ data: emptyDiagram,
163
+ format: "xml",
164
+ mode: "replace",
165
+ filename: "blank.drawio",
166
+ },
167
+ });
168
+ await context.page.waitForFunction(() => {
169
+ const maybeWindow = window;
170
+ const graph = maybeWindow.ui?.editor?.graph;
171
+ const model = graph?.getModel?.();
172
+ const cells = Object.values(model?.cells ?? {});
173
+ return cells.filter((cell) => cell?.vertex).length === 0;
174
+ });
175
+ }
@@ -0,0 +1,82 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
2
+ import { existsSync, mkdtempSync, readFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { createRealEnvironmentContext, disposeRealEnvironmentContext, getCellById, resetDiagram, } from "./harness.js";
6
+ import { expectNoBrowserErrors, expectNoServerErrors, withVerificationScreenshot, } from "./assertions.js";
7
+ import { callToolJson, callToolRaw } from "./tools.js";
8
+ import { expectToolSuccess } from "./test-helpers.js";
9
+ describe("real environment/import export", () => {
10
+ let context;
11
+ beforeAll(async () => {
12
+ context = await createRealEnvironmentContext();
13
+ }, 180000);
14
+ afterAll(async () => {
15
+ await disposeRealEnvironmentContext(context);
16
+ });
17
+ it("covers xml export to file and xml import into the live diagram", async () => {
18
+ await resetDiagram(context);
19
+ context.browserMessages.length = 0;
20
+ const logCountBefore = context.logger.entries.length;
21
+ const { payload: rectangle } = await callToolJson(context, "add-rectangle", {
22
+ x: 120,
23
+ y: 110,
24
+ width: 150,
25
+ height: 90,
26
+ text: "Export me",
27
+ });
28
+ expectToolSuccess(rectangle);
29
+ const xmlExportDir = mkdtempSync(join(tmpdir(), "drawio-real-export-"));
30
+ const xmlExportPath = join(xmlExportDir, "diagram.xml");
31
+ const xmlExport = await callToolRaw(context, "export-diagram", {
32
+ format: "xml",
33
+ output_path: xmlExportPath,
34
+ });
35
+ expect(existsSync(xmlExportPath)).toBe(true);
36
+ expect(readFileSync(xmlExportPath, "utf-8")).toContain("mxGraphModel");
37
+ const xmlTextContent = xmlExport.content.find((item) => item.type === "text" && item.text?.includes("mxGraphModel"));
38
+ expect(xmlTextContent).toBeDefined();
39
+ const importXml = '<mxGraphModel dx="0" dy="0" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0"><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="imported-1" value="Imported cell" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1"><mxGeometry x="80" y="220" width="160" height="80" as="geometry"/></mxCell></root></mxGraphModel>';
40
+ const { payload: importResult } = await callToolJson(context, "import-diagram", {
41
+ data: importXml,
42
+ format: "xml",
43
+ mode: "replace",
44
+ filename: "imported.drawio",
45
+ });
46
+ expect(importResult?.success).toBe(true);
47
+ await context.page.waitForFunction(() => {
48
+ const maybeWindow = window;
49
+ const graph = maybeWindow.ui?.editor?.graph;
50
+ const model = graph?.getModel?.();
51
+ const cells = Object.values(model?.cells ?? {});
52
+ return cells.some((cell) => cell?.value === "Imported cell");
53
+ });
54
+ await withVerificationScreenshot(context, "import-export", "before-live-state-verification", async () => {
55
+ const rectangleCell = await getCellById(context.page, rectangle.result.id);
56
+ expect(rectangleCell).toBeNull();
57
+ const importedCell = await context.page.evaluate(() => {
58
+ const maybeWindow = window;
59
+ const graph = maybeWindow.ui?.editor?.graph;
60
+ const model = graph?.getModel?.();
61
+ const cells = Object.values(model?.cells ?? {});
62
+ const cell = cells.find((candidate) => candidate?.value === "Imported cell");
63
+ if (!cell) {
64
+ return null;
65
+ }
66
+ return {
67
+ value: cell.value,
68
+ style: String(cell.style ?? ""),
69
+ width: typeof cell.geometry?.width === "number"
70
+ ? cell.geometry.width
71
+ : null,
72
+ };
73
+ });
74
+ expect(importedCell).not.toBeNull();
75
+ expect(importedCell?.value).toBe("Imported cell");
76
+ expect(importedCell?.style).toContain("fillColor=#ffe6cc");
77
+ expect(importedCell?.width).toBe(160);
78
+ });
79
+ await expectNoBrowserErrors(context, "import-export");
80
+ await expectNoServerErrors(context, "import-export", logCountBefore);
81
+ }, 180000);
82
+ });