drawio-mcp-server 2.0.3 → 2.1.0

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 (71) hide show
  1. package/README.md +29 -3
  2. package/build/assets/downloader.js +16 -17
  3. package/build/config.js +253 -5
  4. package/build/config.test.js +449 -1
  5. package/build/emitter_bus.js +3 -0
  6. package/build/emitter_bus.test.js +7 -0
  7. package/build/index.capabilities.test.js +45 -0
  8. package/build/index.js +496 -106
  9. package/build/install-desktop-plugin.js +56 -0
  10. package/build/install-desktop-plugin.test.js +66 -0
  11. package/build/multi-transport.test.js +277 -0
  12. package/build/plugin/mcp-plugin.js +2180 -1105
  13. package/build/prefetch-assets.js +6 -5
  14. package/build/real-environment/document-targeting.test.js +124 -0
  15. package/build/real-environment/export-diagram.test.js +473 -0
  16. package/build/real-environment/harness.js +120 -32
  17. package/build/real-environment/import-mermaid.test.js +81 -0
  18. package/build/real-environment/pages-and-concurrency.test.js +448 -0
  19. package/build/real-environment/shapes.test.js +58 -0
  20. package/build/real-environment/tools.js +51 -6
  21. package/build/register-tool.js +31 -0
  22. package/build/request_queue.js +29 -0
  23. package/build/request_queue.test.js +98 -0
  24. package/build/stdio-transport-purity.test.js +88 -0
  25. package/build/strip-schema.js +61 -0
  26. package/build/tls/expiry.js +14 -0
  27. package/build/tls/expiry.test.js +54 -0
  28. package/build/tls/generate.js +123 -0
  29. package/build/tls/generate.test.js +115 -0
  30. package/build/tls/index.js +80 -0
  31. package/build/tls/index.test.js +141 -0
  32. package/build/tls/install-hint.js +45 -0
  33. package/build/tls/install-hint.test.js +32 -0
  34. package/build/tls/load.js +18 -0
  35. package/build/tls/load.test.js +40 -0
  36. package/build/tls/paths.js +30 -0
  37. package/build/tls/paths.test.js +53 -0
  38. package/build/tls/san.js +27 -0
  39. package/build/tls/san.test.js +72 -0
  40. package/build/tool-registry.test.js +823 -0
  41. package/build/tool.js +74 -15
  42. package/build/tool.test.js +250 -7
  43. package/build/tools/add-cell-of-shape.js +4 -2
  44. package/build/tools/add-edge.js +3 -1
  45. package/build/tools/add-rectangle.js +4 -2
  46. package/build/tools/copy-page.js +13 -0
  47. package/build/tools/create-layer.js +4 -2
  48. package/build/tools/create-page.js +8 -0
  49. package/build/tools/delete-cell-by-id.js +4 -2
  50. package/build/tools/edit-cell.js +3 -1
  51. package/build/tools/edit-edge.js +3 -1
  52. package/build/tools/export-diagram.js +7 -3
  53. package/build/tools/get-active-layer.js +4 -1
  54. package/build/tools/get-current-page.js +5 -0
  55. package/build/tools/get-selected-cell.js +4 -1
  56. package/build/tools/import-diagram.js +12 -2
  57. package/build/tools/import-mermaid.js +31 -0
  58. package/build/tools/index.js +14 -0
  59. package/build/tools/list-documents.js +17 -0
  60. package/build/tools/list-layers.js +4 -1
  61. package/build/tools/list-paged-model.js +4 -3
  62. package/build/tools/list-pages.js +5 -0
  63. package/build/tools/move-cell-to-layer.js +3 -1
  64. package/build/tools/rename-page.js +10 -0
  65. package/build/tools/set-active-layer.js +4 -2
  66. package/build/tools/set-cell-data.js +3 -1
  67. package/build/tools/set-cell-parent.js +3 -1
  68. package/build/tools/set-cell-shape.js +3 -1
  69. package/build/tools/shared.js +35 -0
  70. package/build/tools/shared.test.js +29 -0
  71. package/package.json +22 -14
@@ -1,11 +1,12 @@
1
1
  import { ensureAssets } from "./assets/index.js";
2
+ import { create_logger } from "./mcp_console_logger.js";
2
3
  async function main() {
3
- const { assetRoot } = await ensureAssets({}, (message) => {
4
- console.log(message);
5
- });
6
- console.log(`Assets ready at ${assetRoot}`);
4
+ const log = create_logger();
5
+ const { assetRoot } = await ensureAssets({}, log);
6
+ log.log("info", `Assets ready at ${assetRoot}`);
7
7
  }
8
8
  main().catch((error) => {
9
- console.error("Failed to prefetch draw.io assets", error);
9
+ const log = create_logger();
10
+ log.log("error", "Failed to prefetch draw.io assets", error);
10
11
  process.exit(1);
11
12
  });
@@ -0,0 +1,124 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
2
+ import { createRealEnvironmentContext, disposeRealEnvironmentContext, openConnectedDrawioPage, resetDiagram, } from "./harness.js";
3
+ import { expectNoBrowserErrors, expectNoServerErrors } from "./assertions.js";
4
+ import { callToolJson } from "./tools.js";
5
+ import { expectToolSuccess, unwrapToolPayload } from "./test-helpers.js";
6
+ function firstText(result) {
7
+ return result.content.find((item) => item.type === "text")?.text;
8
+ }
9
+ describe("real environment/document targeting", () => {
10
+ let context;
11
+ beforeAll(async () => {
12
+ context = await createRealEnvironmentContext();
13
+ }, 180000);
14
+ afterAll(async () => {
15
+ await disposeRealEnvironmentContext(context);
16
+ });
17
+ it("lists connected documents and requires target_document when multiple tabs are connected", async () => {
18
+ await resetDiagram(context);
19
+ context.browserMessages.length = 0;
20
+ const logCountBefore = context.logger.entries.length;
21
+ const { payload: renamedDocOnePayload } = await callToolJson(context, "rename-page", {
22
+ page: { index: 0 },
23
+ name: "Doc One Page A",
24
+ });
25
+ expectToolSuccess(renamedDocOnePayload);
26
+ const docOneCurrentPage = unwrapToolPayload(renamedDocOnePayload);
27
+ expect(docOneCurrentPage.name).toBe("Doc One Page A");
28
+ const { payload: createdDocOnePagePayload } = await callToolJson(context, "create-page", {
29
+ name: "Doc One Page B",
30
+ });
31
+ expectToolSuccess(createdDocOnePagePayload);
32
+ const docOneSecondPage = unwrapToolPayload(createdDocOnePagePayload);
33
+ expect(docOneSecondPage.name).toBe("Doc One Page B");
34
+ const { payload: singleDocumentPayload } = await callToolJson(context, "list-documents", {});
35
+ expectToolSuccess(singleDocumentPayload);
36
+ const [docOne] = unwrapToolPayload(singleDocumentPayload);
37
+ expect(docOne).toMatchObject({
38
+ page_count: 2,
39
+ current_page: {
40
+ id: docOneCurrentPage.id,
41
+ name: "Doc One Page A",
42
+ is_current: true,
43
+ },
44
+ });
45
+ const secondPage = await openConnectedDrawioPage({
46
+ browser: context.browser,
47
+ httpPort: context.httpPort,
48
+ wsPort: context.wsPort,
49
+ browserMessages: context.browserMessages,
50
+ });
51
+ try {
52
+ const { payload: twoDocumentsPayload } = await callToolJson(context, "list-documents", {});
53
+ expectToolSuccess(twoDocumentsPayload);
54
+ const documents = unwrapToolPayload(twoDocumentsPayload);
55
+ expect(documents).toHaveLength(2);
56
+ const refreshedDocOne = documents.find((document) => document.id === docOne.id);
57
+ const docTwo = documents.find((document) => document.id !== docOne.id);
58
+ expect(refreshedDocOne?.page_count).toBe(2);
59
+ expect(docTwo).toBeDefined();
60
+ const { payload: renamedDocTwoPayload } = await callToolJson(context, "rename-page", {
61
+ target_document: { id: docTwo.id },
62
+ page: { index: 0 },
63
+ name: "Doc Two Page Only",
64
+ });
65
+ expectToolSuccess(renamedDocTwoPayload);
66
+ const docTwoCurrentPage = unwrapToolPayload(renamedDocTwoPayload);
67
+ expect(docTwoCurrentPage.name).toBe("Doc Two Page Only");
68
+ const ambiguousListPages = (await context.client.callTool({
69
+ name: "list-pages",
70
+ arguments: {},
71
+ }));
72
+ expect(ambiguousListPages.isError).toBe(true);
73
+ expect(firstText(ambiguousListPages)).toContain("Multiple Draw.io documents are connected");
74
+ const { payload: docOnePagesPayload } = await callToolJson(context, "list-pages", {
75
+ target_document: { id: docOne.id },
76
+ });
77
+ expectToolSuccess(docOnePagesPayload);
78
+ const docOnePages = unwrapToolPayload(docOnePagesPayload);
79
+ expect(docOnePages.map((page) => page.name)).toEqual([
80
+ "Doc One Page A",
81
+ "Doc One Page B",
82
+ ]);
83
+ const { payload: docTwoPagesPayload } = await callToolJson(context, "list-pages", {
84
+ target_document: { id: docTwo.id },
85
+ });
86
+ expectToolSuccess(docTwoPagesPayload);
87
+ const docTwoPages = unwrapToolPayload(docTwoPagesPayload);
88
+ expect(docTwoPages).toEqual([
89
+ {
90
+ index: 0,
91
+ id: docTwoCurrentPage.id,
92
+ name: "Doc Two Page Only",
93
+ is_current: true,
94
+ },
95
+ ]);
96
+ const { payload: finalDocumentsPayload } = await callToolJson(context, "list-documents", {});
97
+ expectToolSuccess(finalDocumentsPayload);
98
+ const finalDocuments = unwrapToolPayload(finalDocumentsPayload);
99
+ const finalDocOne = finalDocuments.find((document) => document.id === docOne.id);
100
+ const finalDocTwo = finalDocuments.find((document) => document.id === docTwo.id);
101
+ expect(finalDocOne).toMatchObject({
102
+ page_count: 2,
103
+ current_page: {
104
+ id: docOneCurrentPage.id,
105
+ name: "Doc One Page A",
106
+ is_current: true,
107
+ },
108
+ });
109
+ expect(finalDocTwo).toMatchObject({
110
+ page_count: 1,
111
+ current_page: {
112
+ id: docTwoCurrentPage.id,
113
+ name: "Doc Two Page Only",
114
+ is_current: true,
115
+ },
116
+ });
117
+ }
118
+ finally {
119
+ await secondPage.close();
120
+ }
121
+ await expectNoBrowserErrors(context, "document-targeting");
122
+ await expectNoServerErrors(context, "document-targeting", logCountBefore);
123
+ }, 180000);
124
+ });
@@ -0,0 +1,473 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { createRealEnvironmentContext, disposeRealEnvironmentContext, resetDiagram, selectCell, } 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
+ function findText(content, substring) {
10
+ return content.find((item) => item.type === "text" && item.text?.includes(substring));
11
+ }
12
+ function parseDimensions(metaText) {
13
+ const match = metaText.match(/(\d+)x(\d+)/);
14
+ return {
15
+ width: match ? parseInt(match[1], 10) : 0,
16
+ height: match ? parseInt(match[2], 10) : 0,
17
+ };
18
+ }
19
+ async function saveArtifact(artifactRunDir, name, data) {
20
+ const filePath = join(artifactRunDir, name);
21
+ await writeFile(filePath, data);
22
+ return filePath;
23
+ }
24
+ describe("real environment/export-diagram", () => {
25
+ let context;
26
+ beforeAll(async () => {
27
+ context = await createRealEnvironmentContext();
28
+ }, 180000);
29
+ afterAll(async () => {
30
+ await disposeRealEnvironmentContext(context);
31
+ });
32
+ it("exports as XML with cell content, geometry, and style preserved", async () => {
33
+ await resetDiagram(context);
34
+ context.browserMessages.length = 0;
35
+ const logCountBefore = context.logger.entries.length;
36
+ const { payload: rectangle } = await callToolJson(context, "add-rectangle", {
37
+ x: 100,
38
+ y: 100,
39
+ width: 200,
40
+ height: 100,
41
+ text: "XML Export Cell",
42
+ style: "fillColor=#dae8fc;strokeColor=#6c8ebf;whiteSpace=wrap;html=1;",
43
+ });
44
+ expectToolSuccess(rectangle);
45
+ const result = await callToolRaw(context, "export-diagram", {
46
+ format: "xml",
47
+ });
48
+ const content = result.content;
49
+ await withVerificationScreenshot(context, "export-xml", "before-live-state-verification", async () => {
50
+ const xmlContent = findText(content, "mxGraphModel");
51
+ expect(xmlContent).toBeDefined();
52
+ const xml = xmlContent.text;
53
+ expect(xml).toContain("XML Export Cell");
54
+ expect(xml).toContain(rectangle.result.id);
55
+ expect(xml).toContain("fillColor=#dae8fc");
56
+ expect(xml).toContain("strokeColor=#6c8ebf");
57
+ expect(xml).toContain('width="200"');
58
+ expect(xml).toContain('height="100"');
59
+ expect(xml).toContain('x="100"');
60
+ expect(xml).toContain('y="100"');
61
+ expect(xml).toContain("mxGeometry");
62
+ expect(xml).toContain('vertex="1"');
63
+ const metaContent = findText(content, "Exported xml");
64
+ expect(metaContent).toBeDefined();
65
+ expect(metaContent.text).toContain("application/xml");
66
+ await saveArtifact(context.artifactRunDir, "export-xml.xml", xml);
67
+ });
68
+ await expectNoBrowserErrors(context, "export-xml");
69
+ await expectNoServerErrors(context, "export-xml", logCountBefore);
70
+ }, 180000);
71
+ it("exports as SVG with renderable structure and positive dimensions", async () => {
72
+ await resetDiagram(context);
73
+ context.browserMessages.length = 0;
74
+ const logCountBefore = context.logger.entries.length;
75
+ const { payload: rectangle } = await callToolJson(context, "add-rectangle", {
76
+ x: 50,
77
+ y: 50,
78
+ width: 160,
79
+ height: 80,
80
+ text: "SVG Export Cell",
81
+ });
82
+ expectToolSuccess(rectangle);
83
+ const result = await callToolRaw(context, "export-diagram", {
84
+ format: "svg",
85
+ scale: 1,
86
+ border: 10,
87
+ background: "#f0f0f0",
88
+ });
89
+ const content = result.content;
90
+ await withVerificationScreenshot(context, "export-svg", "before-live-state-verification", async () => {
91
+ const svgContent = findText(content, "<svg");
92
+ expect(svgContent).toBeDefined();
93
+ const svg = svgContent.text;
94
+ expect(svg).toContain("SVG Export Cell");
95
+ expect(svg).toContain("xmlns=");
96
+ expect(svg).toMatch(/width="\d+/);
97
+ expect(svg).toMatch(/height="\d+/);
98
+ expect(svg).toContain("</svg>");
99
+ const widthMatch = svg.match(/width="(\d+)/);
100
+ const heightMatch = svg.match(/height="(\d+)/);
101
+ expect(parseInt(widthMatch[1], 10)).toBeGreaterThan(0);
102
+ expect(parseInt(heightMatch[1], 10)).toBeGreaterThan(0);
103
+ const metaContent = findText(content, "Exported svg");
104
+ expect(metaContent).toBeDefined();
105
+ expect(metaContent.text).toContain("image/svg+xml");
106
+ const dims = parseDimensions(metaContent.text);
107
+ expect(dims.width).toBeGreaterThan(0);
108
+ expect(dims.height).toBeGreaterThan(0);
109
+ await saveArtifact(context.artifactRunDir, "export-svg.svg", svg);
110
+ });
111
+ await expectNoBrowserErrors(context, "export-svg");
112
+ await expectNoServerErrors(context, "export-svg", logCountBefore);
113
+ }, 180000);
114
+ it("exports as PNG with non-trivial image data and positive dimensions", async () => {
115
+ await resetDiagram(context);
116
+ context.browserMessages.length = 0;
117
+ const logCountBefore = context.logger.entries.length;
118
+ const { payload: rectangle } = await callToolJson(context, "add-rectangle", {
119
+ x: 80,
120
+ y: 80,
121
+ width: 140,
122
+ height: 70,
123
+ text: "PNG Export Cell",
124
+ });
125
+ expectToolSuccess(rectangle);
126
+ const result = await callToolRaw(context, "export-diagram", {
127
+ format: "png",
128
+ scale: 2,
129
+ dpi: 150,
130
+ });
131
+ const content = result.content;
132
+ await withVerificationScreenshot(context, "export-png", "before-live-state-verification", async () => {
133
+ const imageContent = content.find((item) => item.type === "image");
134
+ expect(imageContent).toBeDefined();
135
+ expect(imageContent.mimeType).toBe("image/png");
136
+ expect(imageContent.data).toBeTruthy();
137
+ const pngBuffer = Buffer.from(imageContent.data, "base64");
138
+ expect(pngBuffer[0]).toBe(0x89);
139
+ expect(pngBuffer[1]).toBe(0x50);
140
+ expect(pngBuffer[2]).toBe(0x4e);
141
+ expect(pngBuffer[3]).toBe(0x47);
142
+ expect(pngBuffer.length).toBeGreaterThan(500);
143
+ const metaContent = content.find((item) => item.type === "text" &&
144
+ item.text?.includes("Exported png"));
145
+ expect(metaContent).toBeDefined();
146
+ expect(metaContent.text).toContain("image/png");
147
+ const dims = parseDimensions(metaContent.text);
148
+ expect(dims.width).toBeGreaterThan(0);
149
+ expect(dims.height).toBeGreaterThan(0);
150
+ await saveArtifact(context.artifactRunDir, "export-png.png", pngBuffer);
151
+ });
152
+ await expectNoBrowserErrors(context, "export-png");
153
+ await expectNoServerErrors(context, "export-png", logCountBefore);
154
+ }, 180000);
155
+ it("exports XML to a file and file matches returned content", async () => {
156
+ await resetDiagram(context);
157
+ context.browserMessages.length = 0;
158
+ const logCountBefore = context.logger.entries.length;
159
+ const { payload: rectangle } = await callToolJson(context, "add-rectangle", {
160
+ x: 100,
161
+ y: 100,
162
+ width: 180,
163
+ height: 90,
164
+ text: "File Export Cell",
165
+ style: "fillColor=#ffe6cc;strokeColor=#d79b00;whiteSpace=wrap;html=1;",
166
+ });
167
+ expectToolSuccess(rectangle);
168
+ const exportPath = join(context.artifactRunDir, "export-xml-file.xml");
169
+ const result = await callToolRaw(context, "export-diagram", {
170
+ format: "xml",
171
+ output_path: exportPath,
172
+ });
173
+ const content = result.content;
174
+ await withVerificationScreenshot(context, "export-xml-file", "before-live-state-verification", async () => {
175
+ expect(existsSync(exportPath)).toBe(true);
176
+ const fileContent = readFileSync(exportPath, "utf-8");
177
+ expect(fileContent).toContain("mxGraphModel");
178
+ expect(fileContent).toContain("File Export Cell");
179
+ expect(fileContent).toContain(rectangle.result.id);
180
+ expect(fileContent).toContain("fillColor=#ffe6cc");
181
+ expect(fileContent).toContain('width="180"');
182
+ expect(fileContent).toContain('height="90"');
183
+ const returnedXml = findText(content, "mxGraphModel");
184
+ expect(returnedXml).toBeDefined();
185
+ expect(fileContent).toBe(returnedXml.text);
186
+ const savedContent = findText(content, "Saved to:");
187
+ expect(savedContent).toBeDefined();
188
+ expect(savedContent.text).toContain(exportPath);
189
+ });
190
+ await expectNoBrowserErrors(context, "export-xml-file");
191
+ await expectNoServerErrors(context, "export-xml-file", logCountBefore);
192
+ }, 180000);
193
+ it("exports SVG to a file with valid SVG structure", async () => {
194
+ await resetDiagram(context);
195
+ context.browserMessages.length = 0;
196
+ const logCountBefore = context.logger.entries.length;
197
+ const { payload: rectangle } = await callToolJson(context, "add-rectangle", {
198
+ x: 60,
199
+ y: 60,
200
+ width: 150,
201
+ height: 80,
202
+ text: "SVG File Cell",
203
+ });
204
+ expectToolSuccess(rectangle);
205
+ const exportPath = join(context.artifactRunDir, "export-svg-file.svg");
206
+ const result = await callToolRaw(context, "export-diagram", {
207
+ format: "svg",
208
+ output_path: exportPath,
209
+ });
210
+ const content = result.content;
211
+ await withVerificationScreenshot(context, "export-svg-file", "before-live-state-verification", async () => {
212
+ expect(existsSync(exportPath)).toBe(true);
213
+ const fileContent = readFileSync(exportPath, "utf-8");
214
+ expect(fileContent).toContain("<svg");
215
+ expect(fileContent).toContain("</svg>");
216
+ expect(fileContent).toContain("SVG File Cell");
217
+ expect(fileContent).toContain("xmlns=");
218
+ expect(fileContent).toMatch(/width="\d+/);
219
+ expect(fileContent).toMatch(/height="\d+/);
220
+ const returnedSvg = findText(content, "<svg");
221
+ expect(returnedSvg).toBeDefined();
222
+ expect(fileContent).toBe(returnedSvg.text);
223
+ const savedContent = findText(content, "Saved to:");
224
+ expect(savedContent).toBeDefined();
225
+ });
226
+ await expectNoBrowserErrors(context, "export-svg-file");
227
+ await expectNoServerErrors(context, "export-svg-file", logCountBefore);
228
+ }, 180000);
229
+ it("exports PNG to a file with valid PNG binary data", async () => {
230
+ await resetDiagram(context);
231
+ context.browserMessages.length = 0;
232
+ const logCountBefore = context.logger.entries.length;
233
+ const { payload: rectangle } = await callToolJson(context, "add-rectangle", {
234
+ x: 70,
235
+ y: 70,
236
+ width: 130,
237
+ height: 65,
238
+ text: "PNG File Cell",
239
+ });
240
+ expectToolSuccess(rectangle);
241
+ const exportPath = join(context.artifactRunDir, "export-png-file.png");
242
+ const result = await callToolRaw(context, "export-diagram", {
243
+ format: "png",
244
+ output_path: exportPath,
245
+ });
246
+ const content = result.content;
247
+ await withVerificationScreenshot(context, "export-png-file", "before-live-state-verification", async () => {
248
+ expect(existsSync(exportPath)).toBe(true);
249
+ const fileBuffer = readFileSync(exportPath);
250
+ expect(fileBuffer[0]).toBe(0x89);
251
+ expect(fileBuffer[1]).toBe(0x50);
252
+ expect(fileBuffer[2]).toBe(0x4e);
253
+ expect(fileBuffer[3]).toBe(0x47);
254
+ expect(fileBuffer.length).toBeGreaterThan(500);
255
+ const imageContent = content.find((item) => item.type === "image");
256
+ expect(imageContent).toBeDefined();
257
+ const returnedBuffer = Buffer.from(imageContent.data, "base64");
258
+ expect(fileBuffer.equals(returnedBuffer)).toBe(true);
259
+ const savedContent = content.find((item) => item.type === "text" &&
260
+ item.text?.includes("Saved to:"));
261
+ expect(savedContent).toBeDefined();
262
+ });
263
+ await expectNoBrowserErrors(context, "export-png-file");
264
+ await expectNoServerErrors(context, "export-png-file", logCountBefore);
265
+ }, 180000);
266
+ it("exports with transparent background and SVG has no background rect", async () => {
267
+ await resetDiagram(context);
268
+ context.browserMessages.length = 0;
269
+ const logCountBefore = context.logger.entries.length;
270
+ const { payload: rectangle } = await callToolJson(context, "add-rectangle", {
271
+ x: 100,
272
+ y: 100,
273
+ width: 120,
274
+ height: 60,
275
+ text: "Transparent BG",
276
+ });
277
+ expectToolSuccess(rectangle);
278
+ const opaqueResult = await callToolRaw(context, "export-diagram", {
279
+ format: "svg",
280
+ transparent: false,
281
+ background: "#ff0000",
282
+ });
283
+ const opaqueContent = opaqueResult.content;
284
+ const opaqueSvg = findText(opaqueContent, "<svg");
285
+ const transparentResult = await callToolRaw(context, "export-diagram", {
286
+ format: "svg",
287
+ transparent: true,
288
+ });
289
+ const transparentContent = transparentResult.content;
290
+ await withVerificationScreenshot(context, "export-transparent", "before-live-state-verification", async () => {
291
+ const svgContent = findText(transparentContent, "<svg");
292
+ expect(svgContent).toBeDefined();
293
+ expect(svgContent.text).toContain("Transparent BG");
294
+ expect(svgContent.text).toContain("</svg>");
295
+ expect(svgContent.text).not.toContain("#ff0000");
296
+ expect(opaqueSvg).toBeDefined();
297
+ expect(opaqueSvg.text).toContain("#ff0000");
298
+ const metaContent = findText(transparentContent, "Exported svg");
299
+ expect(metaContent).toBeDefined();
300
+ await saveArtifact(context.artifactRunDir, "export-transparent-opaque.svg", opaqueSvg.text);
301
+ await saveArtifact(context.artifactRunDir, "export-transparent-transparent.svg", svgContent.text);
302
+ });
303
+ await expectNoBrowserErrors(context, "export-transparent");
304
+ await expectNoServerErrors(context, "export-transparent", logCountBefore);
305
+ }, 180000);
306
+ it("exports SVG with embedded XML containing diagram data", async () => {
307
+ await resetDiagram(context);
308
+ context.browserMessages.length = 0;
309
+ const logCountBefore = context.logger.entries.length;
310
+ const { payload: rectangle } = await callToolJson(context, "add-rectangle", {
311
+ x: 90,
312
+ y: 90,
313
+ width: 150,
314
+ height: 75,
315
+ text: "Embed XML Cell",
316
+ });
317
+ expectToolSuccess(rectangle);
318
+ const plainResult = await callToolRaw(context, "export-diagram", {
319
+ format: "svg",
320
+ embed_xml: false,
321
+ });
322
+ const plainContent = plainResult.content;
323
+ const embeddedResult = await callToolRaw(context, "export-diagram", {
324
+ format: "svg",
325
+ embed_xml: true,
326
+ });
327
+ const embeddedContent = embeddedResult.content;
328
+ await withVerificationScreenshot(context, "export-svg-embed-xml", "before-live-state-verification", async () => {
329
+ const embeddedSvg = findText(embeddedContent, "Embed XML Cell");
330
+ expect(embeddedSvg).toBeDefined();
331
+ const embeddedData = embeddedSvg.text;
332
+ expect(embeddedData).toContain("mxGraphModel");
333
+ expect(embeddedData).toContain("mxfile");
334
+ const plainSvg = findText(plainContent, "<svg");
335
+ expect(plainSvg).toBeDefined();
336
+ expect(plainSvg.text).not.toContain("mxfile");
337
+ expect(embeddedData).not.toBe(plainSvg.text);
338
+ const metaContent = findText(embeddedContent, "Exported svg");
339
+ expect(metaContent).toBeDefined();
340
+ expect(metaContent.text).toContain("image/svg+xml");
341
+ await saveArtifact(context.artifactRunDir, "export-svg-plain.svg", plainSvg.text);
342
+ await saveArtifact(context.artifactRunDir, "export-svg-embedded.svg", embeddedData);
343
+ });
344
+ await expectNoBrowserErrors(context, "export-svg-embed-xml");
345
+ await expectNoServerErrors(context, "export-svg-embed-xml", logCountBefore);
346
+ }, 180000);
347
+ it("exports only the selected cell in SVG and excludes unselected cells", async () => {
348
+ await resetDiagram(context);
349
+ context.browserMessages.length = 0;
350
+ const logCountBefore = context.logger.entries.length;
351
+ const { payload: selectedRect } = await callToolJson(context, "add-rectangle", {
352
+ x: 100,
353
+ y: 100,
354
+ width: 140,
355
+ height: 70,
356
+ text: "Selected Cell",
357
+ });
358
+ expectToolSuccess(selectedRect);
359
+ const { payload: otherRect } = await callToolJson(context, "add-rectangle", {
360
+ x: 350,
361
+ y: 100,
362
+ width: 140,
363
+ height: 70,
364
+ text: "Unselected Cell",
365
+ });
366
+ expectToolSuccess(otherRect);
367
+ await selectCell(context.page, selectedRect.result.id);
368
+ // SVG export respects selection_only by rendering only the selected cells
369
+ const selectionResult = await callToolRaw(context, "export-diagram", {
370
+ format: "svg",
371
+ selection_only: true,
372
+ size: "selection",
373
+ });
374
+ const selectionContent = selectionResult.content;
375
+ const fullResult = await callToolRaw(context, "export-diagram", {
376
+ format: "svg",
377
+ });
378
+ const fullContent = fullResult.content;
379
+ await withVerificationScreenshot(context, "export-selection-only", "before-live-state-verification", async () => {
380
+ const selectionSvg = findText(selectionContent, "<svg");
381
+ expect(selectionSvg).toBeDefined();
382
+ expect(selectionSvg.text).toContain("Selected Cell");
383
+ expect(selectionSvg.text).not.toContain("Unselected Cell");
384
+ const fullSvg = findText(fullContent, "<svg");
385
+ expect(fullSvg).toBeDefined();
386
+ expect(fullSvg.text).toContain("Selected Cell");
387
+ expect(fullSvg.text).toContain("Unselected Cell");
388
+ await saveArtifact(context.artifactRunDir, "export-selection-only.svg", selectionSvg.text);
389
+ await saveArtifact(context.artifactRunDir, "export-selection-full.svg", fullSvg.text);
390
+ });
391
+ await expectNoBrowserErrors(context, "export-selection-only");
392
+ await expectNoServerErrors(context, "export-selection-only", logCountBefore);
393
+ }, 180000);
394
+ it("exports multiple cells and all appear in the output", async () => {
395
+ await resetDiagram(context);
396
+ context.browserMessages.length = 0;
397
+ const logCountBefore = context.logger.entries.length;
398
+ const cellNames = ["Alpha", "Beta", "Gamma"];
399
+ const cellIds = [];
400
+ for (let i = 0; i < cellNames.length; i++) {
401
+ const { payload } = await callToolJson(context, "add-rectangle", {
402
+ x: 50 + i * 200,
403
+ y: 100,
404
+ width: 140,
405
+ height: 70,
406
+ text: cellNames[i],
407
+ });
408
+ expectToolSuccess(payload);
409
+ cellIds.push(payload.result.id);
410
+ }
411
+ const xmlResult = await callToolRaw(context, "export-diagram", {
412
+ format: "xml",
413
+ });
414
+ const xmlContent = xmlResult.content;
415
+ const svgResult = await callToolRaw(context, "export-diagram", {
416
+ format: "svg",
417
+ });
418
+ const svgContent = svgResult.content;
419
+ const pngResult = await callToolRaw(context, "export-diagram", {
420
+ format: "png",
421
+ });
422
+ const pngContent = pngResult.content;
423
+ await withVerificationScreenshot(context, "export-multi-cell", "before-live-state-verification", async () => {
424
+ const xml = findText(xmlContent, "mxGraphModel");
425
+ expect(xml).toBeDefined();
426
+ for (const name of cellNames) {
427
+ expect(xml.text).toContain(name);
428
+ }
429
+ for (const id of cellIds) {
430
+ expect(xml.text).toContain(id);
431
+ }
432
+ const vertexMatches = xml.text.match(/vertex="1"/g);
433
+ expect(vertexMatches).not.toBeNull();
434
+ expect(vertexMatches.length).toBeGreaterThanOrEqual(cellNames.length);
435
+ const svg = findText(svgContent, "<svg");
436
+ expect(svg).toBeDefined();
437
+ for (const name of cellNames) {
438
+ expect(svg.text).toContain(name);
439
+ }
440
+ await saveArtifact(context.artifactRunDir, "export-multi-cell.xml", xml.text);
441
+ await saveArtifact(context.artifactRunDir, "export-multi-cell.svg", svg.text);
442
+ const pngImage = pngContent.find((item) => item.type === "image");
443
+ expect(pngImage).toBeDefined();
444
+ await saveArtifact(context.artifactRunDir, "export-multi-cell.png", Buffer.from(pngImage.data, "base64"));
445
+ });
446
+ await expectNoBrowserErrors(context, "export-multi-cell");
447
+ await expectNoServerErrors(context, "export-multi-cell", logCountBefore);
448
+ }, 180000);
449
+ it("exports an empty diagram with only root cells", async () => {
450
+ await resetDiagram(context);
451
+ context.browserMessages.length = 0;
452
+ const logCountBefore = context.logger.entries.length;
453
+ const result = await callToolRaw(context, "export-diagram", {
454
+ format: "xml",
455
+ });
456
+ const content = result.content;
457
+ await withVerificationScreenshot(context, "export-empty", "before-live-state-verification", async () => {
458
+ const xmlContent = findText(content, "mxGraphModel");
459
+ expect(xmlContent).toBeDefined();
460
+ const xml = xmlContent.text;
461
+ expect(xml).toContain("mxGraphModel");
462
+ expect(xml).toContain('<mxCell id="0"');
463
+ expect(xml).toContain('<mxCell id="1"');
464
+ expect(xml).not.toMatch(/vertex="1"/);
465
+ const metaContent = findText(content, "Exported xml");
466
+ expect(metaContent).toBeDefined();
467
+ expect(metaContent.text).toContain("application/xml");
468
+ await saveArtifact(context.artifactRunDir, "export-empty.xml", xml);
469
+ });
470
+ await expectNoBrowserErrors(context, "export-empty");
471
+ await expectNoServerErrors(context, "export-empty", logCountBefore);
472
+ }, 180000);
473
+ });