drawio-mcp-server 2.1.1 → 2.3.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 (38) hide show
  1. package/README.md +21 -68
  2. package/build/assets/auto-refresh.js +21 -0
  3. package/build/assets/auto-refresh.test.js +54 -0
  4. package/build/assets/downloader.js +4 -0
  5. package/build/assets/version.js +38 -0
  6. package/build/assets/version.test.js +26 -0
  7. package/build/documents-changed-broadcast.test.js +123 -0
  8. package/build/drawio-compat/log-report.js +21 -0
  9. package/build/drawio-compat/log-report.test.js +43 -0
  10. package/build/drawio-compat/matrix.js +14 -0
  11. package/build/index.js +152 -11
  12. package/build/install/config-io.js +21 -0
  13. package/build/install/config-io.test.js +37 -0
  14. package/build/install/hosts/claude-code.js +37 -0
  15. package/build/install/hosts/claude-code.test.js +47 -0
  16. package/build/install/hosts/claude-desktop.js +45 -0
  17. package/build/install/hosts/claude-desktop.test.js +57 -0
  18. package/build/install/hosts/codex.js +171 -0
  19. package/build/install/hosts/codex.test.js +124 -0
  20. package/build/install/hosts/index.js +15 -0
  21. package/build/install/hosts/opencode.js +48 -0
  22. package/build/install/hosts/opencode.test.js +60 -0
  23. package/build/install/hosts/zed.js +37 -0
  24. package/build/install/hosts/zed.test.js +47 -0
  25. package/build/install/index.js +170 -0
  26. package/build/install/index.test.js +115 -0
  27. package/build/install/install.integration.test.js +103 -0
  28. package/build/install/types.js +1 -0
  29. package/build/multi-transport.test.js +1 -0
  30. package/build/plugin/mcp-plugin.js +406 -89
  31. package/build/real-environment/import-export.test.js +107 -0
  32. package/build/stdio-shutdown.test.js +177 -0
  33. package/build/tool-registry.test.js +57 -0
  34. package/build/tools/index.js +4 -0
  35. package/build/tools/save-document.js +5 -0
  36. package/build/tools/set-document-title.js +12 -0
  37. package/build/vendored/compat/index.js +35 -0
  38. package/package.json +15 -10
@@ -224,94 +224,6 @@ var DrawMcp = (() => {
224
224
  );
225
225
  }
226
226
  }
227
- function import_mermaid(ui2, options) {
228
- const opts = options;
229
- const mermaidSource = opts.mermaid_source;
230
- const mode = opts.mode ?? "native";
231
- const insertMode = opts.insert_mode ?? "add";
232
- if (!mermaidSource || typeof mermaidSource !== "string") {
233
- return Promise.resolve({
234
- success: false,
235
- message: "mermaid_source must be a non-empty string"
236
- });
237
- }
238
- if (typeof ui2?.parseMermaidDiagram !== "function") {
239
- return Promise.resolve({
240
- success: false,
241
- message: "ui.parseMermaidDiagram is not available; this Draw.io build does not expose Mermaid support."
242
- });
243
- }
244
- const enableParser = mode === "native";
245
- return new Promise((resolve) => {
246
- let settled = false;
247
- const settle = (result) => {
248
- if (settled) {
249
- return;
250
- }
251
- settled = true;
252
- resolve(result);
253
- };
254
- try {
255
- ui2.parseMermaidDiagram(
256
- mermaidSource,
257
- void 0,
258
- (xml) => {
259
- if (!xml || typeof xml !== "string") {
260
- settle({
261
- success: false,
262
- message: "Mermaid parser returned empty XML"
263
- });
264
- return;
265
- }
266
- try {
267
- const importResult = import_diagram(ui2, {
268
- data: xml,
269
- format: "xml",
270
- mode: insertMode
271
- });
272
- if (!importResult.success) {
273
- settle({
274
- success: false,
275
- message: `Mermaid converted, but inserting into the diagram failed: ${importResult.message}`
276
- });
277
- return;
278
- }
279
- settle({
280
- success: true,
281
- mode,
282
- message: importResult.message,
283
- cells: importResult.cells,
284
- xml
285
- });
286
- } catch (err) {
287
- settle({
288
- success: false,
289
- message: `Insert after Mermaid conversion failed: ${err instanceof Error ? err.message : String(err)}`
290
- });
291
- }
292
- },
293
- (err) => {
294
- settle({
295
- success: false,
296
- message: `Mermaid render failed: ${err?.message ?? String(err)}`
297
- });
298
- },
299
- (err) => {
300
- settle({
301
- success: false,
302
- message: `Mermaid parse error: ${err?.message ?? String(err)}`
303
- });
304
- },
305
- enableParser
306
- );
307
- } catch (err) {
308
- settle({
309
- success: false,
310
- message: `parseMermaidDiagram threw: ${err instanceof Error ? err.message : String(err)}`
311
- });
312
- }
313
- });
314
- }
315
227
  function transform_NamedNodeMap_to_record(attributes) {
316
228
  const tstr = Object.prototype.toString.call(attributes);
317
229
  if (tstr !== "[object NamedNodeMap]") {
@@ -1997,6 +1909,7 @@ var DrawMcp = (() => {
1997
1909
  );
1998
1910
  codec.decode(graphModelElement, model);
1999
1911
  }
1912
+ sync_live_current_page_state(ui2);
2000
1913
  return {
2001
1914
  success: true,
2002
1915
  message: `Diagram replaced successfully${filename ? ` from ${filename}` : ""}`,
@@ -2160,6 +2073,7 @@ var DrawMcp = (() => {
2160
2073
  } finally {
2161
2074
  model.endUpdate();
2162
2075
  }
2076
+ sync_live_current_page_state(ui2);
2163
2077
  return {
2164
2078
  success: true,
2165
2079
  message: `Diagram imported successfully (new page created)${filename ? `: ${filename}` : ""}`,
@@ -2304,6 +2218,343 @@ var DrawMcp = (() => {
2304
2218
  return s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "") || "default";
2305
2219
  }
2306
2220
 
2221
+ // ../drawio-mcp-compat/dist/index.js
2222
+ var SEMVER_HEAD = /^(\d+)\.(\d+)\.(\d+)/;
2223
+ function parseVersion(raw) {
2224
+ const m = SEMVER_HEAD.exec(raw);
2225
+ if (!m)
2226
+ return null;
2227
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
2228
+ }
2229
+ function compareVersion(a, b) {
2230
+ for (let i = 0; i < 3; i++) {
2231
+ if (a[i] < b[i])
2232
+ return -1;
2233
+ if (a[i] > b[i])
2234
+ return 1;
2235
+ }
2236
+ return 0;
2237
+ }
2238
+ function isBelowFloor(v, floor) {
2239
+ const parsed = parseVersion(floor);
2240
+ if (!parsed)
2241
+ return false;
2242
+ return compareVersion(v, parsed) < 0;
2243
+ }
2244
+ function isInRange(v, r) {
2245
+ const min = parseVersion(r.min);
2246
+ if (!min)
2247
+ return false;
2248
+ if (compareVersion(v, min) < 0)
2249
+ return false;
2250
+ if (r.maxExclusive === null)
2251
+ return true;
2252
+ const max = parseVersion(r.maxExclusive);
2253
+ if (!max)
2254
+ return true;
2255
+ return compareVersion(v, max) < 0;
2256
+ }
2257
+
2258
+ // src/drawio-compat/detect.ts
2259
+ function detectDrawioVersion(ui2) {
2260
+ const raw = globalThis.EditorUi?.VERSION ?? ui2?.constructor?.VERSION;
2261
+ if (typeof raw !== "string") {
2262
+ return { ok: false, reason: "missing", raw: null };
2263
+ }
2264
+ const semver = parseVersion(raw);
2265
+ if (!semver) return { ok: false, reason: "unparseable", raw };
2266
+ return { ok: true, raw, semver };
2267
+ }
2268
+ var cached = null;
2269
+ function getDetectedDrawioVersion(ui2) {
2270
+ if (cached !== null) return cached;
2271
+ const result = detectDrawioVersion(ui2);
2272
+ if (result.ok) cached = result;
2273
+ return result;
2274
+ }
2275
+
2276
+ // src/drawio-compat/dispatch.ts
2277
+ function dispatchTool(toolName, detected, matrix) {
2278
+ const entries = matrix.versionedTools[toolName];
2279
+ if (!entries || entries.length === 0) return null;
2280
+ if (!detected.ok) {
2281
+ return { kind: "no-version", reason: detected.reason };
2282
+ }
2283
+ if (isBelowFloor(detected.semver, matrix.supportedFloor)) {
2284
+ return {
2285
+ kind: "below-floor",
2286
+ floor: matrix.supportedFloor,
2287
+ detected: detected.raw
2288
+ };
2289
+ }
2290
+ for (const entry of entries) {
2291
+ if (isInRange(detected.semver, entry.range)) {
2292
+ return { kind: "matched", impl: entry.impl };
2293
+ }
2294
+ }
2295
+ const sorted = [...entries].sort((a, b) => {
2296
+ const am = parseVersion(a.range.min);
2297
+ const bm = parseVersion(b.range.min);
2298
+ return compareVersion(am, bm);
2299
+ });
2300
+ return {
2301
+ kind: "above-window",
2302
+ lastRangeMin: sorted.at(-1).range.min,
2303
+ detected: detected.raw
2304
+ };
2305
+ }
2306
+
2307
+ // src/tools/import-mermaid/shared.ts
2308
+ function validateOptions(options) {
2309
+ const opts = options;
2310
+ const source = opts.mermaid_source;
2311
+ if (!source || typeof source !== "string") {
2312
+ return {
2313
+ success: false,
2314
+ message: "mermaid_source must be a non-empty string"
2315
+ };
2316
+ }
2317
+ return {
2318
+ source,
2319
+ mode: opts.mode ?? "native",
2320
+ insertMode: opts.insert_mode ?? "add"
2321
+ };
2322
+ }
2323
+ function runInsertFlow(ui2, mode, insertMode, resolve) {
2324
+ let settled = false;
2325
+ const settle = (result) => {
2326
+ if (settled) return;
2327
+ settled = true;
2328
+ resolve(result);
2329
+ };
2330
+ const onXml = (xml) => {
2331
+ if (!xml || typeof xml !== "string") {
2332
+ settle({ success: false, message: "Mermaid parser returned empty XML" });
2333
+ return;
2334
+ }
2335
+ try {
2336
+ const importResult = import_diagram(ui2, {
2337
+ data: xml,
2338
+ format: "xml",
2339
+ mode: insertMode
2340
+ });
2341
+ if (!importResult.success) {
2342
+ settle({
2343
+ success: false,
2344
+ message: `Mermaid converted, but inserting into the diagram failed: ${importResult.message}`
2345
+ });
2346
+ return;
2347
+ }
2348
+ settle({
2349
+ success: true,
2350
+ mode,
2351
+ message: importResult.message,
2352
+ cells: importResult.cells,
2353
+ xml
2354
+ });
2355
+ } catch (err) {
2356
+ settle({
2357
+ success: false,
2358
+ message: `Insert after Mermaid conversion failed: ${err instanceof Error ? err.message : String(err)}`
2359
+ });
2360
+ }
2361
+ };
2362
+ const onError = (err) => {
2363
+ settle({
2364
+ success: false,
2365
+ message: `Mermaid render failed: ${err?.message ?? String(err)}`
2366
+ });
2367
+ };
2368
+ return { onXml, onError };
2369
+ }
2370
+
2371
+ // src/tools/import-mermaid/v29.ts
2372
+ function import_mermaid(ui2, options) {
2373
+ const validated = validateOptions(options);
2374
+ if ("success" in validated) return Promise.resolve(validated);
2375
+ const { source, mode, insertMode } = validated;
2376
+ if (typeof ui2?.parseMermaidDiagram !== "function") {
2377
+ return Promise.resolve({
2378
+ success: false,
2379
+ message: "ui.parseMermaidDiagram is not available; this Draw.io build does not expose Mermaid support."
2380
+ });
2381
+ }
2382
+ const enableParser = mode === "native";
2383
+ return new Promise((resolve) => {
2384
+ const { onXml, onError } = runInsertFlow(ui2, mode, insertMode, resolve);
2385
+ try {
2386
+ ui2.parseMermaidDiagram(
2387
+ source,
2388
+ void 0,
2389
+ onXml,
2390
+ onError,
2391
+ onError,
2392
+ enableParser
2393
+ );
2394
+ } catch (err) {
2395
+ resolve({
2396
+ success: false,
2397
+ message: `parseMermaidDiagram threw: ${err instanceof Error ? err.message : String(err)}`
2398
+ });
2399
+ }
2400
+ });
2401
+ }
2402
+
2403
+ // src/tools/import-mermaid/v30.ts
2404
+ function import_mermaid2(ui2, options) {
2405
+ const validated = validateOptions(options);
2406
+ if ("success" in validated) return Promise.resolve(validated);
2407
+ const { source, mode, insertMode } = validated;
2408
+ if (typeof ui2?.parseMermaidDiagram !== "function") {
2409
+ return Promise.resolve({
2410
+ success: false,
2411
+ message: "ui.parseMermaidDiagram is not available; this Draw.io build does not expose Mermaid support."
2412
+ });
2413
+ }
2414
+ return new Promise((resolve) => {
2415
+ const { onXml, onError } = runInsertFlow(ui2, mode, insertMode, resolve);
2416
+ try {
2417
+ if (mode === "embed" && typeof ui2.parseMermaidImage === "function") {
2418
+ ui2.parseMermaidImage(source, onXml, onError);
2419
+ } else {
2420
+ ui2.parseMermaidDiagram(source, void 0, onXml, onError, onError);
2421
+ }
2422
+ } catch (err) {
2423
+ resolve({
2424
+ success: false,
2425
+ message: `Mermaid API threw: ${err instanceof Error ? err.message : String(err)}`
2426
+ });
2427
+ }
2428
+ });
2429
+ }
2430
+
2431
+ // src/drawio-compat/matrix.ts
2432
+ var COMPAT_MATRIX = {
2433
+ supportedFloor: "29.0.0",
2434
+ versionedTools: {
2435
+ "import-mermaid": [
2436
+ {
2437
+ range: { min: "29.0.0", maxExclusive: "30.0.0" },
2438
+ impl: import_mermaid
2439
+ },
2440
+ {
2441
+ range: { min: "30.0.0", maxExclusive: null },
2442
+ impl: import_mermaid2
2443
+ }
2444
+ ]
2445
+ }
2446
+ };
2447
+
2448
+ // src/tools/import-mermaid/index.ts
2449
+ var TOOL_NAME = "import-mermaid";
2450
+ function import_mermaid3(ui2, options) {
2451
+ const detected = getDetectedDrawioVersion(ui2);
2452
+ const outcome = dispatchTool(TOOL_NAME, detected, COMPAT_MATRIX);
2453
+ if (outcome === null) {
2454
+ return Promise.resolve({
2455
+ success: false,
2456
+ message: `import-mermaid has no matrix entry; refusing to guess.`
2457
+ });
2458
+ }
2459
+ switch (outcome.kind) {
2460
+ case "matched":
2461
+ return outcome.impl(ui2, options);
2462
+ case "above-window": {
2463
+ const entries = COMPAT_MATRIX.versionedTools[TOOL_NAME] ?? [];
2464
+ const fallback = entries.at(-1);
2465
+ if (!fallback) {
2466
+ return Promise.resolve({
2467
+ success: false,
2468
+ message: `no impl available for drawio v${outcome.detected}`
2469
+ });
2470
+ }
2471
+ return fallback.impl(ui2, options);
2472
+ }
2473
+ case "below-floor":
2474
+ return Promise.resolve({
2475
+ success: false,
2476
+ message: `drawio v${outcome.detected} predates supported floor v${outcome.floor}. Upgrade drawio.`
2477
+ });
2478
+ case "no-version":
2479
+ return Promise.resolve({
2480
+ success: false,
2481
+ message: `cannot detect drawio version (${outcome.reason}); pin a supported drawio build.`
2482
+ });
2483
+ }
2484
+ }
2485
+
2486
+ // src/tools/save-document/index.ts
2487
+ function normalize_optional_string2(value) {
2488
+ if (value === void 0 || value === null || value === "") {
2489
+ return null;
2490
+ }
2491
+ return String(value);
2492
+ }
2493
+ var save_document = (ui2) => {
2494
+ const saveAction = ui2?.actions?.get?.("save");
2495
+ if (!saveAction || typeof saveAction.funct !== "function") {
2496
+ throw new Error("The Draw.io save action is not available");
2497
+ }
2498
+ if (typeof saveAction.isEnabled === "function" && saveAction.isEnabled() !== true) {
2499
+ throw new Error("The Draw.io save action is currently disabled");
2500
+ }
2501
+ saveAction.funct();
2502
+ const file = ui2?.getCurrentFile?.();
2503
+ return {
2504
+ triggered: true,
2505
+ title: normalize_optional_string2(file?.getTitle?.()),
2506
+ mode: normalize_optional_string2(file?.getMode?.())
2507
+ };
2508
+ };
2509
+
2510
+ // src/tools/set-document-title/index.ts
2511
+ var DRAWIO_FILE_SUFFIX = /(\.drawio\.(?:svg|png)|\.drawio|\.xml|\.svg|\.png)$/i;
2512
+ function normalize_optional_string3(value) {
2513
+ if (value === void 0 || value === null || value === "") {
2514
+ return null;
2515
+ }
2516
+ return String(value);
2517
+ }
2518
+ function title_with_preserved_suffix(currentTitle, requestedTitle) {
2519
+ const title = normalize_optional_string3(requestedTitle)?.trim() ?? "";
2520
+ if (!title) {
2521
+ throw new Error("`title` must not be empty");
2522
+ }
2523
+ const currentSuffix = currentTitle?.match(DRAWIO_FILE_SUFFIX)?.[1] ?? "";
2524
+ if (!currentSuffix || DRAWIO_FILE_SUFFIX.test(title)) {
2525
+ return title;
2526
+ }
2527
+ return `${title}${currentSuffix}`;
2528
+ }
2529
+ function tool_error(error) {
2530
+ return error instanceof Error ? error : new Error(String(error));
2531
+ }
2532
+ var set_document_title = (ui2, options) => {
2533
+ const file = ui2?.getCurrentFile?.();
2534
+ if (!file) {
2535
+ throw new Error("No active Draw.io file is available");
2536
+ }
2537
+ if (typeof file.rename !== "function") {
2538
+ throw new Error("Document renaming is not supported by this storage mode");
2539
+ }
2540
+ const previousTitle = normalize_optional_string3(file.getTitle?.());
2541
+ const nextTitle = title_with_preserved_suffix(previousTitle, options.title);
2542
+ return new Promise(
2543
+ (resolve, reject) => {
2544
+ file.rename?.(
2545
+ nextTitle,
2546
+ () => {
2547
+ resolve({
2548
+ previous_title: previousTitle,
2549
+ title: normalize_optional_string3(file.getTitle?.()) ?? nextTitle
2550
+ });
2551
+ },
2552
+ (error) => reject(tool_error(error))
2553
+ );
2554
+ }
2555
+ );
2556
+ };
2557
+
2307
2558
  // src/tool-registry.ts
2308
2559
  var VISIBLE_PAGE_EXECUTION = {
2309
2560
  mode: "visible-page"
@@ -2583,7 +2834,7 @@ var DrawMcp = (() => {
2583
2834
  page_tool({
2584
2835
  name: "import-mermaid",
2585
2836
  params: /* @__PURE__ */ new Set(["mermaid_source", "mode", "insert_mode", "target_page"]),
2586
- handler: import_mermaid,
2837
+ handler: import_mermaid3,
2587
2838
  pageExecution: VISIBLE_PAGE_MUTATION_EXECUTION,
2588
2839
  skip: skip_page_execution_for_new_page_mermaid
2589
2840
  }),
@@ -2611,6 +2862,16 @@ var DrawMcp = (() => {
2611
2862
  name: "rename-page",
2612
2863
  params: /* @__PURE__ */ new Set(["page", "name"]),
2613
2864
  handler: rename_page
2865
+ },
2866
+ {
2867
+ name: "set-document-title",
2868
+ params: /* @__PURE__ */ new Set(["title"]),
2869
+ handler: set_document_title
2870
+ },
2871
+ {
2872
+ name: "save-document",
2873
+ params: /* @__PURE__ */ new Set([]),
2874
+ handler: save_document
2614
2875
  }
2615
2876
  ];
2616
2877
  var toolDefinitions = rawToolDefinitions.map(
@@ -2621,6 +2882,61 @@ var DrawMcp = (() => {
2621
2882
  })
2622
2883
  );
2623
2884
 
2885
+ // src/drawio-compat/report.ts
2886
+ function computeCompatReport(matrix = COMPAT_MATRIX) {
2887
+ const detected = getDetectedDrawioVersion();
2888
+ if (!detected.ok) {
2889
+ return {
2890
+ drawioVersion: detected.raw,
2891
+ state: "no-version",
2892
+ floor: matrix.supportedFloor,
2893
+ detail: detected.reason
2894
+ };
2895
+ }
2896
+ if (isBelowFloor(detected.semver, matrix.supportedFloor)) {
2897
+ return {
2898
+ drawioVersion: detected.raw,
2899
+ state: "below-floor",
2900
+ floor: matrix.supportedFloor
2901
+ };
2902
+ }
2903
+ const anyBounded = Object.values(matrix.versionedTools).some(
2904
+ (entries) => entries.at(-1)?.range.maxExclusive !== null
2905
+ );
2906
+ if (anyBounded) {
2907
+ const boundedFor = Object.entries(matrix.versionedTools).filter(
2908
+ ([, entries]) => entries.at(-1)?.range.maxExclusive !== null
2909
+ );
2910
+ for (const [, entries] of boundedFor) {
2911
+ const newest = entries.at(-1);
2912
+ const max = parseVersion(newest.range.maxExclusive);
2913
+ if (max) {
2914
+ if (detected.semver[0] > max[0] || detected.semver[0] === max[0] && detected.semver[1] > max[1] || detected.semver[0] === max[0] && detected.semver[1] === max[1] && detected.semver[2] >= max[2]) {
2915
+ return {
2916
+ drawioVersion: detected.raw,
2917
+ state: "above-window",
2918
+ floor: matrix.supportedFloor,
2919
+ detail: newest.range.min
2920
+ };
2921
+ }
2922
+ }
2923
+ }
2924
+ }
2925
+ return {
2926
+ drawioVersion: detected.raw,
2927
+ state: "ok",
2928
+ floor: matrix.supportedFloor
2929
+ };
2930
+ }
2931
+ function sendCompatReport(send, log = console.log.bind(console)) {
2932
+ const report = computeCompatReport();
2933
+ send({ __control: "compat-report", ...report });
2934
+ log(
2935
+ `[drawio-mcp] drawio version: ${report.drawioVersion ?? "unknown"} \u2014 compat state: ${report.state} (floor: ${report.floor})`
2936
+ );
2937
+ return report;
2938
+ }
2939
+
2624
2940
  // src/bootstrap.ts
2625
2941
  var SHAPE_EXTRACTION_MAX_ATTEMPTS = 10;
2626
2942
  var SHAPE_EXTRACTION_INTERVAL_MS = 1e3;
@@ -2822,6 +3138,7 @@ var DrawMcp = (() => {
2822
3138
  cleanups.push(() => clearInterval(intervalId));
2823
3139
  }
2824
3140
  }
3141
+ sendCompatReport(transport2.send.bind(transport2));
2825
3142
  return {
2826
3143
  syncDocumentState,
2827
3144
  dispose: () => {
@@ -79,4 +79,111 @@ describe("real environment/import export", () => {
79
79
  await expectNoBrowserErrors(context, "import-export");
80
80
  await expectNoServerErrors(context, "import-export", logCountBefore);
81
81
  }, 180000);
82
+ it("keeps ui.currentPage.root in sync with graph.model.root after replace import (regression for #56)", async () => {
83
+ await resetDiagram(context);
84
+ context.browserMessages.length = 0;
85
+ const logCountBefore = context.logger.entries.length;
86
+ 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="regression-1" value="Regression cell" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1"><mxGeometry x="40" y="40" width="120" height="60" as="geometry"/></mxCell></root></mxGraphModel>';
87
+ const { payload: importResult } = await callToolJson(context, "import-diagram", {
88
+ data: importXml,
89
+ format: "xml",
90
+ mode: "replace",
91
+ filename: "regression.drawio",
92
+ });
93
+ expectToolSuccess(importResult);
94
+ await context.page.waitForFunction(() => {
95
+ const maybeWindow = window;
96
+ const graph = maybeWindow.ui?.editor?.graph;
97
+ const model = graph?.getModel?.();
98
+ const cells = Object.values(model?.cells ?? {});
99
+ return cells.some((cell) => cell?.value === "Regression cell");
100
+ });
101
+ const pageState = await context.page.evaluate(() => {
102
+ const maybeWindow = window;
103
+ const ui = maybeWindow.ui;
104
+ const page = ui?.currentPage;
105
+ const model = ui?.editor?.graph?.getModel?.();
106
+ const modelRoot = model?.getRoot?.() ?? model?.root ?? null;
107
+ return {
108
+ pageRootMatchesModelRoot: page?.root === modelRoot,
109
+ graphModelNodeIsNull: page?.graphModelNode == null,
110
+ pageRootHasImportedCell: (() => {
111
+ if (!page?.root || typeof page.root.getChildAt !== "function")
112
+ return null;
113
+ const rootChild = page.root.getChildAt(0);
114
+ if (!rootChild || typeof rootChild.getChildAt !== "function")
115
+ return null;
116
+ for (let i = 0; i < (rootChild.getChildCount?.() ?? 0); i++) {
117
+ const child = rootChild.getChildAt(i);
118
+ if (child?.value === "Regression cell")
119
+ return true;
120
+ }
121
+ return false;
122
+ })(),
123
+ };
124
+ });
125
+ expect(pageState.pageRootMatchesModelRoot).toBe(true);
126
+ expect(pageState.graphModelNodeIsNull).toBe(true);
127
+ expect(pageState.pageRootHasImportedCell).toBe(true);
128
+ // The page-root identity asserted above is a proxy for correctness. What a
129
+ // user actually loses when currentPage goes stale is the *serialized* file:
130
+ // drawio serializes through the Page abstraction, so a stale page root ends
131
+ // up in the saved bytes while the live graph (holding the import) does not.
132
+ // Assert the import survives serialization, not just the in-memory model.
133
+ const serializedFileData = await context.page.evaluate(() => {
134
+ const maybeWindow = window;
135
+ const ui = maybeWindow.ui;
136
+ try {
137
+ if (typeof ui?.getXmlFileData === "function" &&
138
+ typeof maybeWindow.mxUtils?.getXml === "function") {
139
+ return String(maybeWindow.mxUtils.getXml(ui.getXmlFileData(true, false, true)));
140
+ }
141
+ }
142
+ catch {
143
+ /* fall through to getFileData */
144
+ }
145
+ try {
146
+ if (typeof ui?.getFileData === "function") {
147
+ return String(ui.getFileData(true, null, null, null, true, null, null, null, null, true));
148
+ }
149
+ }
150
+ catch {
151
+ /* unsupported build */
152
+ }
153
+ return null;
154
+ });
155
+ if (serializedFileData !== null) {
156
+ expect(serializedFileData).toContain("Regression cell");
157
+ }
158
+ const survivesPageRoundTrip = await context.page.evaluate(async () => {
159
+ const maybeWindow = window;
160
+ const ui = maybeWindow.ui;
161
+ if (!ui?.insertPage || !ui?.selectPage || !ui?.currentPage) {
162
+ return { skipped: true };
163
+ }
164
+ const originalPage = ui.currentPage;
165
+ const scratch = ui.insertPage();
166
+ ui.selectPage(scratch);
167
+ await new Promise((r) => setTimeout(r, 20));
168
+ ui.selectPage(originalPage);
169
+ await new Promise((r) => setTimeout(r, 20));
170
+ const model = ui.editor.graph.getModel();
171
+ const cells = Object.values(model?.cells ?? {});
172
+ const stillPresent = cells.some((cell) => cell?.value === "Regression cell");
173
+ if (typeof ui.removePage === "function") {
174
+ try {
175
+ ui.removePage(scratch);
176
+ }
177
+ catch {
178
+ /* best-effort cleanup */
179
+ }
180
+ }
181
+ return { skipped: false, stillPresent };
182
+ });
183
+ if (!survivesPageRoundTrip.skipped) {
184
+ expect(survivesPageRoundTrip.stillPresent).toBe(true);
185
+ }
186
+ await expectNoBrowserErrors(context, "import-export");
187
+ await expectNoServerErrors(context, "import-export", logCountBefore);
188
+ }, 180000);
82
189
  });