@miragon/modeler-shell 0.0.0-stage → 0.2.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 (147) hide show
  1. package/API.md +2869 -0
  2. package/LICENSE +21 -0
  3. package/README.md +566 -2
  4. package/THIRD_PARTY_NOTICES.md +52 -0
  5. package/dist/appearance-BBaM_4bD.d.ts +323 -0
  6. package/dist/appearance-D3FUcbu6.d.cts +53 -0
  7. package/dist/appearance-D3FUcbu6.d.ts +53 -0
  8. package/dist/appearance-XX3JC3pI.d.cts +323 -0
  9. package/dist/chunk-66HCSJNY.js +2792 -0
  10. package/dist/chunk-66HCSJNY.js.map +1 -0
  11. package/dist/chunk-KYDG5DMX.js +136 -0
  12. package/dist/chunk-KYDG5DMX.js.map +1 -0
  13. package/dist/chunk-N4QXFF3A.js +23 -0
  14. package/dist/chunk-N4QXFF3A.js.map +1 -0
  15. package/dist/chunk-OADW3GPA.js +785 -0
  16. package/dist/chunk-OADW3GPA.js.map +1 -0
  17. package/dist/core.cjs +873 -0
  18. package/dist/core.cjs.map +1 -0
  19. package/dist/core.d.cts +246 -0
  20. package/dist/core.d.ts +246 -0
  21. package/dist/core.js +131 -0
  22. package/dist/core.js.map +1 -0
  23. package/dist/diagram-js.cjs +1147 -0
  24. package/dist/diagram-js.cjs.map +1 -0
  25. package/dist/diagram-js.d.cts +170 -0
  26. package/dist/diagram-js.d.ts +170 -0
  27. package/dist/diagram-js.js +879 -0
  28. package/dist/diagram-js.js.map +1 -0
  29. package/dist/index.cjs +3632 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/dist/index.d.cts +29 -0
  32. package/dist/index.d.ts +29 -0
  33. package/dist/index.js +143 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/protocol-DhYBakWL.d.cts +15 -0
  36. package/dist/protocol-IeW8bf4f.d.ts +15 -0
  37. package/dist/shell.css +1603 -0
  38. package/dist/testing-vscode-stub.cjs +332 -0
  39. package/dist/testing-vscode-stub.cjs.map +1 -0
  40. package/dist/testing-vscode-stub.d.cts +65 -0
  41. package/dist/testing-vscode-stub.d.ts +65 -0
  42. package/dist/testing-vscode-stub.js +154 -0
  43. package/dist/testing-vscode-stub.js.map +1 -0
  44. package/dist/testing.cjs +4663 -0
  45. package/dist/testing.cjs.map +1 -0
  46. package/dist/testing.d.cts +231 -0
  47. package/dist/testing.d.ts +231 -0
  48. package/dist/testing.js +4483 -0
  49. package/dist/testing.js.map +1 -0
  50. package/dist/toasts-Bp0REqdq.d.cts +142 -0
  51. package/dist/toasts-Bp0REqdq.d.ts +142 -0
  52. package/dist/types-CnRrINRs.d.cts +445 -0
  53. package/dist/types-PuDZczzy.d.ts +445 -0
  54. package/dist/vscode-extension.cjs +215 -0
  55. package/dist/vscode-extension.cjs.map +1 -0
  56. package/dist/vscode-extension.d.cts +103 -0
  57. package/dist/vscode-extension.d.ts +103 -0
  58. package/dist/vscode-extension.js +24 -0
  59. package/dist/vscode-extension.js.map +1 -0
  60. package/dist/vscode-webview.cjs +3561 -0
  61. package/dist/vscode-webview.cjs.map +1 -0
  62. package/dist/vscode-webview.d.cts +84 -0
  63. package/dist/vscode-webview.d.ts +84 -0
  64. package/dist/vscode-webview.js +259 -0
  65. package/dist/vscode-webview.js.map +1 -0
  66. package/package.json +145 -4
  67. package/src/core/appearance.ts +104 -0
  68. package/src/core/feedback/appearance.ts +141 -0
  69. package/src/core/feedback/problems.ts +23 -0
  70. package/src/core/feedback/toasts.ts +118 -0
  71. package/src/core/fit.ts +220 -0
  72. package/src/core/index.ts +127 -0
  73. package/src/core/keymap.ts +295 -0
  74. package/src/core/layout.ts +69 -0
  75. package/src/core/messages.ts +159 -0
  76. package/src/core/sizes.ts +85 -0
  77. package/src/core/types.ts +106 -0
  78. package/src/core/web-chrome/bottom-row.ts +37 -0
  79. package/src/core/web-chrome/drop.ts +78 -0
  80. package/src/core/web-chrome/top-row.ts +66 -0
  81. package/src/core/zoom.ts +74 -0
  82. package/src/core.ts +2 -0
  83. package/src/diagram-js/connect.ts +75 -0
  84. package/src/diagram-js/index.ts +16 -0
  85. package/src/diagram-js/module.ts +20 -0
  86. package/src/diagram-js/palette-skin.ts +267 -0
  87. package/src/diagram-js/palette.css +269 -0
  88. package/src/diagram-js/service.ts +243 -0
  89. package/src/diagram-js/types.ts +67 -0
  90. package/src/diagram-js/viewport.ts +583 -0
  91. package/src/diagram-js.ts +2 -0
  92. package/src/dom/buttons.css +124 -0
  93. package/src/dom/element.ts +51 -0
  94. package/src/dom/feedback/appearance.ts +184 -0
  95. package/src/dom/feedback/feedback.css +435 -0
  96. package/src/dom/feedback/problems-popover.ts +287 -0
  97. package/src/dom/feedback/toasts.ts +431 -0
  98. package/src/dom/hint.ts +51 -0
  99. package/src/dom/icons.ts +309 -0
  100. package/src/dom/index.ts +42 -0
  101. package/src/dom/inspector.ts +35 -0
  102. package/src/dom/keys.ts +67 -0
  103. package/src/dom/ledger.ts +87 -0
  104. package/src/dom/legend.ts +85 -0
  105. package/src/dom/menu.ts +404 -0
  106. package/src/dom/mount.ts +731 -0
  107. package/src/dom/problems.ts +73 -0
  108. package/src/dom/shell-dom.css +969 -0
  109. package/src/dom/status.ts +72 -0
  110. package/src/dom/tooltip.ts +237 -0
  111. package/src/dom/types.ts +459 -0
  112. package/src/dom/web/drop-zone.ts +181 -0
  113. package/src/dom/web/empty-state.ts +144 -0
  114. package/src/dom/web/legal.ts +75 -0
  115. package/src/dom/web/menu-groups.ts +69 -0
  116. package/src/dom/web/primary.ts +124 -0
  117. package/src/dom/web/web.css +386 -0
  118. package/src/dom/zoom-panel.ts +154 -0
  119. package/src/index.ts +3 -0
  120. package/src/shell.css +9 -0
  121. package/src/testing/case-tools.ts +164 -0
  122. package/src/testing/cases.ts +1077 -0
  123. package/src/testing/checks.ts +868 -0
  124. package/src/testing/facts.ts +641 -0
  125. package/src/testing/geometry.ts +652 -0
  126. package/src/testing/harness.ts +187 -0
  127. package/src/testing/index.ts +19 -0
  128. package/src/testing/page-tools.ts +457 -0
  129. package/src/testing/page.ts +27 -0
  130. package/src/testing/vscode-stub.ts +237 -0
  131. package/src/testing/web-cases.ts +913 -0
  132. package/src/testing/web-checks.ts +1049 -0
  133. package/src/testing/web-facts.ts +713 -0
  134. package/src/testing-vscode-stub.ts +2 -0
  135. package/src/testing.ts +2 -0
  136. package/src/vscode/extension/html.ts +107 -0
  137. package/src/vscode/extension/index.ts +8 -0
  138. package/src/vscode/extension/reveal.ts +9 -0
  139. package/src/vscode/extension/theme-bridge.ts +85 -0
  140. package/src/vscode/extension/types.ts +36 -0
  141. package/src/vscode/protocol.ts +31 -0
  142. package/src/vscode/webview/appearance.ts +141 -0
  143. package/src/vscode/webview/index.ts +8 -0
  144. package/src/vscode/webview/mount.ts +88 -0
  145. package/src/vscode/webview/state.ts +116 -0
  146. package/src/vscode-extension.ts +2 -0
  147. package/src/vscode-webview.ts +2 -0
@@ -0,0 +1,4663 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name2 in all)
8
+ __defProp(target, name2, { get: all[name2], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/testing.ts
21
+ var testing_exports = {};
22
+ __export(testing_exports, {
23
+ GEOMETRY_TOLERANCE_PX: () => GEOMETRY_TOLERANCE_PX,
24
+ expectedChromeBoxes: () => expectedChromeBoxes,
25
+ measureChrome: () => measureChrome,
26
+ registerShellConformance: () => registerShellConformance,
27
+ shellConformanceCases: () => shellConformanceCases,
28
+ vsCodeStubSearch: () => vsCodeStubSearch
29
+ });
30
+ module.exports = __toCommonJS(testing_exports);
31
+
32
+ // src/core/types.ts
33
+ var CHROME_ISLANDS = [
34
+ "menu",
35
+ "palette",
36
+ "primary",
37
+ "zoom",
38
+ "problems",
39
+ "legend",
40
+ "inspector",
41
+ "status",
42
+ "legal",
43
+ "toasts",
44
+ "empty-state",
45
+ "drop-zone",
46
+ "problems-popover"
47
+ ];
48
+
49
+ // src/core/sizes.ts
50
+ var SHELL = {
51
+ edge: 12,
52
+ gap: 8,
53
+ row: 40,
54
+ target: 32,
55
+ entry: 36,
56
+ free: 64,
57
+ menuMinWidth: 248,
58
+ inspectorWidth: 288,
59
+ fitInsetTop: 88,
60
+ fitInsetTopSecondRow: 140,
61
+ fitInsetSide: 24,
62
+ fitInsetBottom: 64,
63
+ fitInsetBottomWeb: 92,
64
+ narrow: 760,
65
+ veryNarrow: 440,
66
+ low: 820,
67
+ layerIslands: 20,
68
+ layerInspector: 25,
69
+ layerTopRow: 30,
70
+ layerPopups: 40,
71
+ layerDialog: 50,
72
+ layerToast: 60
73
+ };
74
+
75
+ // src/core/web-chrome/bottom-row.ts
76
+ var LEGAL_LINE_HEIGHT = 20;
77
+ function legalLineHeight(measured) {
78
+ if (measured === void 0 || !Number.isFinite(measured) || measured <= 0) return LEGAL_LINE_HEIGHT;
79
+ return Math.max(1, Math.round(measured / LEGAL_LINE_HEIGHT)) * LEGAL_LINE_HEIGHT;
80
+ }
81
+ function zoomRowRaised(input) {
82
+ const { containerWidth, legalRight, zoomWidth } = input;
83
+ if (legalRight === void 0 || zoomWidth === void 0) return false;
84
+ const zoomLeft = containerWidth - SHELL.edge - zoomWidth;
85
+ return legalRight + SHELL.gap > zoomLeft;
86
+ }
87
+
88
+ // src/core/messages.ts
89
+ var defaultMessages = {
90
+ menu: "Menu",
91
+ tools: "Tools",
92
+ canvas: "Diagram",
93
+ zoom: "Zoom",
94
+ zoomOut: "Zoom out",
95
+ zoomIn: "Zoom in",
96
+ zoomToActual: (percent) => `Zoom to ${percent}`,
97
+ zoomToActualNow: (percent, now) => `Zoom to ${percent} (now ${now})`,
98
+ fit: "Fit to view",
99
+ legend: "Legend",
100
+ closeLegend: "Close legend",
101
+ problems: (count, formattedCount) => `${formattedCount} ${count === 1 ? "problem" : "problems"}`,
102
+ problemsLabel: (count, errors, formattedCount, formattedErrors) => `${formattedCount} ${count === 1 ? "problem" : "problems"}, ${errors === 0 ? "no errors" : `${formattedErrors} ${errors === 1 ? "error" : "errors"}`}`,
103
+ showProblems: "Show in Problems view",
104
+ emptyHint: "Pick a tool from the palette to start.",
105
+ insertExample: "Insert example",
106
+ invalidFile: (diagram) => `This file isn't a valid ${diagram} right now. The diagram shows the last valid version and is read-only until you fix the text.`,
107
+ keyNames: { ctrl: "Ctrl", shift: "Shift", plus: "Plus", minus: "Minus" },
108
+ share: "Share",
109
+ legal: "Legal",
110
+ copyright: (year) => `\xA9 ${year} Miragon GmbH`,
111
+ opensInNewTab: "Opens in a new tab",
112
+ newDiagram: "New diagram",
113
+ openFile: "Open\u2026",
114
+ showExample: "Show example",
115
+ importFile: "Import\u2026",
116
+ appearance: "Appearance",
117
+ appearanceSystem: "System",
118
+ appearanceLight: "Light",
119
+ appearanceDark: "Dark",
120
+ problemsTitle: "Problems",
121
+ problemSeverity: (severity) => severity === "error" ? "Error" : "Warning",
122
+ close: "Close",
123
+ dropFile: "Drop file here",
124
+ dropForbidden: (reason) => reason === "multiple" ? "Drop one file at a time" : reason === "dialog" ? "Close the dialog to drop a file" : "This modeler can't open this file type",
125
+ dropHint: "Or drop a file here",
126
+ openFileLink: "Open a file\u2026",
127
+ cannotOpenFile: (name2, accepted) => `Couldn't open \u201C${name2}\u201D. Drop a ${accepted} file.`
128
+ };
129
+
130
+ // src/core/layout.ts
131
+ var PALETTE_CLEARANCE = 16;
132
+ var TOOLTIP_DELAY_MS = 300;
133
+ var INVALID_STATUS_DELAY_MS = 800;
134
+ function decidePaletteRow(input) {
135
+ const { containerWidth, paletteWidth, menuRight, primaryLeft } = input;
136
+ if (containerWidth <= SHELL.narrow) return "second-row";
137
+ const paletteLeft = (containerWidth - paletteWidth) / 2;
138
+ const paletteRight = (containerWidth + paletteWidth) / 2;
139
+ if (paletteLeft < menuRight + PALETTE_CLEARANCE) return "second-row";
140
+ if (primaryLeft !== void 0 && paletteRight > primaryLeft - PALETTE_CLEARANCE) return "second-row";
141
+ return "top";
142
+ }
143
+
144
+ // src/core/web-chrome/top-row.ts
145
+ function decideTopRow(input) {
146
+ const { containerWidth, paletteWidth, menuRight, primary } = input;
147
+ if (!primary) {
148
+ return {
149
+ paletteRow: decidePaletteRow({ containerWidth, paletteWidth, menuRight }),
150
+ primaryLabel: "shown"
151
+ };
152
+ }
153
+ const paletteRowBeside = (primaryWidth) => decidePaletteRow({
154
+ containerWidth,
155
+ paletteWidth,
156
+ menuRight,
157
+ primaryLeft: containerWidth - SHELL.edge - primaryWidth
158
+ });
159
+ if (paletteRowBeside(primary.withLabel) === "top") return { paletteRow: "top", primaryLabel: "shown" };
160
+ if (paletteRowBeside(primary.iconOnly) === "top") return { paletteRow: "top", primaryLabel: "collapsed" };
161
+ const labelledLeft = containerWidth - SHELL.edge - primary.withLabel;
162
+ return {
163
+ paletteRow: "second-row",
164
+ primaryLabel: menuRight + PALETTE_CLEARANCE <= labelledLeft ? "shown" : "collapsed"
165
+ };
166
+ }
167
+
168
+ // src/core/feedback/toasts.ts
169
+ var MAX_TOASTS = 3;
170
+ var TOAST_DURATION_MS = {
171
+ success: 2800,
172
+ info: 2800,
173
+ warning: 5e3,
174
+ danger: 5e3
175
+ };
176
+ var TOAST_MAX_WIDTH = 480;
177
+ function toastStackWidth(containerWidth) {
178
+ return Math.max(0, Math.min(TOAST_MAX_WIDTH, containerWidth - 2 * SHELL.edge));
179
+ }
180
+ function toastStackBottom(input) {
181
+ const stackWidth = toastStackWidth(input.containerWidth);
182
+ const { cardWidth } = input;
183
+ const width = cardWidth !== void 0 && Number.isFinite(cardWidth) ? Math.min(stackWidth, Math.max(0, cardWidth)) : stackWidth;
184
+ const left = (input.containerWidth - width) / 2;
185
+ const right = left + width;
186
+ let highest;
187
+ for (const obstacle of input.obstacles) {
188
+ const distance = Math.max(obstacle.left - right, left - obstacle.right);
189
+ if (distance >= SHELL.gap) continue;
190
+ highest = highest === void 0 ? obstacle.top : Math.max(highest, obstacle.top);
191
+ }
192
+ return highest === void 0 ? SHELL.edge : Math.max(SHELL.edge, highest + SHELL.gap);
193
+ }
194
+
195
+ // src/core/feedback/appearance.ts
196
+ var APPEARANCE_STORAGE_KEY = "miragon.darstellung";
197
+
198
+ // src/testing/page-tools.ts
199
+ var installPageTools = (_) => {
200
+ const scope = window;
201
+ if (scope.__modelerShellConformance) return;
202
+ const modifierKeys = ["Control", "Meta", "Shift", "Alt", "AltGraph", "CapsLock", "Fn"];
203
+ const frame = () => new Promise((resolve) => requestAnimationFrame(() => resolve()));
204
+ const borders = (element) => {
205
+ const style = getComputedStyle(element);
206
+ return {
207
+ left: parseFloat(style.borderLeftWidth) || 0,
208
+ top: parseFloat(style.borderTopWidth) || 0,
209
+ right: parseFloat(style.borderRightWidth) || 0,
210
+ bottom: parseFloat(style.borderBottomWidth) || 0
211
+ };
212
+ };
213
+ const visible = (element) => {
214
+ if (!element.isConnected) return false;
215
+ if (typeof element.checkVisibility === "function") {
216
+ if (!element.checkVisibility({ visibilityProperty: true })) return false;
217
+ } else {
218
+ if (element.closest("[hidden]")) return false;
219
+ for (let node = element; node; node = node.parentElement) {
220
+ if (getComputedStyle(node).display === "none") return false;
221
+ }
222
+ if (getComputedStyle(element).visibility === "hidden") return false;
223
+ }
224
+ const rect = element.getBoundingClientRect();
225
+ return rect.width > 0 && rect.height > 0;
226
+ };
227
+ const describe = (element) => {
228
+ const id = element.id ? `#${element.id}` : "";
229
+ const classes = [...element.classList].slice(0, 3).map((name2) => `.${name2}`).join("");
230
+ const chrome = element.getAttribute("data-chrome");
231
+ return `${element.tagName.toLowerCase()}${id}${classes}${chrome ? `[data-chrome="${chrome}"]` : ""}`;
232
+ };
233
+ const tools = {
234
+ keys: [],
235
+ documentKeys: [],
236
+ errors: [],
237
+ injected: [],
238
+ cleanups: [],
239
+ clicks: 0,
240
+ frame,
241
+ async settle() {
242
+ await frame();
243
+ await frame();
244
+ const running = typeof document.getAnimations === "function" ? document.getAnimations().filter((animation) => {
245
+ const end = animation.effect?.getComputedTiming().endTime;
246
+ return animation.playState === "running" && typeof end === "number" && Number.isFinite(end);
247
+ }) : [];
248
+ if (running.length > 0) {
249
+ await Promise.race([
250
+ Promise.allSettled(running.map((animation) => animation.finished)),
251
+ new Promise((resolve) => setTimeout(resolve, 2e3))
252
+ ]);
253
+ }
254
+ await frame();
255
+ },
256
+ visible,
257
+ container(root) {
258
+ const rect = root.getBoundingClientRect();
259
+ const border = borders(root);
260
+ return {
261
+ width: rect.width - border.left - border.right,
262
+ height: rect.height - border.top - border.bottom
263
+ };
264
+ },
265
+ box(element, root) {
266
+ const rect = element.getBoundingClientRect();
267
+ const origin = root.getBoundingClientRect();
268
+ const border = borders(root);
269
+ return {
270
+ left: rect.left - origin.left - border.left,
271
+ top: rect.top - origin.top - border.top,
272
+ width: rect.width,
273
+ height: rect.height
274
+ };
275
+ },
276
+ describe,
277
+ facts(element) {
278
+ const attributes = {};
279
+ for (const name2 of element.getAttributeNames()) attributes[name2] = element.getAttribute(name2) ?? "";
280
+ return {
281
+ tag: element.tagName.toLowerCase(),
282
+ description: describe(element),
283
+ attributes,
284
+ text: (element.textContent ?? "").replace(/\s+/g, " ").trim(),
285
+ visible: visible(element),
286
+ disabled: Boolean(element.disabled)
287
+ };
288
+ }
289
+ };
290
+ const recorder = (into) => (event) => {
291
+ if (modifierKeys.includes(event.key)) return;
292
+ into.push({
293
+ key: event.key,
294
+ code: event.code,
295
+ ctrlKey: event.ctrlKey,
296
+ metaKey: event.metaKey,
297
+ shiftKey: event.shiftKey,
298
+ altKey: event.altKey,
299
+ defaultPrevented: event.defaultPrevented
300
+ });
301
+ };
302
+ document.addEventListener("keydown", recorder(tools.documentKeys));
303
+ window.addEventListener("keydown", recorder(tools.keys));
304
+ window.addEventListener("error", (event) => {
305
+ tools.errors.push(event.message || String(event.error));
306
+ });
307
+ window.addEventListener("unhandledrejection", (event) => {
308
+ tools.errors.push(`unhandled rejection: ${String(event.reason)}`);
309
+ });
310
+ scope.__modelerShellConformance = tools;
311
+ };
312
+ async function inPage(page, pageFunction, arg) {
313
+ await page.evaluate(installPageTools, null);
314
+ return page.evaluate(pageFunction, arg);
315
+ }
316
+ var settleInPage = (_) => window.__modelerShellConformance.settle();
317
+ function settle(page) {
318
+ return inPage(page, settleInPage, null);
319
+ }
320
+ var CHROME_TARGET_SELECTORS = {
321
+ menu: '[data-chrome="menu"]',
322
+ "palette-entry": '[data-chrome="palette"] [data-action]',
323
+ "palette-tab-stop": '[data-chrome="palette"] [data-action][tabindex="0"]',
324
+ "zoom-out": '[data-chrome="zoom"] [data-zoom="out"]',
325
+ "zoom-actual": '[data-chrome="zoom"] [data-zoom="actual"]',
326
+ "zoom-in": '[data-chrome="zoom"] [data-zoom="in"]',
327
+ "zoom-fit": '[data-chrome="zoom"] [data-zoom="fit"]',
328
+ canvas: ".djs-container > svg",
329
+ primary: '[data-chrome="primary"]',
330
+ pill: '[data-chrome="problems"]'
331
+ };
332
+ var targetProbe = ({ root, selector }) => {
333
+ const tools = window.__modelerShellConformance;
334
+ const rootElement = document.querySelector(root);
335
+ if (!rootElement) return null;
336
+ const element = [...rootElement.querySelectorAll(selector)].find((candidate) => tools.visible(candidate));
337
+ if (!element) return null;
338
+ const rect = element.getBoundingClientRect();
339
+ return {
340
+ client: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
341
+ box: tools.box(element, rootElement),
342
+ label: (element.getAttribute("aria-label") ?? element.textContent ?? "").replace(/\s+/g, " ").trim()
343
+ };
344
+ };
345
+ async function findTarget(page, root, target) {
346
+ return inPage(page, targetProbe, { root, selector: CHROME_TARGET_SELECTORS[target] });
347
+ }
348
+ var focusProbe = ({ root, selector }) => {
349
+ const tools = window.__modelerShellConformance;
350
+ const element = document.querySelector(root)?.querySelector(selector);
351
+ if (!element) return null;
352
+ element.focus();
353
+ return document.activeElement === element ? tools.describe(element) : null;
354
+ };
355
+ async function focusTarget(page, root, target) {
356
+ return inPage(page, focusProbe, { root, selector: CHROME_TARGET_SELECTORS[target] });
357
+ }
358
+ var overlayProbe = ({ root, role }) => {
359
+ const tools = window.__modelerShellConformance;
360
+ const rootElement = document.querySelector(root);
361
+ if (!rootElement) return null;
362
+ const shown = [...rootElement.querySelectorAll(`[role="${role}"]`)].filter(
363
+ (element) => tools.visible(element)
364
+ );
365
+ const first = shown[0];
366
+ if (!first) return null;
367
+ return {
368
+ box: tools.box(first, rootElement),
369
+ text: (first.textContent ?? "").replace(/\s+/g, " ").trim(),
370
+ count: shown.length,
371
+ id: first.id
372
+ };
373
+ };
374
+ async function findOverlay(page, root, role) {
375
+ return inPage(page, overlayProbe, { root, role });
376
+ }
377
+ var afterPressProbe = async ({ root }) => {
378
+ const tools = window.__modelerShellConformance;
379
+ await tools.settle();
380
+ const readout = document.querySelector(root)?.querySelector('[data-chrome="zoom"] [data-zoom="actual"]')?.textContent;
381
+ const digits = (readout ?? "").replace(/[^0-9]/g, "");
382
+ return {
383
+ zoom: digits ? Number(digits) : null,
384
+ keys: tools.keys.splice(0),
385
+ documentKeys: tools.documentKeys.splice(0)
386
+ };
387
+ };
388
+ var clearKeysProbe = (_) => {
389
+ const tools = window.__modelerShellConformance;
390
+ tools.keys.splice(0);
391
+ tools.documentKeys.splice(0);
392
+ };
393
+ var injectProbe = ({ root, kind }) => {
394
+ const tools = window.__modelerShellConformance;
395
+ const parent = document.querySelector(root) ?? tools.armedRoot ?? document.body;
396
+ const host = parent.isConnected ? parent : document.body;
397
+ const element = kind === "input" || kind === "textarea" ? document.createElement(kind) : document.createElement("div");
398
+ element.setAttribute("data-modeler-shell-conformance", kind);
399
+ element.style.cssText = "position:absolute;left:0;top:0;width:40px;height:20px;";
400
+ if (kind === "modal" || kind === "popover") {
401
+ element.setAttribute("role", "dialog");
402
+ element.setAttribute("aria-label", "Conformance stand-in");
403
+ if (kind === "modal") element.setAttribute("aria-modal", "true");
404
+ }
405
+ if (kind === "contenteditable") element.setAttribute("contenteditable", "true");
406
+ else if (kind !== "input" && kind !== "textarea") element.tabIndex = -1;
407
+ host.append(element);
408
+ tools.injected.push(element);
409
+ element.focus();
410
+ return document.activeElement === element;
411
+ };
412
+ var removeInjectedProbe = (_) => {
413
+ const tools = window.__modelerShellConformance;
414
+ for (const element of tools.injected.splice(0)) element.remove();
415
+ for (const cleanup of tools.cleanups.splice(0)) cleanup();
416
+ };
417
+ var canvasHandlesProbe = ({ root, key }) => {
418
+ const tools = window.__modelerShellConformance;
419
+ const svg = document.querySelector(root)?.querySelector(".djs-container > svg");
420
+ if (!svg) return false;
421
+ const listener = (event) => {
422
+ if (event.key === key) event.preventDefault();
423
+ };
424
+ svg.addEventListener("keydown", listener);
425
+ tools.cleanups.push(() => svg.removeEventListener("keydown", listener));
426
+ return true;
427
+ };
428
+ var armCleanupProbe = ({ root }) => {
429
+ const tools = window.__modelerShellConformance;
430
+ const rootElement = document.querySelector(root);
431
+ if (rootElement) tools.armedRoot = rootElement;
432
+ tools.errors.splice(0);
433
+ tools.keys.splice(0);
434
+ tools.documentKeys.splice(0);
435
+ return Boolean(rootElement);
436
+ };
437
+ var leftoverProbe = (_) => {
438
+ const tools = window.__modelerShellConformance;
439
+ const ours = (element) => element.hasAttribute("data-modeler-shell-conformance");
440
+ const shellClass = (name2) => name2.startsWith("cd-shell");
441
+ const root = tools.armedRoot;
442
+ return {
443
+ classedElements: [...document.querySelectorAll("[class]")].filter((element) => element !== document.documentElement && !ours(element)).filter((element) => [...element.classList].some(shellClass)).map((element) => tools.describe(element)),
444
+ documentClasses: [...document.documentElement.classList].filter(shellClass),
445
+ chromeElements: [...document.querySelectorAll("[data-chrome]")].map((element) => tools.describe(element)),
446
+ rootClasses: root ? [...root.classList].filter(shellClass) : [],
447
+ rootAttributes: root ? root.getAttributeNames() : [],
448
+ errors: [...tools.errors]
449
+ };
450
+ };
451
+ var hostPageModeProbe = ({ theme, kontrast }) => {
452
+ const html = document.documentElement;
453
+ html.setAttribute("data-cd-theme", theme);
454
+ if (kontrast) html.setAttribute("data-cd-kontrast", kontrast);
455
+ else html.removeAttribute("data-cd-kontrast");
456
+ };
457
+
458
+ // src/testing/geometry.ts
459
+ var GEOMETRY_TOLERANCE_PX = 1;
460
+ var DEFAULT_ROOT = ".cd-shell";
461
+ var ZOOM_PANEL_WIDTH = 173;
462
+ var LEGAL_LINE = LEGAL_LINE_HEIGHT;
463
+ var EMPTY_STATE_MAX_WIDTH = 400;
464
+ var POPOVER_MAX_WIDTH = 320;
465
+ var POPOVER_MAX_HEIGHT = 320;
466
+ var LEGEND_MIN_WIDTH = 184;
467
+ var STATUS_MAX_WIDTH = 560;
468
+ var DROPDOWN_MAX_WIDTH = 320;
469
+ var TOOLTIP_MAX_WIDTH = 280;
470
+ var SHEET_MAX_HEIGHT_SHARE = 0.45;
471
+ var measureProbe = ({ root, islands }) => {
472
+ const tools = window.__modelerShellConformance;
473
+ const rootElement = document.querySelector(root);
474
+ if (!rootElement) return { error: `no element matches the shell root selector "${root}"` };
475
+ const boxes = {};
476
+ const duplicates = [];
477
+ for (const element of rootElement.querySelectorAll("[data-chrome]")) {
478
+ const name2 = element.getAttribute("data-chrome") ?? "";
479
+ if (!islands.includes(name2) || !tools.visible(element)) continue;
480
+ if (boxes[name2]) {
481
+ duplicates.push(name2);
482
+ continue;
483
+ }
484
+ boxes[name2] = tools.box(element, rootElement);
485
+ }
486
+ const cardWidths = [...rootElement.querySelectorAll('[data-chrome="toasts"] > [data-status]')].filter((card) => tools.visible(card)).map((card) => card.getBoundingClientRect().width);
487
+ return {
488
+ ...cardWidths.length > 0 ? { toastCardWidth: Math.max(...cardWidths) } : {},
489
+ container: tools.container(rootElement),
490
+ paletteRow: rootElement.getAttribute("data-palette") === "second-row" ? "second-row" : "top",
491
+ islands: boxes,
492
+ open: {
493
+ legend: rootElement.getAttribute("data-legend") === "open",
494
+ inspector: rootElement.getAttribute("data-inspector") === "open",
495
+ problems: rootElement.getAttribute("data-problems") === "shown",
496
+ emptyState: rootElement.getAttribute("data-empty-state") === "shown",
497
+ primaryCollapsed: rootElement.getAttribute("data-primary") === "collapsed",
498
+ zoomRaised: rootElement.getAttribute("data-zoom-row") === "raised"
499
+ },
500
+ duplicates
501
+ };
502
+ };
503
+ async function measureChrome(page, root = DEFAULT_ROOT) {
504
+ const result = await inPage(page, measureProbe, { root, islands: CHROME_ISLANDS });
505
+ if ("error" in result) throw new Error(`@miragon/modeler-shell conformance: ${result.error}`);
506
+ if (result.duplicates.length > 0) {
507
+ const names = [...new Set(result.duplicates)].map((name2) => `[data-chrome="${name2}"]`).join(", ");
508
+ throw new Error(`@miragon/modeler-shell conformance: more than one visible ${names}`);
509
+ }
510
+ return {
511
+ container: result.container,
512
+ paletteRow: result.paletteRow,
513
+ islands: result.islands,
514
+ open: result.open,
515
+ ...result.toastCardWidth !== void 0 ? { toastCardWidth: result.toastCardWidth } : {}
516
+ };
517
+ }
518
+ function isNarrow(container) {
519
+ return container.width <= SHELL.narrow;
520
+ }
521
+ function isVeryNarrow(container) {
522
+ return container.width <= SHELL.veryNarrow;
523
+ }
524
+ function sheetOpen(measurement) {
525
+ return measurement.open.inspector && isNarrow(measurement.container);
526
+ }
527
+ function bottomObstacle(box, container) {
528
+ return { left: box.left, right: box.left + box.width, top: container.height - box.top };
529
+ }
530
+ var WEB_ONLY = {
531
+ primary: "the top right stays empty in IDE and embedded hosts (guide \xA75.7)",
532
+ legal: "the legal line belongs to the web app; an IDE or an embedding host carries its own (guide \xA75.2)",
533
+ "empty-state": "IDE and embedded hosts have no start screen; they show the empty-file hint (guide \xA713.5)",
534
+ "drop-zone": "IDE and embedded hosts have no drop zone; their files belong to the host (guide \xA75.2)"
535
+ };
536
+ var WORK_CHROME = [
537
+ "menu",
538
+ "primary",
539
+ "palette",
540
+ "zoom",
541
+ "problems",
542
+ "legend",
543
+ "status",
544
+ "inspector"
545
+ ];
546
+ function chromeExpectations(measurement, host) {
547
+ const { container, islands, open, paletteRow } = measurement;
548
+ const { edge, gap, row, free, inspectorWidth } = SHELL;
549
+ const web = host === "web";
550
+ const startScreen2 = web && open.emptyState;
551
+ const raised = web && open.zoomRaised;
552
+ const secondRow = paletteRow === "second-row";
553
+ const sheet = sheetOpen(measurement);
554
+ const sideInspector = open.inspector && !isNarrow(container);
555
+ const legalHeight = legalLineHeight(web ? islands.legal?.height : void 0);
556
+ const boxes = {};
557
+ const limits = {};
558
+ const hidden = {};
559
+ const required = startScreen2 ? ["empty-state"] : ["menu", "palette"];
560
+ if (web) required.push("legal");
561
+ boxes.menu = { left: edge, top: edge, width: row, height: row };
562
+ boxes.palette = { top: secondRow ? free : edge, height: row, centerX: container.width / 2 };
563
+ const menu = islands.menu;
564
+ const primary = web ? islands.primary : void 0;
565
+ if (secondRow) {
566
+ limits.palette = { maxWidth: container.width - 2 * edge };
567
+ } else {
568
+ limits.palette = {};
569
+ if (menu) limits.palette.minLeft = menu.left + menu.width + PALETTE_CLEARANCE;
570
+ if (primary) limits.palette.minRight = container.width - primary.left + PALETTE_CLEARANCE;
571
+ }
572
+ if (sheet) {
573
+ const reason = "hidden while the inspector is a bottom sheet (guide \xA75.11)";
574
+ hidden.zoom = reason;
575
+ hidden.problems = reason;
576
+ hidden.legend = reason;
577
+ } else {
578
+ if (!startScreen2) required.push("zoom");
579
+ const legalStack = edge + legalHeight + gap;
580
+ boxes.zoom = { right: edge, bottom: raised ? legalStack : edge, width: ZOOM_PANEL_WIDTH, height: row };
581
+ const stackBase = web ? legalStack : edge;
582
+ if (islands.problems) boxes.problems = { left: edge, bottom: stackBase, height: row };
583
+ if (islands.legend) {
584
+ const besidePill = open.problems ? stackBase + row + gap : stackBase;
585
+ const aboveBottomRow = (raised ? legalStack : edge) + row + gap;
586
+ const bottom = isVeryNarrow(container) ? Math.max(besidePill, aboveBottomRow) : besidePill;
587
+ boxes.legend = { left: edge, bottom };
588
+ limits.legend = { minWidth: LEGEND_MIN_WIDTH };
589
+ }
590
+ }
591
+ if (islands.inspector) {
592
+ if (sheet) {
593
+ boxes.inspector = { left: edge, right: edge, bottom: edge };
594
+ limits.inspector = { maxHeight: container.height * SHEET_MAX_HEIGHT_SHARE };
595
+ } else {
596
+ boxes.inspector = { top: free, right: edge, width: inspectorWidth };
597
+ limits.inspector = { minBottom: free };
598
+ }
599
+ }
600
+ if (islands.status) {
601
+ const strip = sideInspector ? container.width - edge - inspectorWidth : container.width;
602
+ boxes.status = { top: secondRow ? free + row + edge : free, centerX: strip / 2 };
603
+ limits.status = {
604
+ minLeft: edge,
605
+ maxWidth: Math.min(STATUS_MAX_WIDTH, container.width - 2 * edge)
606
+ };
607
+ }
608
+ if (web) {
609
+ if (primary) {
610
+ boxes.primary = { top: edge, right: edge, height: row };
611
+ if (open.primaryCollapsed) boxes.primary.width = row;
612
+ }
613
+ boxes.legal = { left: edge, bottom: edge, height: legalHeight };
614
+ limits.legal = { maxWidth: container.width - 2 * edge };
615
+ if (islands["empty-state"]) {
616
+ boxes["empty-state"] = {
617
+ centerX: container.width / 2,
618
+ centerY: container.height / 2,
619
+ width: Math.min(EMPTY_STATE_MAX_WIDTH, container.width - 2 * edge)
620
+ };
621
+ }
622
+ if (islands["drop-zone"]) boxes["drop-zone"] = { left: edge, top: edge, right: edge, bottom: edge };
623
+ } else {
624
+ Object.assign(hidden, WEB_ONLY);
625
+ }
626
+ if (host === "ide") {
627
+ hidden.toasts = "the IDE host shows no toasts; the extension shows the host's notification (guide \xA713.5)";
628
+ hidden["problems-popover"] = "the IDE host opens its Problems view, never a list of its own (guide \xA75.10)";
629
+ } else {
630
+ if (islands.toasts) {
631
+ const obstacles = ["zoom", "legal", "problems"].flatMap((name2) => {
632
+ const box = islands[name2];
633
+ return box ? [bottomObstacle(box, container)] : [];
634
+ });
635
+ boxes.toasts = {
636
+ centerX: container.width / 2,
637
+ width: toastStackWidth(container.width),
638
+ bottom: toastStackBottom({
639
+ containerWidth: container.width,
640
+ obstacles,
641
+ cardWidth: measurement.toastCardWidth
642
+ })
643
+ };
644
+ }
645
+ const pill = islands.problems;
646
+ if (pill) {
647
+ boxes["problems-popover"] = { left: edge, bottom: container.height - pill.top + gap };
648
+ limits["problems-popover"] = {
649
+ maxWidth: Math.min(POPOVER_MAX_WIDTH, container.width - 2 * edge),
650
+ maxHeight: POPOVER_MAX_HEIGHT
651
+ };
652
+ } else {
653
+ hidden["problems-popover"] = "the problems list opens only above a visible pill (guide \xA75.10)";
654
+ }
655
+ }
656
+ return { boxes, limits, required, hidden };
657
+ }
658
+ function expectedChromeBoxes(measurement, host) {
659
+ return chromeExpectations(measurement, host).boxes;
660
+ }
661
+ var round = (value) => Math.round(value * 10) / 10;
662
+ function sides(box, container) {
663
+ return {
664
+ left: box.left,
665
+ top: box.top,
666
+ right: container.width - box.left - box.width,
667
+ bottom: container.height - box.top - box.height,
668
+ width: box.width,
669
+ height: box.height,
670
+ centerX: box.left + box.width / 2,
671
+ centerY: box.top + box.height / 2
672
+ };
673
+ }
674
+ function boxMismatches(name2, box, expected, container, tolerance = GEOMETRY_TOLERANCE_PX) {
675
+ const actual = sides(box, container);
676
+ return Object.keys(expected).flatMap((field) => {
677
+ const want = expected[field];
678
+ if (want === void 0) return [];
679
+ const have = actual[field];
680
+ return Math.abs(have - want) > tolerance + 1e-6 ? [`${name2}: ${field} is ${round(have)}px, expected ${round(want)}px (\xB1${tolerance}px)`] : [];
681
+ });
682
+ }
683
+ function limitViolations(name2, box, limits, container, tolerance = GEOMETRY_TOLERANCE_PX) {
684
+ const actual = sides(box, container);
685
+ const problems = [];
686
+ const atLeast = (field, have, limit) => {
687
+ if (limit !== void 0 && have < limit - tolerance - 1e-6) {
688
+ problems.push(`${name2}: ${field} is ${round(have)}px, at least ${round(limit)}px expected`);
689
+ }
690
+ };
691
+ const atMost = (field, have, limit) => {
692
+ if (limit !== void 0 && have > limit + tolerance + 1e-6) {
693
+ problems.push(`${name2}: ${field} is ${round(have)}px, at most ${round(limit)}px expected`);
694
+ }
695
+ };
696
+ atLeast("width", actual.width, limits.minWidth);
697
+ atMost("width", actual.width, limits.maxWidth);
698
+ atMost("height", actual.height, limits.maxHeight);
699
+ atLeast("left", actual.left, limits.minLeft);
700
+ atLeast("right", actual.right, limits.minRight);
701
+ atLeast("bottom", actual.bottom, limits.minBottom);
702
+ return problems;
703
+ }
704
+ function geometryProblems(measurement, host, tolerance = GEOMETRY_TOLERANCE_PX) {
705
+ const { container, islands, open, paletteRow } = measurement;
706
+ const expectations = chromeExpectations(measurement, host);
707
+ const problems = [];
708
+ const sheet = sheetOpen(measurement);
709
+ for (const name2 of expectations.required) {
710
+ if (!islands[name2]) problems.push(`${name2}: [data-chrome="${name2}"] is missing or not visible`);
711
+ }
712
+ for (const name2 of CHROME_ISLANDS) {
713
+ const box = islands[name2];
714
+ if (!box) continue;
715
+ const expected = expectations.boxes[name2];
716
+ if (!expected) {
717
+ const reason = expectations.hidden[name2] ?? "no anchor of the guide applies";
718
+ problems.push(`${name2}: visible, but ${reason}`);
719
+ continue;
720
+ }
721
+ problems.push(...boxMismatches(name2, box, expected, container, tolerance));
722
+ const limits = expectations.limits[name2];
723
+ if (limits) problems.push(...limitViolations(name2, box, limits, container, tolerance));
724
+ }
725
+ if (isNarrow(container) && paletteRow !== "second-row") {
726
+ problems.push(
727
+ `palette: data-palette="${paletteRow}" in a ${round(container.width)}px wide container; up to ${SHELL.narrow}px it is "second-row" (guide \xA75.15)`
728
+ );
729
+ }
730
+ const palette = islands.palette;
731
+ const menu = islands.menu;
732
+ const primary = host === "web" ? islands.primary : void 0;
733
+ if (paletteRow === "second-row" && !isNarrow(container) && palette && menu) {
734
+ const clipped = palette.width >= container.width - 2 * SHELL.edge - tolerance;
735
+ const topRowLeft = (container.width - palette.width) / 2;
736
+ const toMenu = topRowLeft - menu.left - menu.width;
737
+ const toPrimary = primary ? container.width - SHELL.edge - SHELL.row - (topRowLeft + palette.width) : Number.POSITIVE_INFINITY;
738
+ const fits = toMenu >= PALETTE_CLEARANCE + tolerance && toPrimary >= PALETTE_CLEARANCE + tolerance;
739
+ if (!clipped && fits) {
740
+ const kept = primary ? `${round(toMenu)}px to the menu trigger and ${round(toPrimary)}px to the primary action's icon` : `${round(toMenu)}px to the menu trigger`;
741
+ problems.push(
742
+ `palette: in the second row although the top row keeps ${kept} (at least ${PALETTE_CLEARANCE}px suffice, guide \xA75.15)`
743
+ );
744
+ }
745
+ }
746
+ const state = (island, flag, attribute2, hiddenBySheet) => {
747
+ const shown = Boolean(islands[island]);
748
+ if (flag && !shown && !hiddenBySheet) {
749
+ problems.push(`${island}: the root carries ${attribute2}, but the island is not visible`);
750
+ }
751
+ if (!flag && shown) problems.push(`${island}: visible, but the root lacks ${attribute2}`);
752
+ };
753
+ state("problems", open.problems, 'data-problems="shown"', sheet || open.emptyState);
754
+ state("legend", open.legend, 'data-legend="open"', sheet || open.emptyState);
755
+ state("inspector", open.inspector, 'data-inspector="open"', open.emptyState);
756
+ state("empty-state", open.emptyState, 'data-empty-state="shown"', false);
757
+ problems.push(...webRowProblems(measurement, host, tolerance));
758
+ return problems;
759
+ }
760
+ function webRowProblems(measurement, host, tolerance = GEOMETRY_TOLERANCE_PX) {
761
+ const { container, islands, open } = measurement;
762
+ const problems = [];
763
+ if (host !== "web") {
764
+ if (open.primaryCollapsed)
765
+ problems.push('primary: the root carries data-primary="collapsed" outside the web app');
766
+ if (open.zoomRaised) problems.push('zoom: the root carries data-zoom-row="raised" outside the web app');
767
+ return problems;
768
+ }
769
+ if (open.primaryCollapsed && !islands.primary && !open.emptyState) {
770
+ problems.push('primary: the root carries data-primary="collapsed", but no primary action is visible');
771
+ }
772
+ const legal = islands.legal;
773
+ const zoom = islands.zoom;
774
+ if (legal && zoom) {
775
+ const legalRight = legal.left + legal.width;
776
+ const gap = container.width - SHELL.edge - zoom.width - legalRight;
777
+ const raised = zoomRowRaised({ containerWidth: container.width, legalRight, zoomWidth: zoom.width });
778
+ const borderline = Math.abs(gap - SHELL.gap) <= tolerance;
779
+ if (raised && !open.zoomRaised && !borderline) {
780
+ problems.push(
781
+ `zoom: ${round(gap)}px between the legal line and the zoom panel, but the root lacks data-zoom-row="raised" (below ${SHELL.gap}px the zoom panel stands above the legal line)`
782
+ );
783
+ }
784
+ if (!raised && open.zoomRaised && !borderline) {
785
+ problems.push(
786
+ `zoom: the root carries data-zoom-row="raised" although the zoom panel keeps ${round(gap)}px to the legal line (at least ${SHELL.gap}px)`
787
+ );
788
+ }
789
+ }
790
+ return problems;
791
+ }
792
+ function expectedFitInsets(measurement, host) {
793
+ const { container, islands, open, paletteRow } = measurement;
794
+ const insets = {
795
+ top: paletteRow === "second-row" ? SHELL.fitInsetTopSecondRow : SHELL.fitInsetTop,
796
+ right: SHELL.fitInsetSide,
797
+ bottom: host === "web" && (open.problems || open.zoomRaised) ? SHELL.fitInsetBottomWeb + legalLineHeight(islands.legal?.height) - LEGAL_LINE : SHELL.fitInsetBottom,
798
+ left: SHELL.fitInsetSide
799
+ };
800
+ if (sheetOpen(measurement)) {
801
+ const sheet = islands.inspector;
802
+ insets.bottom = sheet ? container.height - sheet.top + SHELL.edge : SHELL.fitInsetBottom;
803
+ return insets;
804
+ }
805
+ if (open.inspector) insets.right = SHELL.edge + SHELL.inspectorWidth + SHELL.fitInsetSide;
806
+ const legend = islands.legend;
807
+ if (open.legend && legend && legend.width > 0 && container.width > SHELL.veryNarrow) {
808
+ insets.left = SHELL.edge + legend.width + SHELL.fitInsetSide;
809
+ }
810
+ return insets;
811
+ }
812
+ function freeRectangle(measurement, host) {
813
+ const { container } = measurement;
814
+ const insets = expectedFitInsets(measurement, host);
815
+ const horizontal = container.width - insets.left - insets.right > 0;
816
+ const vertical = container.height - insets.top - insets.bottom > 0;
817
+ return {
818
+ left: horizontal ? insets.left : 0,
819
+ top: vertical ? insets.top : 0,
820
+ right: horizontal ? container.width - insets.right : container.width,
821
+ bottom: vertical ? container.height - insets.bottom : container.height
822
+ };
823
+ }
824
+ function dropdownProblems(box, container, tolerance = GEOMETRY_TOLERANCE_PX) {
825
+ const { edge, gap, row, menuMinWidth } = SHELL;
826
+ return [
827
+ ...boxMismatches("dropdown", box, { top: edge + row + gap, left: edge }, container, tolerance),
828
+ ...limitViolations(
829
+ "dropdown",
830
+ box,
831
+ {
832
+ minWidth: menuMinWidth,
833
+ maxWidth: Math.min(DROPDOWN_MAX_WIDTH, container.width - 2 * edge),
834
+ minBottom: edge
835
+ },
836
+ container,
837
+ tolerance
838
+ )
839
+ ];
840
+ }
841
+ function tooltipProblems(tooltip, anchor, row, container, tolerance = GEOMETRY_TOLERANCE_PX, bottomRow = SHELL.edge) {
842
+ const { edge, gap, row: rowHeight, free } = SHELL;
843
+ const clamp = (centred) => Math.max(edge, Math.min(centred, container.width - edge - tooltip.width));
844
+ const expectedAt = (left) => row === "bottom" ? { left, bottom: bottomRow + rowHeight + gap } : { left, top: (row === "top" ? edge : free) + rowHeight + gap };
845
+ let mismatches = boxMismatches(
846
+ "tooltip",
847
+ tooltip,
848
+ expectedAt(clamp(anchor.left + anchor.width / 2 - tooltip.width / 2)),
849
+ container,
850
+ tolerance
851
+ );
852
+ if (row === "second-row" && mismatches.length > 0) {
853
+ const inContainer = boxMismatches(
854
+ "tooltip",
855
+ tooltip,
856
+ expectedAt(clamp(container.width / 2 - tooltip.width / 2)),
857
+ container,
858
+ tolerance
859
+ );
860
+ if (inContainer.length === 0) mismatches = inContainer;
861
+ }
862
+ return [
863
+ ...mismatches,
864
+ ...limitViolations(
865
+ "tooltip",
866
+ tooltip,
867
+ { minLeft: edge, minRight: edge, maxWidth: Math.min(TOOLTIP_MAX_WIDTH, container.width - 2 * edge) },
868
+ container,
869
+ tolerance
870
+ )
871
+ ];
872
+ }
873
+
874
+ // src/testing/facts.ts
875
+ var DOM_ORDER = [
876
+ "menu",
877
+ "primary",
878
+ "palette",
879
+ "canvas",
880
+ "status",
881
+ "hint",
882
+ "empty-state",
883
+ "inspector",
884
+ "problems",
885
+ "legend",
886
+ "legal",
887
+ "zoom",
888
+ "toasts"
889
+ ];
890
+ var ariaProbe = ({ root }) => {
891
+ const tools = window.__modelerShellConformance;
892
+ const rootElement = document.querySelector(root);
893
+ if (!rootElement) return null;
894
+ const find = (selector) => rootElement.querySelector(selector);
895
+ const factsOf = (element) => element ? tools.facts(element) : null;
896
+ const navigatorLike = navigator;
897
+ const trigger = find('[data-chrome="menu"]');
898
+ const controlsId = trigger?.getAttribute("aria-controls");
899
+ const palette = find('[data-chrome="palette"]');
900
+ const toolbar = palette?.querySelector('[role="toolbar"]') ?? palette?.querySelector(".djs-palette-entries");
901
+ const zoom = find('[data-chrome="zoom"]');
902
+ const zoomButtons = {};
903
+ for (const name2 of ["out", "actual", "in", "fit"]) {
904
+ const button = zoom?.querySelector(`[data-zoom="${name2}"]`);
905
+ if (button) zoomButtons[name2] = tools.facts(button);
906
+ }
907
+ const legend = find('[data-chrome="legend"]');
908
+ const labelledBy = legend?.getAttribute("aria-labelledby");
909
+ const legal = find('[data-chrome="legal"]');
910
+ const pillControls = find('[data-chrome="problems"]')?.getAttribute("aria-controls");
911
+ const parts = [
912
+ ["menu", trigger],
913
+ ["primary", find('[data-chrome="primary"]')],
914
+ ["palette", palette],
915
+ ["canvas", find(".djs-container > svg")],
916
+ ["status", find('[data-chrome="status"]')],
917
+ ["hint", find(".cd-shell-hint")],
918
+ ["inspector", find('[data-chrome="inspector"]')],
919
+ ["problems", find('[data-chrome="problems"]')],
920
+ ["empty-state", find('[data-chrome="empty-state"]')],
921
+ ["legend", legend],
922
+ ["legal", find('[data-chrome="legal"]')],
923
+ ["zoom", zoom],
924
+ ["toasts", find('[data-chrome="toasts"]')]
925
+ ];
926
+ const present = parts.filter((part) => part[1] !== null);
927
+ present.sort(([, a], [, b]) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
928
+ const titledSelector = [
929
+ '[data-chrome="menu"][title]',
930
+ '[data-chrome="primary"][title]',
931
+ '[data-chrome="palette"] [data-action][title]',
932
+ '[data-chrome="zoom"] [title]',
933
+ '[data-chrome="problems"][title]'
934
+ ].join(", ");
935
+ return {
936
+ platform: /mac/i.test(navigatorLike.userAgentData?.platform ?? navigatorLike.platform ?? "") ? "mac" : "other",
937
+ documentLang: document.documentElement.lang,
938
+ rootLang: rootElement.getAttribute("lang"),
939
+ trigger: factsOf(trigger),
940
+ controlled: factsOf(controlsId ? document.getElementById(controlsId) : null),
941
+ palette: factsOf(palette),
942
+ toolbar: factsOf(toolbar),
943
+ canvas: factsOf(find(".djs-container > svg")),
944
+ entries: [...palette?.querySelectorAll("[data-action]") ?? []].map((entry) => ({
945
+ ...tools.facts(entry),
946
+ group: entry.closest("[data-group]")?.getAttribute("data-group") ?? null
947
+ })),
948
+ zoom: factsOf(zoom),
949
+ zoomButtons,
950
+ order: present.map(([name2]) => name2),
951
+ positiveTabindex: [...rootElement.querySelectorAll("[tabindex]")].filter((element) => Number(element.getAttribute("tabindex")) > 0).map((element) => `${tools.describe(element)} (tabindex="${element.getAttribute("tabindex")}")`),
952
+ titled: [...rootElement.querySelectorAll(titledSelector)].map((element) => tools.describe(element)),
953
+ problems: factsOf(find('[data-chrome="problems"]')),
954
+ status: factsOf(find('[data-chrome="status"]')),
955
+ legend: factsOf(legend),
956
+ legendTitle: labelledBy ? labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim() ?? "").join(" ").trim() : null,
957
+ legendClose: factsOf(legend?.querySelector("button")),
958
+ inspector: factsOf(find('[data-chrome="inspector"]')),
959
+ primary: factsOf(find('[data-chrome="primary"]')),
960
+ primaryCollapsed: rootElement.getAttribute("data-primary") === "collapsed",
961
+ legal: factsOf(legal),
962
+ legalLinks: [...legal?.querySelectorAll("a") ?? []].map((link) => ({
963
+ text: (link.textContent ?? "").replace(/\s+/g, " ").trim(),
964
+ href: link.getAttribute("href") ?? "",
965
+ lang: link.getAttribute("lang"),
966
+ target: link.getAttribute("target"),
967
+ rel: link.getAttribute("rel")
968
+ })),
969
+ problemsControlled: factsOf(pillControls ? document.getElementById(pillControls) : null)
970
+ };
971
+ };
972
+ var menuProbe = ({ root }) => {
973
+ const tools = window.__modelerShellConformance;
974
+ const rootElement = document.querySelector(root);
975
+ if (!rootElement) return null;
976
+ const trigger = rootElement.querySelector('[data-chrome="menu"]');
977
+ const menu = [...rootElement.querySelectorAll('[role="menu"]')].find((element) => tools.visible(element)) ?? null;
978
+ const items = menu ? [
979
+ ...menu.querySelectorAll('[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]')
980
+ ].filter((item) => tools.visible(item)) : [];
981
+ return {
982
+ expanded: trigger?.getAttribute("aria-expanded") ?? null,
983
+ controls: trigger?.getAttribute("aria-controls") ?? null,
984
+ menu: menu ? tools.facts(menu) : null,
985
+ items: items.map((item) => {
986
+ const group = item.closest('[role="group"]');
987
+ return {
988
+ role: item.getAttribute("role") ?? "",
989
+ name: (item.getAttribute("aria-label") ?? item.textContent ?? "").replace(/\s+/g, " ").trim(),
990
+ checked: item.getAttribute("aria-checked"),
991
+ disabled: item.getAttribute("aria-disabled"),
992
+ haspopup: item.getAttribute("aria-haspopup"),
993
+ group: group && menu?.contains(group) ? group.getAttribute("aria-label") ?? "" : null
994
+ };
995
+ }),
996
+ active: items.findIndex((item) => item === document.activeElement),
997
+ activeIsTrigger: Boolean(trigger) && document.activeElement === trigger
998
+ };
999
+ };
1000
+ var paletteFocusProbe = ({ root }) => {
1001
+ const palette = document.querySelector(root)?.querySelector('[data-chrome="palette"]');
1002
+ if (!palette) return null;
1003
+ const entries = [...palette.querySelectorAll("[data-action]")];
1004
+ const active = document.activeElement;
1005
+ return {
1006
+ entries: entries.map((entry) => entry.getAttribute("data-action") ?? ""),
1007
+ tabStops: entries.filter((entry) => entry.getAttribute("tabindex") === "0").map((entry) => entry.getAttribute("data-action") ?? ""),
1008
+ active: active && entries.includes(active) ? active.getAttribute("data-action") : null
1009
+ };
1010
+ };
1011
+ var zoomButtonsProbe = ({ root }) => {
1012
+ const tools = window.__modelerShellConformance;
1013
+ const zoom = document.querySelector(root)?.querySelector('[data-chrome="zoom"]');
1014
+ const buttons = {};
1015
+ for (const name2 of ["out", "actual", "in", "fit"]) {
1016
+ const button = zoom?.querySelector(`[data-zoom="${name2}"]`);
1017
+ if (button) buttons[name2] = tools.facts(button);
1018
+ }
1019
+ return buttons;
1020
+ };
1021
+ var MODE_ROLES = ["--cd-ui-grund", "--cd-ui-text", "--cd-ui-flaeche-hoch", "--cd-ui-linie"];
1022
+ var appearanceProbe = ({
1023
+ root,
1024
+ roles
1025
+ }) => {
1026
+ const tools = window.__modelerShellConformance;
1027
+ const rootElement = document.querySelector(root);
1028
+ if (!rootElement) return null;
1029
+ const mode = (name2, element) => {
1030
+ const style = getComputedStyle(element);
1031
+ const resolved = {};
1032
+ for (const role of roles) resolved[role] = style.getPropertyValue(role).trim();
1033
+ return { name: name2, description: tools.describe(element), colorScheme: style.colorScheme, roles: resolved };
1034
+ };
1035
+ const parts = [mode("root", rootElement)];
1036
+ const canvas = rootElement.querySelector(".djs-container");
1037
+ if (canvas) parts.push(mode("canvas", canvas));
1038
+ for (const island of rootElement.querySelectorAll("[data-chrome]")) {
1039
+ if (tools.visible(island)) parts.push(mode(island.getAttribute("data-chrome") ?? "island", island));
1040
+ }
1041
+ for (const [name2, role] of [
1042
+ ["dropdown", "menu"],
1043
+ ["tooltip", "tooltip"]
1044
+ ]) {
1045
+ const popup = [...rootElement.querySelectorAll(`[role="${role}"]`)].find(
1046
+ (element) => tools.visible(element)
1047
+ );
1048
+ if (popup) parts.push(mode(name2, popup));
1049
+ }
1050
+ return {
1051
+ documentClasses: [...document.documentElement.classList],
1052
+ themed: [...document.querySelectorAll("[data-cd-theme], [data-cd-kontrast]")].map((element) => ({
1053
+ description: tools.describe(element),
1054
+ isDocument: element === document.documentElement,
1055
+ relation: element === rootElement ? "self" : element.contains(rootElement) ? "ancestor" : rootElement.contains(element) ? "descendant" : "elsewhere",
1056
+ theme: element.getAttribute("data-cd-theme"),
1057
+ kontrast: element.getAttribute("data-cd-kontrast")
1058
+ })),
1059
+ parts
1060
+ };
1061
+ };
1062
+ var activeStopProbe = ({ root }) => {
1063
+ const tools = window.__modelerShellConformance;
1064
+ const active = document.activeElement;
1065
+ const rootElement = document.querySelector(root);
1066
+ if (!active || active === document.body || !rootElement) return { description: "<body>", kind: "other" };
1067
+ const description = tools.describe(active);
1068
+ if (!rootElement.contains(active))
1069
+ return { description: `${description} outside the shell root`, kind: "other" };
1070
+ if (active.matches('[data-chrome="palette"] [data-action]')) return { description, kind: "palette" };
1071
+ if (active.matches(".djs-container > svg")) return { description, kind: "canvas" };
1072
+ if (active.closest('[data-chrome="primary"]')) return { description, kind: "primary" };
1073
+ const tabindex = active.getAttribute("tabindex");
1074
+ return {
1075
+ description: tabindex === null ? description : `${description}[tabindex="${tabindex}"]`,
1076
+ kind: "other"
1077
+ };
1078
+ };
1079
+ var paletteShortcutProbe = ({
1080
+ root
1081
+ }) => {
1082
+ const tools = window.__modelerShellConformance;
1083
+ const rootElement = document.querySelector(root);
1084
+ const active = document.activeElement;
1085
+ if (!rootElement || !active?.matches('[data-chrome="palette"] [data-action]')) return null;
1086
+ const tooltip = [...rootElement.querySelectorAll('[role="tooltip"]')].find(
1087
+ (element) => tools.visible(element)
1088
+ );
1089
+ const chip = tooltip ? [...tooltip.querySelectorAll("kbd")].find((element) => tools.visible(element)) : void 0;
1090
+ return {
1091
+ action: active.getAttribute("data-action") ?? "",
1092
+ chip: (chip?.textContent ?? "").trim(),
1093
+ keyShortcuts: active.getAttribute("aria-keyshortcuts")
1094
+ };
1095
+ };
1096
+ var surroundProbe = ({ root }) => {
1097
+ const tools = window.__modelerShellConformance;
1098
+ const rootElement = document.querySelector(root);
1099
+ if (!rootElement) return null;
1100
+ const transparent = (color) => {
1101
+ if (color === "" || color === "transparent") return true;
1102
+ const legacy = /^rgba\([^)]*,\s*([\d.]+)\)$/.exec(color);
1103
+ if (legacy) return Number(legacy[1]) === 0;
1104
+ const modern = /\/\s*([\d.]+)%?\s*\)$/.exec(color);
1105
+ return modern ? Number(modern[1]) === 0 : false;
1106
+ };
1107
+ const start = rootElement.querySelector(".djs-container");
1108
+ let painted = null;
1109
+ for (let node = start; node && !painted; node = node.parentElement) {
1110
+ const color = getComputedStyle(node).backgroundColor;
1111
+ if (!transparent(color)) painted = { description: tools.describe(node), color };
1112
+ if (node === rootElement) break;
1113
+ }
1114
+ const probe = document.createElement("span");
1115
+ probe.setAttribute("data-modeler-shell-conformance", "role-probe");
1116
+ probe.style.cssText = "position:absolute;visibility:hidden;background-color:var(--cd-ui-grund);";
1117
+ rootElement.append(probe);
1118
+ const grund = getComputedStyle(probe).backgroundColor;
1119
+ probe.remove();
1120
+ return { container: start !== null, painted, grund };
1121
+ };
1122
+ var paletteEntriesProbe = ({ root }) => {
1123
+ const tools = window.__modelerShellConformance;
1124
+ const palette = document.querySelector(root)?.querySelector('[data-chrome="palette"]');
1125
+ if (!palette) return [];
1126
+ return [...palette.querySelectorAll("[data-action]")].filter((entry) => tools.visible(entry)).map((entry) => {
1127
+ const rect = entry.getBoundingClientRect();
1128
+ return {
1129
+ action: entry.getAttribute("data-action") ?? "",
1130
+ group: entry.closest("[data-group]")?.getAttribute("data-group") ?? null,
1131
+ pressed: entry.getAttribute("aria-pressed") === "true",
1132
+ disabled: entry.getAttribute("aria-disabled") === "true",
1133
+ client: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
1134
+ };
1135
+ });
1136
+ };
1137
+ var paletteColorProbe = ({ root }) => {
1138
+ const tools = window.__modelerShellConformance;
1139
+ const palette = document.querySelector(root)?.querySelector('[data-chrome="palette"]');
1140
+ if (!palette) return null;
1141
+ const resolve = (role) => {
1142
+ const probe = document.createElement("span");
1143
+ probe.setAttribute("data-modeler-shell-conformance", "role-probe");
1144
+ probe.style.cssText = `position:absolute;visibility:hidden;color:var(${role});`;
1145
+ palette.append(probe);
1146
+ const color = getComputedStyle(probe).color;
1147
+ probe.remove();
1148
+ return color;
1149
+ };
1150
+ const iconOf = (entry) => {
1151
+ const svg = entry.querySelector("svg");
1152
+ if (svg) return { kind: "svg", color: getComputedStyle(svg).color };
1153
+ const before = getComputedStyle(entry, "::before");
1154
+ if (before.content && before.content !== "none" && before.content !== "normal") {
1155
+ return { kind: "::before", color: before.color };
1156
+ }
1157
+ return null;
1158
+ };
1159
+ return {
1160
+ roles: {
1161
+ text: resolve("--cd-ui-text"),
1162
+ interaktion: resolve("--cd-ui-interaktion"),
1163
+ textLeise: resolve("--cd-ui-text-leise")
1164
+ },
1165
+ entries: [...palette.querySelectorAll("[data-action]")].filter((entry) => tools.visible(entry)).map((entry) => ({
1166
+ action: entry.getAttribute("data-action") ?? "",
1167
+ group: entry.closest("[data-group]")?.getAttribute("data-group") ?? null,
1168
+ pressed: entry.getAttribute("aria-pressed") === "true",
1169
+ disabled: entry.getAttribute("aria-disabled") === "true",
1170
+ hovered: entry.matches(":hover"),
1171
+ color: getComputedStyle(entry).color,
1172
+ icon: iconOf(entry)
1173
+ }))
1174
+ };
1175
+ };
1176
+ var shapesProbe = ({ root }) => {
1177
+ const tools = window.__modelerShellConformance;
1178
+ const rootElement = document.querySelector(root);
1179
+ if (!rootElement) return [];
1180
+ return [...rootElement.querySelectorAll(".djs-container .djs-shape[data-element-id]")].filter((shape) => tools.visible(shape)).map((shape) => {
1181
+ const visual = shape.querySelector(".djs-visual") ?? shape;
1182
+ const rect = visual.getBoundingClientRect();
1183
+ const client = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
1184
+ const local = typeof visual.getBBox === "function" ? visual.getBBox() : rect;
1185
+ const hitElement = document.elementFromPoint(client.x, client.y);
1186
+ return {
1187
+ id: shape.getAttribute("data-element-id") ?? "",
1188
+ selected: shape.classList.contains("selected"),
1189
+ transform: shape.getAttribute("transform") ?? "",
1190
+ size: { width: local.width, height: local.height },
1191
+ box: tools.box(visual, rootElement),
1192
+ client,
1193
+ hit: hitElement !== null && shape.contains(hitElement)
1194
+ };
1195
+ });
1196
+ };
1197
+ var canvasStateProbe = ({ root }) => {
1198
+ const tools = window.__modelerShellConformance;
1199
+ const container = document.querySelector(root)?.querySelector(".djs-container");
1200
+ const active = document.activeElement;
1201
+ const ours = (element) => element.closest("[data-modeler-shell-conformance]") !== null;
1202
+ const visibleIn = (selector) => container ? [...container.querySelectorAll(selector)].filter(
1203
+ (element) => !ours(element) && tools.visible(element)
1204
+ ) : [];
1205
+ return {
1206
+ focusInCanvas: Boolean(container && active && container.contains(active)),
1207
+ focused: !active || active === document.body ? "<body>" : tools.describe(active),
1208
+ contextPadEntries: visibleIn(".djs-context-pad .entry").map(
1209
+ (entry) => entry.getAttribute("data-action") ?? tools.describe(entry)
1210
+ ),
1211
+ textFields: visibleIn('input, textarea, [contenteditable]:not([contenteditable="false"])').map(
1212
+ (field) => tools.describe(field)
1213
+ )
1214
+ };
1215
+ };
1216
+ var readOnlyStateProbe = ({ root }) => {
1217
+ const tools = window.__modelerShellConformance;
1218
+ const rootElement = document.querySelector(root);
1219
+ if (!rootElement) return null;
1220
+ const status = rootElement.querySelector('[data-chrome="status"]');
1221
+ return { readOnly: rootElement.hasAttribute("data-readonly"), status: status ? tools.facts(status) : null };
1222
+ };
1223
+ var menuEntriesProbe = ({ root }) => {
1224
+ const tools = window.__modelerShellConformance;
1225
+ const rootElement = document.querySelector(root);
1226
+ const menu = rootElement ? [...rootElement.querySelectorAll('[role="menu"]')].find((element) => tools.visible(element)) : void 0;
1227
+ if (!menu) return null;
1228
+ const entries = [];
1229
+ const selector = '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]';
1230
+ for (const element of menu.querySelectorAll(selector)) {
1231
+ const role = element.getAttribute("role") ?? "";
1232
+ if (!tools.visible(element)) continue;
1233
+ entries.push({
1234
+ name: (element.getAttribute("aria-label") ?? element.textContent ?? "").replace(/\s+/g, " ").trim(),
1235
+ role,
1236
+ menuId: element.closest("[data-menu-id]")?.getAttribute("data-menu-id") ?? null,
1237
+ disabled: element.getAttribute("aria-disabled") === "true",
1238
+ group: element.closest("[data-menu-group]")?.getAttribute("data-menu-group") ?? null,
1239
+ value: element.getAttribute("data-menu-value"),
1240
+ checked: element.getAttribute("aria-checked"),
1241
+ icon: element.querySelector(".cd-shell-menu-item-icon svg") !== null
1242
+ });
1243
+ }
1244
+ return entries;
1245
+ };
1246
+
1247
+ // src/testing/web-checks.ts
1248
+ var round2 = (value) => Math.round(value * 10) / 10;
1249
+ var normalize = (text) => text.replace(/\s+/g, " ").trim();
1250
+ var MIRAGON_HOME = "https://www.miragon.io/";
1251
+ var MIRAGON_IMPRESSUM = "https://www.miragon.io/impressum/";
1252
+ var MIRAGON_DATENSCHUTZ = "https://www.miragon.io/datenschutz/";
1253
+ var GERMAN_LEGAL_LINKS = ["Impressum", "Datenschutz"];
1254
+ function speaksEnglish(lang) {
1255
+ return /^en(-|$)/i.test(lang.trim());
1256
+ }
1257
+ function primaryAriaProblems(facts, host) {
1258
+ const primary = facts.primary;
1259
+ if (!primary || host !== "web") return [];
1260
+ const problems = [];
1261
+ if (primary.tag !== "button") problems.push(`primary: a <${primary.tag}>, expected a <button>`);
1262
+ const label = normalize(primary.attributes["aria-label"] ?? "");
1263
+ if (!label) {
1264
+ problems.push(
1265
+ "primary: no aria-label; it names the action also while the label is collapsed (guide \xA75.15)"
1266
+ );
1267
+ } else if (!facts.primaryCollapsed && primary.text && !label.includes(primary.text)) {
1268
+ problems.push(
1269
+ `primary: the aria-label "${label}" does not contain the visible label "${primary.text}" (WCAG 2.5.3)`
1270
+ );
1271
+ }
1272
+ if (primary.disabled) {
1273
+ problems.push(
1274
+ "primary: natively disabled; a disabled primary action keeps the focus with aria-disabled (guide \xA78)"
1275
+ );
1276
+ }
1277
+ return problems;
1278
+ }
1279
+ function legalAriaProblems(facts, host) {
1280
+ if (host !== "web") return [];
1281
+ const legal = facts.legal;
1282
+ if (!legal) return ['legal: no [data-chrome="legal"] in the web app (guide \xA75.10)'];
1283
+ const problems = [];
1284
+ if (legal.tag !== "nav") problems.push(`legal: the legal line is a <${legal.tag}>, expected a <nav>`);
1285
+ if (!normalize(legal.attributes["aria-label"] ?? "")) problems.push("legal: the <nav> has no aria-label");
1286
+ for (const text of GERMAN_LEGAL_LINKS) {
1287
+ const link = facts.legalLinks.find((candidate) => candidate.text === text);
1288
+ if (!link) {
1289
+ problems.push(`legal: no "${text}" link (guide \xA75.10)`);
1290
+ } else if (link.lang !== "de") {
1291
+ problems.push(
1292
+ `legal: "${text}" has ${link.lang === null ? "no lang" : `lang="${link.lang}"`}, expected lang="de" (guide \xA72: Impressum and Datenschutz stay German)`
1293
+ );
1294
+ }
1295
+ }
1296
+ for (const link of facts.legalLinks) {
1297
+ if (link.target !== "_blank") continue;
1298
+ const rel = (link.rel ?? "").split(/\s+/);
1299
+ if (!rel.includes("noopener") || !rel.includes("noreferrer")) {
1300
+ problems.push(`legal: the link "${link.text}" opens a new tab without rel="noopener noreferrer"`);
1301
+ }
1302
+ }
1303
+ return problems;
1304
+ }
1305
+ function pillPopupAriaProblems(facts, host) {
1306
+ const pill = facts.problems;
1307
+ if (!pill || host === "ide") return [];
1308
+ const haspopup = pill.attributes["aria-haspopup"];
1309
+ if (host === "embedded" && haspopup === void 0) return [];
1310
+ const problems = [];
1311
+ if (haspopup !== "dialog") {
1312
+ problems.push(
1313
+ `problems: the pill has aria-haspopup "${haspopup ?? ""}", expected "dialog" (it opens the problems list)`
1314
+ );
1315
+ }
1316
+ if (pill.attributes["aria-expanded"] !== "false") {
1317
+ problems.push(
1318
+ `problems: the pill has aria-expanded "${pill.attributes["aria-expanded"] ?? ""}" while the list is closed, expected "false"`
1319
+ );
1320
+ }
1321
+ const controls = pill.attributes["aria-controls"];
1322
+ if (!controls) {
1323
+ problems.push("problems: the pill has no aria-controls");
1324
+ } else if (!facts.problemsControlled) {
1325
+ problems.push(`problems: aria-controls="${controls}" names no element`);
1326
+ } else {
1327
+ const role = facts.problemsControlled.attributes.role;
1328
+ if (role !== "dialog") {
1329
+ problems.push(
1330
+ `problems: aria-controls names a ${facts.problemsControlled.description} with role "${role ?? ""}", expected "dialog"`
1331
+ );
1332
+ }
1333
+ if (facts.problemsControlled.attributes["aria-modal"] === "true") {
1334
+ problems.push(
1335
+ 'problems: the problems list is aria-modal="true"; it is a non-modal popover (guide \xA75.10)'
1336
+ );
1337
+ }
1338
+ }
1339
+ return problems;
1340
+ }
1341
+ var MENU_GROUP_ORDER = ["file", "model", "export", "appearance", "help"];
1342
+ var FILE_ENTRIES = [
1343
+ { id: "new-diagram", icon: true },
1344
+ { id: "open", icon: true },
1345
+ { id: "show-example", icon: false },
1346
+ { id: "import", icon: true }
1347
+ ];
1348
+ var APPEARANCE_VALUES = ["system", "light", "dark"];
1349
+ function menuGroupProblems(entries, host, themes, choice = "system") {
1350
+ const problems = [];
1351
+ const file = entries.filter((entry) => entry.group === "file");
1352
+ const appearance = entries.filter((entry) => entry.group === "appearance");
1353
+ if (host !== "web") {
1354
+ const guide = host === "ide" ? "its file actions are native (guide \xA713.5)" : "the host owns file and mode (guide \xA75.2)";
1355
+ if (file.length > 0) problems.push(`menu: the ${host} host shows a File group; ${guide}`);
1356
+ if (appearance.length > 0) problems.push(`menu: the ${host} host shows an Appearance group; ${guide}`);
1357
+ return problems;
1358
+ }
1359
+ const sequence = [];
1360
+ for (const entry of entries) {
1361
+ if (entry.group && sequence.at(-1) !== entry.group) sequence.push(entry.group);
1362
+ }
1363
+ const rank = (group) => MENU_GROUP_ORDER.indexOf(group);
1364
+ const ordered = sequence.every((group, index) => index === 0 || rank(sequence[index - 1]) < rank(group));
1365
+ if (!ordered) {
1366
+ problems.push(
1367
+ `menu: the groups stand in the order ${sequence.join(", ")}; expected File, Model, Export, Appearance, Help (guide \xA75.4)`
1368
+ );
1369
+ }
1370
+ const known = FILE_ENTRIES.map((entry) => entry.id);
1371
+ for (const entry of file) {
1372
+ const spec = FILE_ENTRIES.find((candidate) => candidate.id === entry.menuId);
1373
+ if (!spec) {
1374
+ problems.push(
1375
+ `menu: File entry "${entry.name}" has data-menu-id "${entry.menuId ?? ""}"; the shell builds the File group from ${known.join(", ")}`
1376
+ );
1377
+ continue;
1378
+ }
1379
+ if (entry.role !== "menuitem")
1380
+ problems.push(`menu: File entry "${entry.name}" is a ${entry.role}, expected a menuitem`);
1381
+ if (spec.icon && !entry.icon) problems.push(`menu: File entry "${entry.name}" has no icon (guide \xA75.4)`);
1382
+ if (!spec.icon && entry.icon) {
1383
+ problems.push(
1384
+ `menu: File entry "${entry.name}" carries an icon; the guide gives ${entry.menuId} none (guide \xA75.14)`
1385
+ );
1386
+ }
1387
+ }
1388
+ const fileIds = file.map((entry) => entry.menuId ?? "");
1389
+ const expectedIds = known.filter((id) => fileIds.includes(id));
1390
+ if (fileIds.join() !== expectedIds.join() && fileIds.every((id) => known.includes(id))) {
1391
+ problems.push(
1392
+ `menu: the File group reads ${fileIds.join(", ")}; expected the order ${expectedIds.join(", ")} (guide \xA75.4)`
1393
+ );
1394
+ }
1395
+ if (themes.includes("dark")) {
1396
+ if (appearance.length === 0) {
1397
+ problems.push("menu: a dark-capable modeler shows no Appearance group (guide \xA75.4, \xA713.3)");
1398
+ } else {
1399
+ const values = appearance.map((entry) => entry.value ?? "");
1400
+ if (values.join() !== APPEARANCE_VALUES.join()) {
1401
+ problems.push(
1402
+ `menu: the Appearance options are ${values.join(", ")}; expected system, light, dark (guide \xA713.3)`
1403
+ );
1404
+ }
1405
+ for (const entry of appearance) {
1406
+ if (entry.role !== "menuitemradio" || entry.menuId !== "appearance") {
1407
+ problems.push(
1408
+ `menu: Appearance option "${entry.name}" is a ${entry.role} in "${entry.menuId ?? ""}"; expected a menuitemradio of the radio group "appearance"`
1409
+ );
1410
+ }
1411
+ }
1412
+ problems.push(...appearanceChoiceProblems(entries, choice));
1413
+ }
1414
+ }
1415
+ return problems;
1416
+ }
1417
+ function appearanceChoiceProblems(entries, choice) {
1418
+ const checked = entries.filter((entry) => entry.group === "appearance" && entry.checked === "true").map((entry) => entry.value ?? "");
1419
+ return checked.length === 1 && checked[0] === choice ? [] : [
1420
+ `menu: the Appearance group checks ${checked.length > 0 ? checked.join(", ") : "nothing"}; expected "${choice}" (aria-checked, guide \xA712)`
1421
+ ];
1422
+ }
1423
+ function lightOnlyMenuProblems(entries, themes) {
1424
+ if (themes.includes("dark")) return [];
1425
+ return entries.some((entry) => entry.group === "appearance" || entry.menuId === "appearance") ? ["menu: a light-only modeler shows the Appearance group; it comes with dark (guide \xA713.3)"] : [];
1426
+ }
1427
+ function labelledWidth(samples) {
1428
+ const widths = samples.flatMap(
1429
+ (sample) => sample.primary && !sample.primary.collapsed && sample.primary.label ? [sample.primary.box.width] : []
1430
+ );
1431
+ return widths.length > 0 ? Math.max(...widths) : void 0;
1432
+ }
1433
+ function topRowProblems(samples, withLabel, name2) {
1434
+ const problems = [];
1435
+ for (const sample of samples) {
1436
+ const { container, paletteWidth, menuRight, primary } = sample;
1437
+ if (!primary || paletteWidth === null || menuRight === null) continue;
1438
+ const decide = (palette, labelled) => decideTopRow({
1439
+ containerWidth: container.width,
1440
+ paletteWidth: palette,
1441
+ menuRight,
1442
+ primary: { withLabel: labelled, iconOnly: SHELL.row }
1443
+ });
1444
+ const expected = decide(paletteWidth, withLabel);
1445
+ const low = decide(paletteWidth - 1, withLabel - 1);
1446
+ const high = decide(paletteWidth + 1, withLabel + 1);
1447
+ const at = `[${round2(container.width)}px wide]`;
1448
+ if (low.paletteRow === high.paletteRow && low.primaryLabel === high.primaryLabel && expected.paletteRow === low.paletteRow && expected.primaryLabel === low.primaryLabel) {
1449
+ const collapsed = expected.primaryLabel === "collapsed";
1450
+ if (collapsed && !primary.collapsed) {
1451
+ problems.push(
1452
+ expected.paletteRow === "top" ? `${at} primary: shows its label, but the ${round2(paletteWidth)}px palette fits the top row only beside the icon; the label collapses first (guide \xA75.15)` : `${at} primary: shows its label, but it no longer fits next to the menu trigger (data-primary="collapsed", guide \xA75.15)`
1453
+ );
1454
+ }
1455
+ if (!collapsed && primary.collapsed) {
1456
+ problems.push(`${at} primary: collapsed although its ${round2(withLabel)}px label fits (guide \xA75.15)`);
1457
+ }
1458
+ if (expected.paletteRow !== sample.paletteRow) {
1459
+ problems.push(
1460
+ expected.paletteRow === "top" ? `${at} palette: in the second row although it fits the top row beside the collapsed primary action; the label collapses first (guide \xA75.15)` : `${at} palette: in the top row although it keeps less than 16px to the primary action even icon only (guide \xA75.15)`
1461
+ );
1462
+ }
1463
+ }
1464
+ if (primary.collapsed) {
1465
+ if (Math.abs(primary.box.width - SHELL.row) > GEOMETRY_TOLERANCE_PX + 1e-6) {
1466
+ problems.push(
1467
+ `${at} primary: ${round2(primary.box.width)}px wide while collapsed, expected ${SHELL.row}px`
1468
+ );
1469
+ }
1470
+ if (primary.label)
1471
+ problems.push(`${at} primary: collapsed, but its label "${primary.label}" still shows`);
1472
+ if (primary.ariaLabel !== name2) {
1473
+ problems.push(
1474
+ `${at} primary: the aria-label is "${primary.ariaLabel ?? ""}" while collapsed; it keeps "${name2}" (guide \xA75.15)`
1475
+ );
1476
+ }
1477
+ } else if (!primary.label) {
1478
+ problems.push(`${at} primary: shows no label although the root lacks data-primary="collapsed"`);
1479
+ }
1480
+ if (primary.describedBy) {
1481
+ problems.push(
1482
+ `${at} primary: aria-describedby="${primary.describedBy}"; its tooltip repeats the name (API.md, Primary action)`
1483
+ );
1484
+ }
1485
+ }
1486
+ return problems;
1487
+ }
1488
+ function primaryTooltipProblems(collapsed, tooltip, name2, width) {
1489
+ const at = `[${round2(width)}px wide]`;
1490
+ if (collapsed && tooltip === null) {
1491
+ return [`${at} primary: no chrome tooltip while collapsed; it shows the label "${name2}" (guide \xA75.15)`];
1492
+ }
1493
+ if (collapsed && tooltip !== null && !normalize(tooltip).startsWith(name2)) {
1494
+ return [`${at} primary: the tooltip while collapsed reads "${normalize(tooltip)}", expected "${name2}"`];
1495
+ }
1496
+ if (!collapsed && tooltip !== null) {
1497
+ return [
1498
+ `${at} primary: a tooltip "${normalize(tooltip)}" while the label shows; only the collapsed action has one`
1499
+ ];
1500
+ }
1501
+ return [];
1502
+ }
1503
+ function primaryActivationProblems(clicks) {
1504
+ const problems = [];
1505
+ if (clicks.pointer !== 1)
1506
+ problems.push(`primary: a pointer click reached it ${clicks.pointer} times, expected once`);
1507
+ if (clicks.enter !== 1) {
1508
+ problems.push(
1509
+ `primary: Enter on the focused primary action clicked it ${clicks.enter} times, expected once (keyboard activation)`
1510
+ );
1511
+ }
1512
+ return problems;
1513
+ }
1514
+ function legalContentProblems(links) {
1515
+ const problems = [];
1516
+ const [copyright, impressum, datenschutz] = links;
1517
+ if (!copyright || !/^©/.test(copyright.text) || !copyright.text.includes("Miragon GmbH")) {
1518
+ problems.push(
1519
+ `legal: the line starts with "${copyright?.text ?? ""}", expected the copyright "\xA9 <year> Miragon GmbH" (guide \xA75.10)`
1520
+ );
1521
+ } else if (copyright.href !== MIRAGON_HOME) {
1522
+ problems.push(`legal: the copyright links "${copyright.href}", expected ${MIRAGON_HOME}`);
1523
+ }
1524
+ const expected = [
1525
+ [impressum, "Impressum", MIRAGON_IMPRESSUM],
1526
+ [datenschutz, "Datenschutz", MIRAGON_DATENSCHUTZ]
1527
+ ];
1528
+ for (const [link, text, href] of expected) {
1529
+ if (link?.text !== text) {
1530
+ problems.push(
1531
+ `legal: the links read ${links.map((entry) => `"${entry.text}"`).join(", ")}; expected the copyright, "Impressum", "Datenschutz", then the modeler's links (guide \xA75.10)`
1532
+ );
1533
+ break;
1534
+ }
1535
+ if (link.href !== href) problems.push(`legal: "${text}" links "${link.href}", expected ${href}`);
1536
+ }
1537
+ return [...new Set(problems)];
1538
+ }
1539
+ function legalReachProblems(facts) {
1540
+ const legal = facts.legal;
1541
+ if (!legal) return [];
1542
+ const width = round2(facts.container.width);
1543
+ const problems = [];
1544
+ for (const link of facts.links) {
1545
+ const box = link.box;
1546
+ const inside = box.left >= Math.max(0, legal.left) - GEOMETRY_TOLERANCE_PX && box.left + box.width <= Math.min(legal.left + legal.width, facts.container.width) + GEOMETRY_TOLERANCE_PX && box.top >= legal.top - GEOMETRY_TOLERANCE_PX && box.top + box.height <= legal.top + legal.height + GEOMETRY_TOLERANCE_PX;
1547
+ if (!inside) {
1548
+ problems.push(
1549
+ `legal: [${width}px wide] "${link.text}" is cut off or scrolled out of the legal line; it wraps instead (guide \xA75.10, WCAG 1.4.10)`
1550
+ );
1551
+ }
1552
+ }
1553
+ return problems;
1554
+ }
1555
+ function zoomRowBottomProblems(zoom, raised, container, legalHeight) {
1556
+ if (!zoom) return [];
1557
+ const bottom = raised ? SHELL.edge + legalLineHeight(legalHeight) + SHELL.gap : SHELL.edge;
1558
+ return boxMismatches("zoom", zoom, { bottom }, container).map(
1559
+ (problem) => `[${round2(container.width)}px wide${raised ? ", raised" : ""}] ${problem}`
1560
+ );
1561
+ }
1562
+ function emptyStateProblems(facts, container, lang) {
1563
+ const problems = [];
1564
+ if (!facts.shown)
1565
+ problems.push('empty-state: the root lacks data-empty-state="shown" after show.emptyState');
1566
+ const card = facts.card;
1567
+ if (!card?.visible) return [...problems, "empty-state: the card is not visible after show.emptyState"];
1568
+ problems.push(
1569
+ ...boxMismatches(
1570
+ "empty-state",
1571
+ card.box,
1572
+ {
1573
+ centerX: container.width / 2,
1574
+ centerY: container.height / 2,
1575
+ width: Math.min(400, container.width - 2 * SHELL.edge)
1576
+ },
1577
+ container
1578
+ )
1579
+ );
1580
+ if (!card.labelledBy) problems.push("empty-state: the card is not labelled by its title (aria-labelledby)");
1581
+ if (!facts.komet) {
1582
+ problems.push("empty-state: no Komet <img> on the card (guide \xA72, \xA78)");
1583
+ } else {
1584
+ if (facts.komet.alt !== "") {
1585
+ problems.push(
1586
+ `empty-state: the Komet has ${facts.komet.alt === null ? "no alt" : `alt="${facts.komet.alt}"`}; it is decorative, alt="" (guide \xA78)`
1587
+ );
1588
+ }
1589
+ if (Math.abs(facts.komet.width - 64) > 1 || Math.abs(facts.komet.height - 64) > 1) {
1590
+ problems.push(
1591
+ `empty-state: the Komet is ${round2(facts.komet.width)} \xD7 ${round2(facts.komet.height)}px, expected 64 \xD7 64 (guide \xA78)`
1592
+ );
1593
+ }
1594
+ }
1595
+ if (facts.heading?.tag !== "h1") {
1596
+ problems.push(
1597
+ `empty-state: the title is ${facts.heading ? `a <${facts.heading.tag}>` : "missing"}, expected an <h1>`
1598
+ );
1599
+ }
1600
+ const [primary, ghost, ...rest] = facts.buttons;
1601
+ if (!primary) {
1602
+ problems.push('empty-state: no "New diagram" button on the card (guide \xA78)');
1603
+ } else {
1604
+ if (primary.variant !== "primary") {
1605
+ problems.push(
1606
+ `empty-state: the first button "${primary.name}" is ${primary.variant ?? "no"} variant, expected the blue primary "New diagram" first (guide \xA78)`
1607
+ );
1608
+ }
1609
+ if (ghost && ghost.variant !== "ghost") {
1610
+ problems.push(
1611
+ `empty-state: the second button "${ghost.name}" is ${ghost.variant ?? "no"} variant, expected the ghost "Show example" (guide \xA78)`
1612
+ );
1613
+ }
1614
+ if (rest.length > 0)
1615
+ problems.push(
1616
+ `empty-state: ${facts.buttons.length} buttons on the card; exactly two actions (guide \xA78)`
1617
+ );
1618
+ for (const button of [primary, ghost]) {
1619
+ if (button && Math.abs(button.height - SHELL.entry) > 1) {
1620
+ problems.push(
1621
+ `empty-state: "${button.name}" is ${round2(button.height)}px high, expected ${SHELL.entry}px (guide \xA78)`
1622
+ );
1623
+ }
1624
+ }
1625
+ if (speaksEnglish(lang)) {
1626
+ if (primary.name !== defaultMessages.newDiagram) {
1627
+ problems.push(
1628
+ `empty-state: the primary button reads "${primary.name}", expected "${defaultMessages.newDiagram}" (guide \xA72)`
1629
+ );
1630
+ }
1631
+ if (ghost && ghost.name !== defaultMessages.showExample) {
1632
+ problems.push(
1633
+ `empty-state: the ghost button reads "${ghost.name}", expected "${defaultMessages.showExample}" (guide \xA72)`
1634
+ );
1635
+ }
1636
+ }
1637
+ }
1638
+ for (const part of facts.work) {
1639
+ if (part.visible) {
1640
+ problems.push(
1641
+ `empty-state: ${part.name} is visible behind the start screen; it hides the work chrome (guide \xA78)`
1642
+ );
1643
+ }
1644
+ }
1645
+ if (!facts.canvasInert) {
1646
+ problems.push(
1647
+ "empty-state: the canvas slot is not inert behind the start screen; nothing else takes a click or the Tab (guide \xA78)"
1648
+ );
1649
+ }
1650
+ if (!facts.legalVisible)
1651
+ problems.push("empty-state: the legal line is not visible; a public page always shows it (guide \xA78)");
1652
+ return problems;
1653
+ }
1654
+ function emptyStateTabProblems(owners) {
1655
+ const problems = [];
1656
+ for (const stop of owners) {
1657
+ if (["empty-state", "legal", "toasts", "body", "outside"].includes(stop.owner)) continue;
1658
+ problems.push(
1659
+ `empty-state: Tab reaches ${stop.description} (${stop.owner}) behind the start screen; only the card and the legal links take the focus (guide \xA78)`
1660
+ );
1661
+ }
1662
+ if (!owners.some((stop) => stop.owner === "empty-state")) {
1663
+ problems.push("empty-state: Tab never reaches the card's buttons");
1664
+ }
1665
+ if (!owners.some((stop) => stop.owner === "legal"))
1666
+ problems.push("empty-state: Tab never reaches the legal links");
1667
+ return [...new Set(problems)];
1668
+ }
1669
+ function emptyStateKeyProblems(key, before, after, keys) {
1670
+ const problems = [];
1671
+ if (after !== before) {
1672
+ problems.push(
1673
+ `empty-state: ${key} behind the start screen changed the zoom from ${before ?? "?"}% to ${after ?? "?"}%; the chrome keys zoom no diagram there`
1674
+ );
1675
+ }
1676
+ if (keys.some((recorded) => recorded.defaultPrevented)) {
1677
+ problems.push(
1678
+ `empty-state: ${key} behind the start screen arrived prevented; with no diagram to zoom the browser keeps its page zoom (WCAG 1.4.4)`
1679
+ );
1680
+ }
1681
+ return problems;
1682
+ }
1683
+ function afterStartProblems(facts, focus) {
1684
+ const problems = [];
1685
+ const after = 'after "New diagram"';
1686
+ if (facts.shown) problems.push(`empty-state: ${after} the root still carries data-empty-state="shown"`);
1687
+ if (facts.card?.visible) problems.push(`empty-state: ${after} the card still shows`);
1688
+ for (const name2 of ["menu", "palette", "zoom"]) {
1689
+ const part = facts.work.find((candidate) => candidate.name === name2);
1690
+ if (part && !part.visible) problems.push(`empty-state: ${after} the ${name2} is still hidden`);
1691
+ }
1692
+ if (facts.canvasInert) problems.push(`empty-state: ${after} the canvas slot is still inert`);
1693
+ if (focus.owner !== "canvas") {
1694
+ problems.push(
1695
+ `empty-state: ${after} the focus is on ${focus.description}, expected the canvas <svg> (API.md, Empty state)`
1696
+ );
1697
+ }
1698
+ return problems;
1699
+ }
1700
+ function toastLiveRegion(regions, status) {
1701
+ const role = status === "danger" ? "alert" : "status";
1702
+ return regions.find((region) => region.role === role);
1703
+ }
1704
+ function toastAnatomyProblems(toast, status, lang, announcement) {
1705
+ const problems = [];
1706
+ const role = status === "danger" ? "alert" : "status";
1707
+ if (!announcement.persistent) {
1708
+ problems.push(
1709
+ `toasts: no role="${role}" live region in the root before the first toast; a live region created together with its text is often not announced (guide \xA78, \xA712)`
1710
+ );
1711
+ } else if (!announcement.announced) {
1712
+ problems.push(
1713
+ `toasts: the ${status} toast's text never reaches the role="${role}" live region (guide \xA78, \xA712)`
1714
+ );
1715
+ }
1716
+ if (toast.role === "status" || toast.role === "alert") {
1717
+ problems.push(
1718
+ `toasts: the ${status} toast's card carries role="${toast.role}"; the live region announces it, so a live card is read twice`
1719
+ );
1720
+ }
1721
+ const close = toast.buttons.at(-1);
1722
+ if (!close || close.text !== "") {
1723
+ problems.push(`toasts: the ${status} toast has no Close icon button at its end (guide \xA78)`);
1724
+ } else if (!close.name) {
1725
+ problems.push(`toasts: the ${status} toast's Close button has no accessible name`);
1726
+ } else if (speaksEnglish(lang) && close.name !== defaultMessages.close) {
1727
+ problems.push(
1728
+ `toasts: the Close button is named "${close.name}", expected "${defaultMessages.close}" (guide \xA78)`
1729
+ );
1730
+ }
1731
+ if (toast.box.height < 40 - GEOMETRY_TOLERANCE_PX) {
1732
+ problems.push(
1733
+ `toasts: the ${status} toast is ${round2(toast.box.height)}px high, at least 40px (guide \xA78)`
1734
+ );
1735
+ }
1736
+ return problems;
1737
+ }
1738
+ function shownToasts(toasts2, texts) {
1739
+ return toasts2.flatMap((toast) => texts.filter((text) => toast.text.includes(text)));
1740
+ }
1741
+ function toastStackProblems(toasts2, shown, expected) {
1742
+ const problems = [];
1743
+ const present = shownToasts(toasts2, shown);
1744
+ if (toasts2.length > MAX_TOASTS)
1745
+ problems.push(`toasts: ${toasts2.length} toasts at once, at most ${MAX_TOASTS} (guide \xA78)`);
1746
+ if (present.join("\0") !== expected.join("\0")) {
1747
+ problems.push(
1748
+ `toasts: after ${shown.length} toasts the stack shows ${present.map((text) => `"${text}"`).join(", ") || "none"}; expected ${expected.map((text) => `"${text}"`).join(", ")} (at most ${MAX_TOASTS}, the oldest without an action leaves, guide \xA78)`
1749
+ );
1750
+ }
1751
+ const tops = toasts2.filter((toast) => shown.some((text) => toast.text.includes(text))).map((toast) => toast.box.top);
1752
+ if (tops.some((top, index) => index > 0 && top < tops[index - 1])) {
1753
+ problems.push("toasts: the newest toast is not at the bottom of the stack (guide \xA78)");
1754
+ }
1755
+ return problems;
1756
+ }
1757
+ function repeatedToastProblems(toasts2, text) {
1758
+ const count = toasts2.filter((toast) => toast.text.includes(text)).length;
1759
+ return count > 1 ? [`toasts: a repeated "${text}" stacks ${count} toasts; it restarts the shown one (API.md, Toasts)`] : [];
1760
+ }
1761
+ var TOAST_RULES = {
1762
+ plain: "it stays for its lifetime",
1763
+ hovered: "hover pauses its time",
1764
+ focused: "focus pauses its time",
1765
+ resumed: "it resumes with at least 1.5s left",
1766
+ action: "a toast with an action never closes by itself"
1767
+ };
1768
+ function toastTimingProblems(timings) {
1769
+ return timings.flatMap((timing) => {
1770
+ const since = timing.condition === "resumed" ? "after the pointer and the focus left it" : "after it appeared";
1771
+ if (timing.expectation === "present" && !timing.present) {
1772
+ return [
1773
+ `toasts: "${timing.text}" closed within ${timing.after}ms ${since}; ${TOAST_RULES[timing.condition]} (guide \xA78)`
1774
+ ];
1775
+ }
1776
+ if (timing.expectation === "gone" && timing.present) {
1777
+ return [
1778
+ `toasts: "${timing.text}" is still there ${timing.after}ms ${since}; it closes after its lifetime (guide \xA78)`
1779
+ ];
1780
+ }
1781
+ return [];
1782
+ });
1783
+ }
1784
+ function popoverKeyboardProblems(steps, rowCount) {
1785
+ const problems = [];
1786
+ for (const step of steps) {
1787
+ if (step.focused?.index !== step.expected) {
1788
+ problems.push(
1789
+ `problems-popover: ${step.key} reaches ${step.focused ? `row ${step.focused.index + 1}` : "no row"}, expected row ${step.expected + 1} of ${rowCount}; the keyboard reads every row, text rows too (guide \xA75.10, WCAG 2.1.1)`
1790
+ );
1791
+ } else if (!step.focused.inView) {
1792
+ problems.push(
1793
+ `problems-popover: row ${step.expected + 1} takes the focus on ${step.key} but stays scrolled out of the list`
1794
+ );
1795
+ }
1796
+ }
1797
+ return problems;
1798
+ }
1799
+ function openPopoverProblems(facts, container, lang) {
1800
+ const problems = [];
1801
+ const popover2 = facts.popover;
1802
+ if (!popover2?.visible)
1803
+ return ["problems-popover: a click on the pill shows no problems list (guide \xA75.10)"];
1804
+ if (popover2.attributes.role !== "dialog") {
1805
+ problems.push(`problems-popover: role "${popover2.attributes.role ?? ""}", expected "dialog"`);
1806
+ }
1807
+ if (popover2.attributes["aria-modal"] === "true")
1808
+ problems.push('problems-popover: aria-modal="true"; it is not modal');
1809
+ if (!popover2.title) {
1810
+ problems.push("problems-popover: not labelled by its title (aria-labelledby)");
1811
+ } else if (speaksEnglish(lang) && popover2.title !== defaultMessages.problemsTitle) {
1812
+ problems.push(
1813
+ `problems-popover: titled "${popover2.title}", expected "${defaultMessages.problemsTitle}" (guide \xA75.10)`
1814
+ );
1815
+ }
1816
+ if (facts.pill?.attributes["aria-expanded"] !== "true") {
1817
+ problems.push(
1818
+ `problems: aria-expanded is "${facts.pill?.attributes["aria-expanded"] ?? ""}" while the list is open, expected "true"`
1819
+ );
1820
+ }
1821
+ if (facts.rows.length === 0) {
1822
+ problems.push("problems-popover: the list has no rows; show.problems passes items in the web host");
1823
+ }
1824
+ for (const row of facts.rows) {
1825
+ if (row.height < SHELL.entry - GEOMETRY_TOLERANCE_PX) {
1826
+ problems.push(
1827
+ `problems-popover: the row "${row.text}" is ${round2(row.height)}px high, at least ${SHELL.entry}px (guide \xA75.10)`
1828
+ );
1829
+ }
1830
+ if (!row.icon)
1831
+ problems.push(`problems-popover: the row "${row.text}" has no severity icon (guide \xA75.10)`);
1832
+ }
1833
+ if (facts.focus !== "popover") {
1834
+ problems.push(
1835
+ `problems-popover: opening it leaves the focus on ${facts.focused}; it moves to the first row (API.md, Problems popover)`
1836
+ );
1837
+ }
1838
+ const pill = facts.pill;
1839
+ if (pill) {
1840
+ problems.push(
1841
+ ...boxMismatches(
1842
+ "problems-popover",
1843
+ popover2.box,
1844
+ { left: SHELL.edge, bottom: container.height - pill.box.top + SHELL.gap },
1845
+ container
1846
+ )
1847
+ );
1848
+ if (popover2.box.width > Math.min(320, container.width - 2 * SHELL.edge) + GEOMETRY_TOLERANCE_PX + 1e-6) {
1849
+ problems.push(
1850
+ `problems-popover: width is ${round2(popover2.box.width)}px, at most ${round2(Math.min(320, container.width - 2 * SHELL.edge))}px expected`
1851
+ );
1852
+ }
1853
+ if (popover2.box.height > 320 + GEOMETRY_TOLERANCE_PX + 1e-6) {
1854
+ problems.push(`problems-popover: height is ${round2(popover2.box.height)}px, at most 320px expected`);
1855
+ }
1856
+ }
1857
+ return problems;
1858
+ }
1859
+ function closedPopoverProblems(facts, how) {
1860
+ const problems = [];
1861
+ if (facts.popover?.visible) problems.push(`problems-popover: ${how} does not close the list`);
1862
+ if (facts.pill && facts.pill.attributes["aria-expanded"] !== "false") {
1863
+ problems.push(
1864
+ `problems: aria-expanded is "${facts.pill.attributes["aria-expanded"] ?? ""}" after ${how}, expected "false"`
1865
+ );
1866
+ }
1867
+ if (how === "Escape" && facts.focus !== "pill") {
1868
+ problems.push(`problems-popover: Escape leaves the focus on ${facts.focused}, expected the pill`);
1869
+ }
1870
+ if (how === "an outside click" && facts.focus === "pill") {
1871
+ problems.push(
1872
+ "problems-popover: an outside click moved the focus to the pill; it closes without moving the focus"
1873
+ );
1874
+ }
1875
+ if (how === "a row with an element" && facts.focus !== "canvas") {
1876
+ problems.push(
1877
+ `problems-popover: a row with an element leaves the focus on ${facts.focused}, expected the canvas`
1878
+ );
1879
+ }
1880
+ return problems;
1881
+ }
1882
+ function dropStateProblems(facts, expected, context, container) {
1883
+ const problems = [];
1884
+ const zone = facts.zone;
1885
+ const over = facts.events.filter((event) => event.type === "dragover");
1886
+ if (expected === "none") {
1887
+ if (facts.state !== null)
1888
+ problems.push(`drop-zone: ${context} sets data-drop="${facts.state}"; it is no file drag`);
1889
+ if (zone?.visible) problems.push(`drop-zone: ${context} shows the drop zone`);
1890
+ if (over.some((event) => event.defaultPrevented))
1891
+ problems.push(`drop-zone: ${context} is prevented; the drop zone leaves non-file drags alone`);
1892
+ return problems;
1893
+ }
1894
+ if (facts.state !== expected) {
1895
+ problems.push(
1896
+ `drop-zone: ${context} sets data-drop="${facts.state ?? ""}", expected "${expected}" (guide \xA79)`
1897
+ );
1898
+ }
1899
+ if (facts.events.some((event) => !event.defaultPrevented && event.type !== "dragleave")) {
1900
+ problems.push(`drop-zone: ${context} is not prevented, so the browser would open the file`);
1901
+ }
1902
+ const effect = expected === "over" ? "copy" : "none";
1903
+ for (const event of over) {
1904
+ if (event.dropEffect !== effect)
1905
+ problems.push(`drop-zone: ${context} has dropEffect "${event.dropEffect}", expected "${effect}"`);
1906
+ }
1907
+ if (!zone?.visible) return [...problems, `drop-zone: ${context} shows no drop zone (guide \xA79)`];
1908
+ problems.push(
1909
+ ...boxMismatches("drop-zone", zone.box, { left: 12, top: 12, right: 12, bottom: 12 }, container).map(
1910
+ (problem) => `${problem} (${context})`
1911
+ )
1912
+ );
1913
+ if (zone.ariaHidden !== "true") problems.push('drop-zone: the overlay lacks aria-hidden="true"');
1914
+ const fill = expected === "over" ? ["--cd-ui-hover", facts.roles.hover] : ["--cd-ui-danger-flaeche", facts.roles.dangerFlaeche];
1915
+ const border = expected === "over" ? ["--cd-ui-interaktion", facts.roles.interaktion] : ["--cd-ui-danger", facts.roles.danger];
1916
+ if (zone.background !== fill[1]) {
1917
+ problems.push(
1918
+ `drop-zone: ${context} fills ${zone.background}, expected ${fill[0]} (${fill[1]}) (guide \xA79)`
1919
+ );
1920
+ }
1921
+ if (zone.borderStyle !== "dashed" || zone.borderWidth !== "2px" || zone.borderColor !== border[1]) {
1922
+ problems.push(
1923
+ `drop-zone: ${context} has a ${zone.borderWidth} ${zone.borderStyle} ${zone.borderColor} border, expected 2px dashed ${border[0]} (${border[1]}) (guide \xA79)`
1924
+ );
1925
+ }
1926
+ if (!facts.chip?.text) {
1927
+ problems.push(`drop-zone: ${context} shows no chip text (guide \xA79)`);
1928
+ } else if (facts.chip.background !== facts.roles.flaecheHoch) {
1929
+ problems.push(
1930
+ `drop-zone: the chip fills ${facts.chip.background}, expected --cd-ui-flaeche-hoch (${facts.roles.flaecheHoch})`
1931
+ );
1932
+ }
1933
+ return problems;
1934
+ }
1935
+ function expectedWebMode(requested, themes, choice) {
1936
+ const kontrast = requested.kontrast ? { kontrast: requested.kontrast } : {};
1937
+ if (!themes.includes("dark")) return { carrier: "light", effective: { theme: "light", ...kontrast } };
1938
+ if (choice === "system") return { carrier: "system", effective: { theme: requested.theme, ...kontrast } };
1939
+ return { carrier: choice, effective: { theme: choice, ...kontrast } };
1940
+ }
1941
+ function webModeProblems(facts, themes, choice, colors) {
1942
+ const problems = [];
1943
+ const dark = themes.includes("dark");
1944
+ if (!dark) {
1945
+ const metas = facts.themeColors;
1946
+ if (metas.length !== 1 || metas[0]?.media !== null) {
1947
+ problems.push(
1948
+ `mode: a light-only web app carries one theme-color without media (guide \xA713.3); found ${metas.map((meta) => meta.media ? `${meta.content} (${meta.media})` : meta.content).join(", ") || "none"}`
1949
+ );
1950
+ }
1951
+ if (facts.colorScheme !== null && facts.colorScheme.split(/\s+/).includes("dark")) {
1952
+ problems.push(`mode: a light-only web app declares color-scheme "${facts.colorScheme}"`);
1953
+ }
1954
+ return problems;
1955
+ }
1956
+ const label = choice === "system" ? "System" : choice === "dark" ? "Dark" : "Light";
1957
+ const expectedScheme = choice === "system" ? "light dark" : choice;
1958
+ if (facts.colorScheme !== expectedScheme) {
1959
+ problems.push(
1960
+ `mode: with ${label} <meta name="color-scheme"> reads "${facts.colorScheme ?? "(none)"}", expected "${expectedScheme}" (guide \xA713.3)`
1961
+ );
1962
+ }
1963
+ const stored = choice === "system" ? null : choice;
1964
+ if (!facts.storageBlocked && facts.stored !== stored) {
1965
+ problems.push(
1966
+ stored === null ? `mode: with System localStorage keeps miragon.darstellung="${facts.stored ?? ""}"; System removes the entry (guide \xA713.3)` : `mode: with ${label} localStorage holds miragon.darstellung=${facts.stored === null ? "(none)" : `"${facts.stored}"`}, expected "${stored}" (guide \xA713.3)`
1967
+ );
1968
+ }
1969
+ const byMedia = (scheme) => facts.themeColors.find((meta) => meta.media?.replace(/\s+/g, "") === `(prefers-color-scheme:${scheme})`);
1970
+ const light = byMedia("light");
1971
+ const darkMeta = byMedia("dark");
1972
+ if (!light || !darkMeta) {
1973
+ problems.push(
1974
+ "mode: a dark-capable web app carries two theme-color metas, one per prefers-color-scheme (guide \xA713.3)"
1975
+ );
1976
+ } else if (colors) {
1977
+ const want = choice === "system" ? colors : { light: colors[choice], dark: colors[choice] };
1978
+ if (light.content !== want.light || darkMeta.content !== want.dark) {
1979
+ problems.push(
1980
+ `mode: with ${label} the theme-color metas read ${light.content} (light) and ${darkMeta.content} (dark), expected ${want.light} and ${want.dark} (guide \xA713.3)`
1981
+ );
1982
+ }
1983
+ }
1984
+ return problems;
1985
+ }
1986
+ function modeSwitchProblems(changes, label) {
1987
+ const relevant = changes.filter((change) => ["theme", "color-scheme", "theme-color"].includes(change.part));
1988
+ if (relevant.length === 0) return [`mode: choosing ${label} changed nothing on <html> or its meta tags`];
1989
+ const problems = [];
1990
+ const frames = [...new Set(relevant.map((change) => change.frame))];
1991
+ if (frames.length > 1) {
1992
+ const parts = relevant.map((change) => `${change.part} in frame ${change.frame}`).join(", ");
1993
+ problems.push(`mode: choosing ${label} changed ${parts}; one frame for all of them (guide \xA713.3)`);
1994
+ }
1995
+ const first = Math.min(...frames);
1996
+ if (!changes.some(
1997
+ (change) => change.part === "no-transition" && change.value === "on" && change.frame === first
1998
+ )) {
1999
+ problems.push(
2000
+ `mode: choosing ${label} switched without cd-shell-no-transition on <html> in that frame (guide \xA77: no colour fades)`
2001
+ );
2002
+ }
2003
+ return problems;
2004
+ }
2005
+
2006
+ // src/testing/checks.ts
2007
+ var normalize2 = (text) => text.replace(/\s+/g, " ").trim();
2008
+ var name = (element) => normalize2(element.attributes["aria-label"] ?? element.text);
2009
+ var attribute = (element, key) => element.attributes[key];
2010
+ function expectedKeyShortcuts(platform) {
2011
+ const modifier = platform === "mac" ? "Meta" : "Control";
2012
+ return {
2013
+ in: `${modifier}+= ${modifier}+Plus`,
2014
+ out: `${modifier}+-`,
2015
+ actual: `${modifier}+0`,
2016
+ fit: "Shift+1"
2017
+ };
2018
+ }
2019
+ function ariaProblems(facts, host) {
2020
+ const problems = [];
2021
+ if (host === "embedded") {
2022
+ if (!facts.rootLang)
2023
+ problems.push("root: an embedded shell root carries the tool's lang itself (guide \xA72, \xA712)");
2024
+ } else if (!facts.documentLang) {
2025
+ problems.push("<html> has no lang (guide \xA72)");
2026
+ }
2027
+ const trigger = facts.trigger;
2028
+ if (!trigger) {
2029
+ problems.push('menu: no [data-chrome="menu"] trigger');
2030
+ } else {
2031
+ if (trigger.tag !== "button")
2032
+ problems.push(`menu: the trigger is a <${trigger.tag}>, expected a <button>`);
2033
+ if (!attribute(trigger, "aria-label")) problems.push("menu: the icon-only trigger has no aria-label");
2034
+ if (attribute(trigger, "aria-haspopup") !== "menu") {
2035
+ problems.push(`menu: aria-haspopup is "${attribute(trigger, "aria-haspopup") ?? ""}", expected "menu"`);
2036
+ }
2037
+ if (attribute(trigger, "aria-expanded") !== "false") {
2038
+ problems.push(
2039
+ `menu: aria-expanded is "${attribute(trigger, "aria-expanded") ?? ""}" while closed, expected "false"`
2040
+ );
2041
+ }
2042
+ if (!attribute(trigger, "aria-controls")) {
2043
+ problems.push("menu: the trigger has no aria-controls");
2044
+ } else if (!facts.controlled) {
2045
+ problems.push(`menu: aria-controls="${attribute(trigger, "aria-controls")}" names no element`);
2046
+ } else if (attribute(facts.controlled, "role") !== "menu") {
2047
+ problems.push(
2048
+ `menu: aria-controls names a ${facts.controlled.description} with role "${attribute(facts.controlled, "role") ?? ""}", expected "menu"`
2049
+ );
2050
+ }
2051
+ }
2052
+ if (!facts.palette) {
2053
+ problems.push('palette: no [data-chrome="palette"]');
2054
+ } else {
2055
+ const toolbar = facts.toolbar;
2056
+ if (!toolbar || attribute(toolbar, "role") !== "toolbar") {
2057
+ problems.push('palette: .djs-palette-entries has no role="toolbar"');
2058
+ } else if (!attribute(toolbar, "aria-label")) {
2059
+ problems.push("palette: the toolbar has no aria-label");
2060
+ }
2061
+ for (const entry of facts.entries) {
2062
+ const action = attribute(entry, "data-action") ?? entry.description;
2063
+ if (attribute(entry, "role") !== "button")
2064
+ problems.push(`palette: entry "${action}" has no role="button"`);
2065
+ if (!attribute(entry, "aria-label"))
2066
+ problems.push(`palette: entry "${action}" has no aria-label (title)`);
2067
+ const tabindex = attribute(entry, "tabindex");
2068
+ if (tabindex !== "0" && tabindex !== "-1") {
2069
+ problems.push(`palette: entry "${action}" has tabindex "${tabindex ?? ""}", expected roving 0/-1`);
2070
+ }
2071
+ }
2072
+ const stops = facts.entries.filter((entry) => attribute(entry, "tabindex") === "0");
2073
+ if (facts.entries.length > 0 && stops.length !== 1) {
2074
+ problems.push(
2075
+ `palette: ${stops.length} entries with tabindex="0", expected exactly one (roving tabindex)`
2076
+ );
2077
+ }
2078
+ const tools = facts.entries.filter(
2079
+ (entry) => entry.group === "tools" && (attribute(entry, "data-action") ?? "").endsWith("-tool")
2080
+ );
2081
+ for (const tool of tools) {
2082
+ const pressed2 = attribute(tool, "aria-pressed");
2083
+ if (pressed2 !== "true" && pressed2 !== "false") {
2084
+ problems.push(`palette: tool "${attribute(tool, "data-action")}" has no aria-pressed`);
2085
+ }
2086
+ }
2087
+ const pressed = tools.filter((tool) => attribute(tool, "aria-pressed") === "true");
2088
+ if (pressed.length > 1) problems.push(`palette: ${pressed.length} tools are aria-pressed at once`);
2089
+ }
2090
+ const zoom = facts.zoom;
2091
+ if (!zoom) {
2092
+ problems.push('zoom: no [data-chrome="zoom"]');
2093
+ } else {
2094
+ if (attribute(zoom, "role") !== "group") problems.push('zoom: the panel has no role="group"');
2095
+ if (!attribute(zoom, "aria-label")) problems.push("zoom: the panel has no aria-label");
2096
+ const shortcuts = expectedKeyShortcuts(facts.platform);
2097
+ for (const button of ["out", "actual", "in", "fit"]) {
2098
+ const control = facts.zoomButtons[button];
2099
+ if (!control) {
2100
+ problems.push(`zoom: no [data-zoom="${button}"] button`);
2101
+ continue;
2102
+ }
2103
+ if (control.tag !== "button") problems.push(`zoom: [data-zoom="${button}"] is a <${control.tag}>`);
2104
+ if (!name(control)) problems.push(`zoom: [data-zoom="${button}"] has no accessible name`);
2105
+ if (attribute(control, "aria-keyshortcuts") !== shortcuts[button]) {
2106
+ problems.push(
2107
+ `zoom: [data-zoom="${button}"] aria-keyshortcuts is "${attribute(control, "aria-keyshortcuts") ?? ""}", expected "${shortcuts[button]}" (${facts.platform})`
2108
+ );
2109
+ }
2110
+ if (control.disabled) {
2111
+ problems.push(
2112
+ `zoom: [data-zoom="${button}"] is natively disabled; aria-disabled keeps the focus (guide \xA75.8)`
2113
+ );
2114
+ }
2115
+ }
2116
+ const readout = facts.zoomButtons.actual;
2117
+ if (readout) {
2118
+ if (!/\d/.test(readout.text))
2119
+ problems.push(`zoom: the readout shows "${readout.text}", expected a percentage`);
2120
+ const label = normalize2(attribute(readout, "aria-label") ?? "");
2121
+ if (!label.includes(readout.text)) {
2122
+ problems.push(
2123
+ `zoom: the readout's aria-label "${label}" does not name the current zoom "${readout.text}"`
2124
+ );
2125
+ }
2126
+ if (attribute(readout, "aria-disabled") === "true")
2127
+ problems.push("zoom: the readout is never disabled");
2128
+ }
2129
+ }
2130
+ const present = new Set(facts.order);
2131
+ const expectedOrder = DOM_ORDER.filter((part) => present.has(part));
2132
+ if (facts.order.join() !== expectedOrder.join()) {
2133
+ problems.push(
2134
+ `DOM order is ${facts.order.join(", ")}; expected ${expectedOrder.join(", ")} (guide \xA75.14)`
2135
+ );
2136
+ }
2137
+ if (!present.has("canvas")) problems.push("canvas: no diagram-js <svg> (.djs-container > svg) in the root");
2138
+ const canvas = facts.canvas;
2139
+ if (canvas && !normalize2(attribute(canvas, "aria-label") ?? "") && !attribute(canvas, "aria-labelledby")) {
2140
+ problems.push(
2141
+ 'canvas: the focusable <svg> has no accessible name (WCAG 4.1.2); shellModule names it role="application" with messages.canvas'
2142
+ );
2143
+ }
2144
+ for (const element of facts.positiveTabindex)
2145
+ problems.push(`positive tabindex on ${element} (guide \xA75.14)`);
2146
+ for (const element of facts.titled) {
2147
+ problems.push(`${element} has a native title; the chrome tooltip replaces it (guide \xA75.6)`);
2148
+ }
2149
+ problems.push(...primaryAriaProblems(facts, host), ...legalAriaProblems(facts, host));
2150
+ return problems;
2151
+ }
2152
+ function optionalIslandProblems(facts, shown) {
2153
+ const problems = [];
2154
+ if (shown.has("problems")) {
2155
+ const pill = facts.problems;
2156
+ if (!pill?.visible) {
2157
+ problems.push("problems: the pill is not visible after show.problems");
2158
+ } else {
2159
+ if (pill.tag !== "button") problems.push(`problems: the pill is a <${pill.tag}>, expected a <button>`);
2160
+ const label = normalize2(attribute(pill, "aria-label") ?? "");
2161
+ if (!label) problems.push("problems: the pill has no aria-label (count and errors)");
2162
+ else if (!label.includes(pill.text)) {
2163
+ problems.push(`problems: the aria-label "${label}" does not contain the visible text "${pill.text}"`);
2164
+ }
2165
+ }
2166
+ }
2167
+ if (shown.has("status")) {
2168
+ const status = facts.status;
2169
+ if (!status?.visible) {
2170
+ problems.push("status: the status box is not visible after show.status");
2171
+ } else {
2172
+ const role = attribute(status, "role");
2173
+ const severity = attribute(status, "data-severity");
2174
+ const expected = severity === "danger" ? "alert" : "status";
2175
+ if (role !== expected) {
2176
+ problems.push(
2177
+ `status: role is "${role ?? ""}" for data-severity="${severity ?? ""}", expected "${expected}"`
2178
+ );
2179
+ }
2180
+ }
2181
+ }
2182
+ if (shown.has("legend")) {
2183
+ const legend = facts.legend;
2184
+ if (!legend?.visible) {
2185
+ problems.push("legend: the legend is not visible after show.legend");
2186
+ } else {
2187
+ if (!facts.legendTitle)
2188
+ problems.push("legend: the card is not labelled by its title (aria-labelledby)");
2189
+ if (!facts.legendClose || !attribute(facts.legendClose, "aria-label")) {
2190
+ problems.push("legend: the close button has no aria-label");
2191
+ }
2192
+ }
2193
+ }
2194
+ if (shown.has("inspector")) {
2195
+ const inspector = facts.inspector;
2196
+ if (!inspector?.visible) {
2197
+ problems.push("inspector: the inspector is not visible after show.inspector");
2198
+ } else {
2199
+ if (inspector.tag !== "aside") problems.push(`inspector: a <${inspector.tag}>, expected an <aside>`);
2200
+ if (!attribute(inspector, "aria-label")) problems.push("inspector: the <aside> has no aria-label");
2201
+ }
2202
+ }
2203
+ return problems;
2204
+ }
2205
+ function tabSequenceProblems(stops, primary = false) {
2206
+ const [first, second, third] = stops;
2207
+ const sequence = first?.kind === "primary" ? [second, third] : [first, second];
2208
+ const [palette, canvas] = sequence;
2209
+ const problems = [];
2210
+ if (primary && first?.kind !== "primary") {
2211
+ problems.push(
2212
+ `tab order: Tab from the menu trigger lands on ${first?.description ?? "nothing"}, expected the primary action (top left, top right, top centre, guide \xA75.14)`
2213
+ );
2214
+ }
2215
+ if (palette?.kind !== "palette") {
2216
+ problems.push(
2217
+ `tab order: Tab from the menu trigger lands on ${palette?.description ?? "nothing"}, expected the palette's roving entry (guide \xA75.14)`
2218
+ );
2219
+ } else if (canvas?.kind !== "canvas") {
2220
+ problems.push(
2221
+ `tab order: Tab from the palette lands on ${canvas?.description ?? "nothing"}, expected the canvas <svg> (an unexpected tab stop, guide \xA75.14)`
2222
+ );
2223
+ }
2224
+ return problems;
2225
+ }
2226
+ function paletteShortcutProblems(entries) {
2227
+ return entries.filter((entry) => entry.chip && !entry.keyShortcuts).map(
2228
+ (entry) => `palette: entry "${entry.action}" shows the shortcut "${entry.chip}" in its tooltip but has no aria-keyshortcuts (guide \xA75.6)`
2229
+ );
2230
+ }
2231
+ function menuKeysAtDocumentProblems(pressed, documentKeys) {
2232
+ const reached = new Set(documentKeys.map((key) => key.key));
2233
+ return pressed.filter((key) => reached.has(key)).map(
2234
+ (key) => `menu: ${key} in the open menu reached a keydown listener on document; a modeler's document keyboard acts on it (stop it at the menu, guide \xA75.4)`
2235
+ );
2236
+ }
2237
+ function menuOpenProblems(facts, focus) {
2238
+ const problems = [];
2239
+ if (facts.expanded !== "true") {
2240
+ problems.push(`menu: aria-expanded is "${facts.expanded ?? ""}" while open, expected "true"`);
2241
+ }
2242
+ const menu = facts.menu;
2243
+ if (!menu) return [...problems, 'menu: no visible role="menu" after opening'];
2244
+ if (!attribute(menu, "aria-label")) problems.push("menu: the dropdown has no aria-label");
2245
+ if (facts.controls && attribute(menu, "id") !== facts.controls) {
2246
+ problems.push(
2247
+ `menu: the visible dropdown is #${attribute(menu, "id") ?? ""}, the trigger controls #${facts.controls}`
2248
+ );
2249
+ }
2250
+ if (facts.items.length === 0) problems.push("menu: the dropdown has no menuitem");
2251
+ facts.items.forEach((item, index) => {
2252
+ const label = item.name || `item ${index + 1}`;
2253
+ if (!item.name) problems.push(`menu: ${label} has no accessible name`);
2254
+ if (item.role !== "menuitem" && item.checked !== "true" && item.checked !== "false") {
2255
+ problems.push(`menu: "${label}" (${item.role}) has aria-checked "${item.checked ?? ""}"`);
2256
+ }
2257
+ if (item.role === "menuitemradio" && !item.group) {
2258
+ problems.push(`menu: radio "${label}" is not in a labelled role="group"`);
2259
+ }
2260
+ if (item.haspopup === "dialog" && !item.name.endsWith("\u2026")) {
2261
+ problems.push(`menu: "${label}" opens a dialog, so its label ends with "\u2026" (U+2026)`);
2262
+ }
2263
+ });
2264
+ if (focus && facts.items.length > 0) {
2265
+ const expected = focus === "first" ? 0 : facts.items.length - 1;
2266
+ if (facts.active !== expected) {
2267
+ problems.push(
2268
+ `menu: focus is on ${facts.active < 0 ? "no entry" : `entry ${facts.active + 1}`}, expected the ${focus} entry`
2269
+ );
2270
+ }
2271
+ }
2272
+ return problems;
2273
+ }
2274
+ function menuClosedProblems(facts, focusOnTrigger) {
2275
+ const problems = [];
2276
+ if (facts.expanded !== "false")
2277
+ problems.push(`menu: aria-expanded is "${facts.expanded ?? ""}" after closing`);
2278
+ if (facts.menu) problems.push("menu: the dropdown is still visible after Escape");
2279
+ if (focusOnTrigger && !facts.activeIsTrigger)
2280
+ problems.push("menu: Escape did not return the focus to the trigger");
2281
+ return problems;
2282
+ }
2283
+ function keyReachProblem(press, reach, host, seen) {
2284
+ const shouldPrevent = reach === "handled" || reach === "canvas";
2285
+ if (host === "ide" && reach !== "free") {
2286
+ if (seen.keys.length > 0) {
2287
+ return `${press} reached a keydown listener on window; in the IDE host the shell stops it on document, or VS Code runs it again (guide \xA713.5)`;
2288
+ }
2289
+ const atDocument = seen.documentKeys ?? [];
2290
+ const prevented2 = atDocument.length === 1 ? atDocument[0]?.defaultPrevented : void 0;
2291
+ if (prevented2 === true && !shouldPrevent) {
2292
+ return `${press} was prevented; the shell only stops it, so the field, dialog or browser still acts (guide \xA713.5)`;
2293
+ }
2294
+ if (prevented2 === false && shouldPrevent) {
2295
+ return `${press} was stopped but not prevented; a handled key also calls preventDefault`;
2296
+ }
2297
+ return void 0;
2298
+ }
2299
+ if (seen.keys.length === 0) {
2300
+ return host === "ide" ? `${press} never reached window; keys the webview does not handle must go on to VS Code (guide \xA713.5)` : `${press} never reached window; outside the IDE host the shell never stops propagation`;
2301
+ }
2302
+ if (seen.keys.length > 1) return `${press} reached window ${seen.keys.length} times`;
2303
+ const prevented = seen.keys[0]?.defaultPrevented ?? false;
2304
+ if (prevented && !shouldPrevent) {
2305
+ return `${press} arrived prevented; ${reach === "free" ? "nothing may handle it" : "the browser, the field or the dialog must act"}`;
2306
+ }
2307
+ if (!prevented && shouldPrevent) {
2308
+ return `${press} arrived unprevented; a handled key calls preventDefault (the browser would zoom the page)`;
2309
+ }
2310
+ return void 0;
2311
+ }
2312
+ function expectedAppearance(requested, themes) {
2313
+ const fallback = themes[0];
2314
+ if (fallback === void 0) throw new Error("@miragon/modeler-shell conformance: harness.themes is empty");
2315
+ const theme = themes.includes(requested.theme) ? requested.theme : fallback;
2316
+ return requested.kontrast ? { theme, kontrast: requested.kontrast } : { theme };
2317
+ }
2318
+ var describeAppearance = (appearance) => appearance.kontrast ? `${appearance.theme} + ${appearance.kontrast}` : appearance.theme;
2319
+ function expectedEmbeddedAppearance(facts, themes) {
2320
+ const fallback = themes[0];
2321
+ if (fallback === void 0) throw new Error("@miragon/modeler-shell conformance: harness.themes is empty");
2322
+ const host = facts.themed.filter((element) => element.relation === "ancestor").at(-1);
2323
+ return host?.kontrast === "hoch" ? { theme: fallback, kontrast: "hoch" } : { theme: fallback };
2324
+ }
2325
+ function appearanceProblems(facts, host, requested, themes, web) {
2326
+ const ownDocument = host !== "embedded";
2327
+ const expected = web?.effective ?? (ownDocument ? expectedAppearance(requested, themes) : expectedEmbeddedAppearance(facts, themes));
2328
+ const carrierTheme = web?.carrier ?? expected.theme;
2329
+ const problems = [];
2330
+ const described = carrierTheme === expected.theme ? describeAppearance(expected) : `${carrierTheme} showing ${describeAppearance(expected)}`;
2331
+ const label = ownDocument ? `requested ${describeAppearance(requested)}, expected ${described}` : `host page ${describeAppearance(requested)}, expected ${described}`;
2332
+ const carrier = ownDocument ? facts.themed.find((element) => element.isDocument && element.theme !== null) : facts.themed.find((element) => element.relation === "self" && element.theme !== null);
2333
+ if (!carrier) {
2334
+ problems.push(
2335
+ ownDocument ? `<html> has no data-cd-theme (${label}); the host always writes it, also "light" (guide \xA713.3)` : `the embedded root has no data-cd-theme (${label}): it takes the host page's mode instead of its own (guide \xA713.3)`
2336
+ );
2337
+ } else {
2338
+ if (carrier.theme !== carrierTheme) {
2339
+ problems.push(`${carrier.description} has data-cd-theme="${carrier.theme}" (${label})`);
2340
+ }
2341
+ if ((carrier.kontrast ?? void 0) !== expected.kontrast) {
2342
+ problems.push(
2343
+ `${carrier.description} has ${carrier.kontrast ? `data-cd-kontrast="${carrier.kontrast}"` : "no data-cd-kontrast"} (${label})`
2344
+ );
2345
+ }
2346
+ }
2347
+ for (const element of facts.themed) {
2348
+ if (element === carrier) continue;
2349
+ const stray = ownDocument ? !element.isDocument || element.theme === null : element.relation === "descendant" || element.relation === "self";
2350
+ if (!stray) continue;
2351
+ const values = [
2352
+ element.theme !== null ? `data-cd-theme="${element.theme}"` : "",
2353
+ element.kontrast !== null ? `data-cd-kontrast="${element.kontrast}"` : ""
2354
+ ].filter(Boolean).join(" ");
2355
+ const sameMode = carrier !== void 0 && element.theme === carrier.theme && element.kontrast === carrier.kontrast;
2356
+ problems.push(
2357
+ element.theme === null ? `${element.description} carries ${values} without data-cd-theme on the same element (guide \xA713.3)` : sameMode ? `${element.description} repeats ${values}: in an own document the attributes stand only on <html> (guide \xA713.5)` : `${element.description} carries ${values}: a second mode inside the modeler is half-dark (guide \xA713.5)`
2358
+ );
2359
+ }
2360
+ if (ownDocument && !facts.documentClasses.includes("cd-shell-document")) {
2361
+ problems.push(
2362
+ "<html> lacks cd-shell-document in an own document (the reset of the webview's host styles)"
2363
+ );
2364
+ }
2365
+ if (!ownDocument && facts.documentClasses.includes("cd-shell-document")) {
2366
+ problems.push(
2367
+ "<html> of a foreign page carries cd-shell-document: the own-document reset restyles the host page"
2368
+ );
2369
+ }
2370
+ const reference = facts.parts[0];
2371
+ if (reference && !reference.roles["--cd-ui-text"]) {
2372
+ problems.push(
2373
+ "--cd-ui-text is empty at the shell root: the vendored cd-tokens.generated.css is not loaded"
2374
+ );
2375
+ }
2376
+ for (const part of facts.parts) {
2377
+ const schemes = part.colorScheme.split(/\s+/);
2378
+ const dark = schemes.includes("dark");
2379
+ const light = schemes.includes("light");
2380
+ if (expected.theme === "light" && (dark || carrier && !light)) {
2381
+ problems.push(`${part.name}: color-scheme is "${part.colorScheme}" (${label})`);
2382
+ }
2383
+ if (expected.theme === "dark" && (!dark || light)) {
2384
+ problems.push(`${part.name}: color-scheme is "${part.colorScheme}" (${label})`);
2385
+ }
2386
+ if (reference && part !== reference) {
2387
+ for (const [role, value] of Object.entries(part.roles)) {
2388
+ if (value !== reference.roles[role]) {
2389
+ problems.push(
2390
+ `${part.name}: ${role} resolves to "${value}", the root to "${reference.roles[role] ?? ""}" (half-dark)`
2391
+ );
2392
+ }
2393
+ }
2394
+ }
2395
+ }
2396
+ for (const required of ["dropdown", "tooltip"]) {
2397
+ if (!facts.parts.some((part) => part.name === required)) {
2398
+ problems.push(`${required}: not visible when the kit opened it, so its mode was not checked`);
2399
+ }
2400
+ }
2401
+ return { problems };
2402
+ }
2403
+ var SHELL_ROOT_ATTRIBUTES = [
2404
+ "data-cd-shell-host",
2405
+ "data-palette",
2406
+ "data-legend",
2407
+ "data-inspector",
2408
+ "data-problems",
2409
+ "data-readonly",
2410
+ "data-primary",
2411
+ "data-zoom-row",
2412
+ "data-empty-state",
2413
+ "data-drop"
2414
+ ];
2415
+ var EMBEDDED_ROOT_ATTRIBUTES = ["data-cd-theme", "data-cd-kontrast"];
2416
+ var BENIGN_ERRORS = /ResizeObserver loop/;
2417
+ function leftoverProblems(facts, host) {
2418
+ const problems = [];
2419
+ for (const element of facts.classedElements) problems.push(`${element} is left after destroy`);
2420
+ const ownDocument = host !== "embedded";
2421
+ for (const name2 of facts.documentClasses) {
2422
+ if (ownDocument && name2 === "cd-shell-document") continue;
2423
+ problems.push(`<html> keeps the class ${name2} after destroy`);
2424
+ }
2425
+ for (const element of facts.chromeElements) problems.push(`${element} is left after destroy`);
2426
+ for (const name2 of facts.rootClasses) problems.push(`the root keeps the class ${name2} after destroy`);
2427
+ const written = ownDocument ? SHELL_ROOT_ATTRIBUTES : [...SHELL_ROOT_ATTRIBUTES, ...EMBEDDED_ROOT_ATTRIBUTES];
2428
+ for (const name2 of facts.rootAttributes) {
2429
+ if (written.includes(name2)) problems.push(`the root keeps ${name2} after destroy`);
2430
+ }
2431
+ for (const error of facts.errors) {
2432
+ if (!BENIGN_ERRORS.test(error)) problems.push(`the page reported an error after destroy: ${error}`);
2433
+ }
2434
+ return problems;
2435
+ }
2436
+ function afterDestroyKeyProblem(press, recorded) {
2437
+ if (recorded.length === 0)
2438
+ return `${press} after destroy never reached window: a shell listener still stops it`;
2439
+ if (recorded.some((key) => key.defaultPrevented)) {
2440
+ return `${press} after destroy arrived prevented: a shell listener is still installed`;
2441
+ }
2442
+ return void 0;
2443
+ }
2444
+ var ABSENT_ISLANDS = ["legend", "inspector", "problems", "status"];
2445
+ function absentIslands(absent) {
2446
+ return ABSENT_ISLANDS.filter((island) => absent?.includes(island));
2447
+ }
2448
+ function absentIslandProblems(visible, absent) {
2449
+ return absentIslands(absent).filter((island) => visible.has(island)).map((island) => `${island}: visible, but harness.absent says the modeler has no ${island}`);
2450
+ }
2451
+ function harnessHookProblems(harness, hooks) {
2452
+ return hooks.filter(
2453
+ (hook) => hook === "drop" ? harness.absent?.includes("drop") && harness.drop : hook !== "problemSelection" && harness.absent?.includes(hook) && harness.show?.[hook]
2454
+ ).map(
2455
+ (hook) => hook === "drop" ? 'harness: absent lists "drop", but harness.drop is given' : `harness: absent lists "${hook}", but show.${hook} is given`
2456
+ );
2457
+ }
2458
+ function surroundProblems(facts) {
2459
+ if (!facts.container) return ["canvas surround: no .djs-container in the root to check"];
2460
+ if (!facts.painted) {
2461
+ return [
2462
+ "canvas surround: nothing from .djs-container out to the root paints a background colour; the surround is var(--cd-ui-grund) (guide \xA710.1)"
2463
+ ];
2464
+ }
2465
+ if (facts.painted.color === facts.grund) return [];
2466
+ return [
2467
+ `canvas surround: ${facts.painted.description} paints ${facts.painted.color}, but --cd-ui-grund resolves to ${facts.grund}; the surround follows the mode (guide \xA710.1)`
2468
+ ];
2469
+ }
2470
+ function isToolEntry(entry) {
2471
+ return entry.group === "tools" || entry.action.endsWith("-tool");
2472
+ }
2473
+ function paletteEntryState(entry) {
2474
+ if (entry.disabled) return "aria-disabled";
2475
+ if (entry.hovered) return entry.pressed ? "pressed and hovered" : "hovered";
2476
+ return entry.pressed ? "pressed" : "at rest";
2477
+ }
2478
+ function paletteColorProblems(facts) {
2479
+ const problems = [];
2480
+ for (const entry of facts.entries) {
2481
+ if (!isToolEntry(entry)) continue;
2482
+ const [role, value] = entry.disabled ? ["--cd-ui-text-leise", facts.roles.textLeise] : entry.hovered ? ["--cd-ui-interaktion", facts.roles.interaktion] : ["--cd-ui-text", facts.roles.text];
2483
+ const state = paletteEntryState(entry);
2484
+ if (entry.color !== value) {
2485
+ problems.push(
2486
+ `palette: tool "${entry.action}" ${state} has color ${entry.color}, expected ${role} (${value}) (guide \xA75.5)`
2487
+ );
2488
+ }
2489
+ if (entry.icon && entry.icon.color !== value) {
2490
+ const icon = entry.icon.kind === "svg" ? "<svg> icon" : "::before icon";
2491
+ problems.push(
2492
+ `palette: the ${icon} of tool "${entry.action}" ${state} has color ${entry.icon.color}, expected ${role} (${value}); a renderer rule overrides the skin (guide \xA75.5)`
2493
+ );
2494
+ }
2495
+ }
2496
+ return problems;
2497
+ }
2498
+ var round3 = (value) => Math.round(value * 10) / 10;
2499
+ function createProblems(facts) {
2500
+ const known = new Set(facts.before.map((shape2) => shape2.id));
2501
+ const added = facts.after.filter((shape2) => !known.has(shape2.id));
2502
+ const pressed = `Enter on palette entry "${facts.entry}"`;
2503
+ if (added.length === 0) {
2504
+ return [
2505
+ `create: ${pressed} added no element; Enter on a creating entry places it in the centre of the free rectangle (guide \xA75.5)`
2506
+ ];
2507
+ }
2508
+ if (added.length > 1) {
2509
+ return [
2510
+ `create: ${pressed} added ${added.length} elements (${added.map((shape2) => shape2.id).join(", ")}), expected exactly one`
2511
+ ];
2512
+ }
2513
+ const [shape] = added;
2514
+ const problems = [];
2515
+ if (!shape.selected) problems.push(`create: the new element "${shape.id}" is not selected (guide \xA75.5)`);
2516
+ if (!facts.canvas.focusInCanvas) {
2517
+ problems.push(
2518
+ `create: after ${pressed} the focus is on ${facts.canvas.focused}, expected the canvas (its <svg> or the label editor, guide \xA75.5)`
2519
+ );
2520
+ }
2521
+ const centre = { x: shape.box.left + shape.box.width / 2, y: shape.box.top + shape.box.height / 2 };
2522
+ const { free } = facts;
2523
+ const tolerance = GEOMETRY_TOLERANCE_PX + 1e-6;
2524
+ const inside = centre.x >= free.left - tolerance && centre.x <= free.right + tolerance && centre.y >= free.top - tolerance && centre.y <= free.bottom + tolerance;
2525
+ if (!inside) {
2526
+ problems.push(
2527
+ `create: the new element "${shape.id}" is centred at (${round3(centre.x)}, ${round3(centre.y)})px, outside the free rectangle ${round3(free.left)}\u2013${round3(free.right)} \xD7 ${round3(free.top)}\u2013${round3(free.bottom)}px (guide \xA75.5, \xA75.9)`
2528
+ );
2529
+ }
2530
+ return problems;
2531
+ }
2532
+ function readOnlyStateProblems(facts) {
2533
+ const problems = [];
2534
+ const after = `${INVALID_STATUS_DELAY_MS}ms after show.invalid`;
2535
+ if (!facts.readOnly) problems.push(`read-only: the root has no data-readonly ${after}`);
2536
+ const status = facts.status;
2537
+ if (!status?.visible) {
2538
+ problems.push(`read-only: the status box is not visible ${after} (guide \xA75.12)`);
2539
+ } else if (status.attributes.role !== "alert") {
2540
+ problems.push(
2541
+ `read-only: the status box of an invalid file has role "${status.attributes.role ?? ""}", expected "alert" (guide \xA713.5)`
2542
+ );
2543
+ }
2544
+ return problems;
2545
+ }
2546
+ var READ_ONLY_PALETTE_ACTIONS = ["lasso-tool", "hand-tool"];
2547
+ function readOnlyPaletteProblems(entries) {
2548
+ return entries.flatMap((entry) => {
2549
+ const staysActive = READ_ONLY_PALETTE_ACTIONS.includes(entry.action);
2550
+ if (staysActive && entry.disabled) {
2551
+ return [
2552
+ `palette: "${entry.action}" is aria-disabled on the read-only canvas; Lasso and Hand stay active (guide \xA713.5)`
2553
+ ];
2554
+ }
2555
+ if (!staysActive && !entry.disabled) {
2556
+ return [`palette: "${entry.action}" is not aria-disabled on the read-only canvas (guide \xA713.5)`];
2557
+ }
2558
+ return [];
2559
+ });
2560
+ }
2561
+ var LEGEND_MENU_ID = "legend";
2562
+ function readOnlyMenuProblems(before, after) {
2563
+ const problems = [];
2564
+ const key = (entry) => `${entry.menuId ?? ""}\0${entry.name}`;
2565
+ const enabledBefore = new Set(before.filter((entry) => !entry.disabled).map(key));
2566
+ for (const entry of after) {
2567
+ if (!entry.disabled) continue;
2568
+ if (entry.menuId === LEGEND_MENU_ID && entry.role === "menuitemcheckbox") {
2569
+ problems.push(
2570
+ "menu: the Legend toggle is aria-disabled on the read-only canvas; a view-only toggle stays usable (guide \xA713.5)"
2571
+ );
2572
+ } else if (entry.group !== "model" && enabledBefore.has(key(entry))) {
2573
+ problems.push(
2574
+ `menu: "${entry.name}" is aria-disabled on the read-only canvas; export and help stay usable (guide \xA713.5)`
2575
+ );
2576
+ }
2577
+ }
2578
+ return problems;
2579
+ }
2580
+ function lockedShapeProblems(facts) {
2581
+ const problems = [];
2582
+ const { before, after } = facts.dragged;
2583
+ if (facts.contextPadEntries.length > 0) {
2584
+ problems.push(
2585
+ `read-only: selecting "${before.id}" shows the context pad (${facts.contextPadEntries.join(", ")}) (guide \xA713.5)`
2586
+ );
2587
+ }
2588
+ if (!after) {
2589
+ problems.push(`read-only: a 40px drag removed "${before.id}" from the canvas (guide \xA713.5)`);
2590
+ } else if (after.transform !== before.transform || Math.abs(after.size.width - before.size.width) > 0.5 || Math.abs(after.size.height - before.size.height) > 0.5) {
2591
+ problems.push(
2592
+ `read-only: a 40px drag changed "${before.id}" (transform "${before.transform}" \u2192 "${after.transform}", ${round3(before.size.width)} \xD7 ${round3(before.size.height)} \u2192 ${round3(after.size.width)} \xD7 ${round3(after.size.height)}) (guide \xA713.5)`
2593
+ );
2594
+ }
2595
+ if (facts.textFields.length > 0) {
2596
+ problems.push(
2597
+ `read-only: a double-click on "${before.id}" opens a text field (${facts.textFields.join(", ")}) (guide \xA713.5)`
2598
+ );
2599
+ }
2600
+ for (const key of ["Delete", "Backspace"]) {
2601
+ if (!facts.kept[key])
2602
+ problems.push(`read-only: ${key} on the canvas deleted "${before.id}" (guide \xA713.5)`);
2603
+ }
2604
+ return problems;
2605
+ }
2606
+
2607
+ // src/testing/case-tools.ts
2608
+ var DEFAULT_SIZES = [
2609
+ { width: 1280, height: 900 },
2610
+ { width: 720, height: 700 },
2611
+ { width: 420, height: 700 }
2612
+ ];
2613
+ var LIGHT = { theme: "light" };
2614
+ var SHOWN_ISLANDS = ["legend", "problems", "status", "inspector"];
2615
+ var POLL_MS = 50;
2616
+ var HOVER_MARGIN_MS = 200;
2617
+ var PARK = { x: 1, y: 1 };
2618
+ function sizeLabel(size) {
2619
+ return `${size.width} \xD7 ${size.height}`;
2620
+ }
2621
+ function fail(caseName, host, problems) {
2622
+ const unique = [...new Set(problems)];
2623
+ throw new Error(
2624
+ `@miragon/modeler-shell conformance: ${caseName} (${host} host)
2625
+ ${unique.map((problem) => `- ${problem}`).join("\n")}`
2626
+ );
2627
+ }
2628
+ function finish(caseName, host, problems, skipped) {
2629
+ if (problems.length > 0) fail(caseName, host, problems);
2630
+ const seen = /* @__PURE__ */ new Set();
2631
+ const unique = [...skipped].filter((skip) => !seen.has(skip.reason) && seen.add(skip.reason));
2632
+ return unique.length > 0 ? { skipped: unique } : void 0;
2633
+ }
2634
+ function assertHooksConsistent(caseName, harness, hooks) {
2635
+ const problems = harnessHookProblems(harness, hooks);
2636
+ if (problems.length > 0) fail(caseName, harness.host, problems);
2637
+ }
2638
+ function unfed(harness, hook) {
2639
+ return !harness.show?.[hook] && !harness.absent?.includes(hook);
2640
+ }
2641
+ async function openHarness(page, harness, context) {
2642
+ if (harness.host === "web" && page.emulateMedia) {
2643
+ await page.emulateMedia({
2644
+ colorScheme: context.requested.theme,
2645
+ contrast: context.requested.kontrast === "hoch" ? "more" : "no-preference"
2646
+ });
2647
+ }
2648
+ await harness.open(page, context);
2649
+ }
2650
+ function webHookSkips(harness, hooks) {
2651
+ return Object.entries(hooks).filter(([hook]) => hook === "toast" ? harness.host !== "ide" : harness.host === "web").filter(([hook]) => unfed(harness, hook)).map(([hook, what]) => ({ hook, reason: `show.${hook} is undefined: ${what} not verified` }));
2652
+ }
2653
+ async function pollUntil(page, read, done, timeoutMs) {
2654
+ let value = await read();
2655
+ for (let waited = 0; !done(value) && waited < timeoutMs; waited += POLL_MS) {
2656
+ await page.waitForTimeout(POLL_MS);
2657
+ value = await read();
2658
+ }
2659
+ return value;
2660
+ }
2661
+ async function hover(page, root, target) {
2662
+ const anchor = await findTarget(page, root, target);
2663
+ if (!anchor) return null;
2664
+ await page.mouse.move(PARK.x, PARK.y);
2665
+ await page.mouse.move(anchor.client.x, anchor.client.y);
2666
+ await page.waitForTimeout(TOOLTIP_DELAY_MS + HOVER_MARGIN_MS);
2667
+ await settle(page);
2668
+ return anchor;
2669
+ }
2670
+ async function openMenuByClick(page, root) {
2671
+ const trigger = await findTarget(page, root, "menu");
2672
+ if (!trigger) return false;
2673
+ await page.mouse.click(trigger.client.x, trigger.client.y);
2674
+ await settle(page);
2675
+ return true;
2676
+ }
2677
+ async function closeByEscape(page) {
2678
+ await page.keyboard.press("Escape");
2679
+ await settle(page);
2680
+ }
2681
+ async function menuEntries(page, root) {
2682
+ if (!await openMenuByClick(page, root)) return [];
2683
+ const entries = await inPage(page, menuEntriesProbe, { root }) ?? [];
2684
+ await closeByEscape(page);
2685
+ return entries;
2686
+ }
2687
+ function shapes(page, root) {
2688
+ return inPage(page, shapesProbe, { root });
2689
+ }
2690
+
2691
+ // src/testing/web-facts.ts
2692
+ var webModeProbe = ({ key }) => {
2693
+ const html = document.documentElement;
2694
+ let stored = null;
2695
+ let storageBlocked = false;
2696
+ try {
2697
+ stored = localStorage.getItem(key);
2698
+ } catch {
2699
+ storageBlocked = true;
2700
+ }
2701
+ return {
2702
+ theme: html.getAttribute("data-cd-theme"),
2703
+ kontrast: html.getAttribute("data-cd-kontrast"),
2704
+ colorScheme: document.querySelector('meta[name="color-scheme"]')?.getAttribute("content") ?? null,
2705
+ themeColors: [...document.querySelectorAll('meta[name="theme-color"]')].map((meta) => ({
2706
+ content: meta.getAttribute("content") ?? "",
2707
+ media: meta.getAttribute("media")
2708
+ })),
2709
+ stored,
2710
+ storageBlocked,
2711
+ noTransition: html.classList.contains("cd-shell-no-transition")
2712
+ };
2713
+ };
2714
+ var watchModeProbe = (_) => {
2715
+ const tools = window.__modelerShellConformance;
2716
+ tools.modeWatch?.stop();
2717
+ const html = document.documentElement;
2718
+ const changes = [];
2719
+ let frame = 0;
2720
+ let running = true;
2721
+ const tick = () => {
2722
+ if (!running) return;
2723
+ frame += 1;
2724
+ requestAnimationFrame(tick);
2725
+ };
2726
+ requestAnimationFrame(tick);
2727
+ const observer = new MutationObserver((records) => {
2728
+ for (const record of records) {
2729
+ const target = record.target;
2730
+ if (target === html) {
2731
+ if (record.attributeName === "class") {
2732
+ const had = (record.oldValue ?? "").split(/\s+/).includes("cd-shell-no-transition");
2733
+ const has = html.classList.contains("cd-shell-no-transition");
2734
+ if (had !== has) changes.push({ part: "no-transition", value: has ? "on" : "off", frame });
2735
+ } else if (record.attributeName === "data-cd-theme") {
2736
+ changes.push({ part: "theme", value: html.getAttribute("data-cd-theme"), frame });
2737
+ } else if (record.attributeName === "data-cd-kontrast") {
2738
+ changes.push({ part: "kontrast", value: html.getAttribute("data-cd-kontrast"), frame });
2739
+ }
2740
+ } else if (target.localName === "meta") {
2741
+ const name2 = target.getAttribute("name");
2742
+ const media = target.getAttribute("media");
2743
+ changes.push({
2744
+ part: name2 === "color-scheme" ? "color-scheme" : "theme-color",
2745
+ value: `${target.getAttribute("content") ?? ""}${media ? ` ${media}` : ""}`,
2746
+ frame
2747
+ });
2748
+ }
2749
+ }
2750
+ });
2751
+ observer.observe(html, {
2752
+ attributes: true,
2753
+ attributeOldValue: true,
2754
+ attributeFilter: ["data-cd-theme", "data-cd-kontrast", "class"]
2755
+ });
2756
+ for (const meta of document.querySelectorAll('meta[name="color-scheme"], meta[name="theme-color"]')) {
2757
+ observer.observe(meta, { attributes: true, attributeFilter: ["content"] });
2758
+ }
2759
+ tools.modeWatch = {
2760
+ changes,
2761
+ stop() {
2762
+ running = false;
2763
+ observer.disconnect();
2764
+ }
2765
+ };
2766
+ };
2767
+ var modeChangesProbe = (_) => {
2768
+ const tools = window.__modelerShellConformance;
2769
+ const watch = tools.modeWatch;
2770
+ if (!watch) return [];
2771
+ watch.stop();
2772
+ tools.modeWatch = void 0;
2773
+ return [...watch.changes];
2774
+ };
2775
+ var legalLineProbe = ({ root }) => {
2776
+ const tools = window.__modelerShellConformance;
2777
+ const rootElement = document.querySelector(root);
2778
+ if (!rootElement) return null;
2779
+ const legal = rootElement.querySelector('[data-chrome="legal"]');
2780
+ return {
2781
+ container: tools.container(rootElement),
2782
+ legal: legal && tools.visible(legal) ? tools.box(legal, rootElement) : null,
2783
+ links: [...legal?.querySelectorAll("a") ?? []].map((link) => ({
2784
+ text: (link.textContent ?? "").replace(/\s+/g, " ").trim(),
2785
+ box: tools.box(link, rootElement)
2786
+ }))
2787
+ };
2788
+ };
2789
+ var topRowProbe = ({ root }) => {
2790
+ const tools = window.__modelerShellConformance;
2791
+ const rootElement = document.querySelector(root);
2792
+ if (!rootElement) return null;
2793
+ const palette = rootElement.querySelector('[data-chrome="palette"]');
2794
+ const menu = rootElement.querySelector('[data-chrome="menu"]');
2795
+ const primary = rootElement.querySelector('[data-chrome="primary"]');
2796
+ const menuBox = menu && tools.visible(menu) ? tools.box(menu, rootElement) : null;
2797
+ let primaryFacts = null;
2798
+ if (primary && tools.visible(primary)) {
2799
+ const rect = primary.getBoundingClientRect();
2800
+ const labels = [...primary.querySelectorAll("span")].filter(
2801
+ (span) => tools.visible(span) && (span.textContent ?? "").trim() !== ""
2802
+ );
2803
+ primaryFacts = {
2804
+ box: tools.box(primary, rootElement),
2805
+ client: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
2806
+ collapsed: rootElement.getAttribute("data-primary") === "collapsed",
2807
+ ariaLabel: primary.getAttribute("aria-label"),
2808
+ describedBy: primary.getAttribute("aria-describedby"),
2809
+ label: labels.length > 0 ? labels.map((span) => (span.textContent ?? "").trim()).join(" ") : null
2810
+ };
2811
+ }
2812
+ return {
2813
+ container: tools.container(rootElement),
2814
+ paletteRow: rootElement.getAttribute("data-palette") === "second-row" ? "second-row" : "top",
2815
+ paletteWidth: palette ? palette.offsetWidth - palette.clientWidth + palette.scrollWidth : null,
2816
+ menuRight: menuBox ? menuBox.left + menuBox.width : null,
2817
+ primary: primaryFacts
2818
+ };
2819
+ };
2820
+ var countClicksProbe = ({ root, selector }) => {
2821
+ const tools = window.__modelerShellConformance;
2822
+ const element = document.querySelector(root)?.querySelector(selector);
2823
+ if (!element) return false;
2824
+ tools.clicks = 0;
2825
+ const listener = () => {
2826
+ tools.clicks += 1;
2827
+ };
2828
+ element.addEventListener("click", listener, true);
2829
+ tools.cleanups.push(() => element.removeEventListener("click", listener, true));
2830
+ return true;
2831
+ };
2832
+ var clicksProbe = (_) => window.__modelerShellConformance.clicks;
2833
+ var emptyStateProbe = ({
2834
+ root,
2835
+ work
2836
+ }) => {
2837
+ const tools = window.__modelerShellConformance;
2838
+ const rootElement = document.querySelector(root);
2839
+ if (!rootElement) return null;
2840
+ const card = rootElement.querySelector('[data-chrome="empty-state"]');
2841
+ const image = card?.querySelector("img") ?? null;
2842
+ const heading = card?.querySelector("h1, h2, h3, h4, h5, h6") ?? null;
2843
+ const labelledBy = card?.getAttribute("aria-labelledby");
2844
+ const parts = work.map((name2) => [
2845
+ name2,
2846
+ rootElement.querySelector(`[data-chrome="${name2}"]`)
2847
+ ]);
2848
+ parts.push(["hint", rootElement.querySelector(".cd-shell-hint")]);
2849
+ const legal = rootElement.querySelector('[data-chrome="legal"]');
2850
+ return {
2851
+ shown: rootElement.getAttribute("data-empty-state") === "shown",
2852
+ card: card ? {
2853
+ visible: tools.visible(card),
2854
+ box: tools.box(card, rootElement),
2855
+ tag: card.tagName.toLowerCase(),
2856
+ labelledBy: labelledBy ? labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim() ?? "").join(" ").trim() : null
2857
+ } : null,
2858
+ komet: image ? {
2859
+ tag: image.tagName.toLowerCase(),
2860
+ alt: image.getAttribute("alt"),
2861
+ width: image.getBoundingClientRect().width,
2862
+ height: image.getBoundingClientRect().height
2863
+ } : null,
2864
+ heading: heading ? { tag: heading.tagName.toLowerCase(), text: (heading.textContent ?? "").replace(/\s+/g, " ").trim() } : null,
2865
+ // The card's actions; a text link inside the hint paragraph is no action (guide §8).
2866
+ buttons: [...card?.querySelectorAll("button") ?? []].filter((button) => tools.visible(button) && !button.closest("p")).map((button) => {
2867
+ const rect = button.getBoundingClientRect();
2868
+ return {
2869
+ variant: button.getAttribute("data-variant"),
2870
+ name: (button.getAttribute("aria-label") ?? button.textContent ?? "").replace(/\s+/g, " ").trim(),
2871
+ height: rect.height,
2872
+ client: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
2873
+ };
2874
+ }),
2875
+ hint: [...card?.querySelectorAll("p") ?? []].map((paragraph) => (paragraph.textContent ?? "").trim()).filter(Boolean).at(-1) ?? null,
2876
+ work: parts.filter((part) => part[1] !== null).map(([name2, element]) => ({
2877
+ name: name2,
2878
+ visibility: getComputedStyle(element).visibility,
2879
+ visible: tools.visible(element)
2880
+ })),
2881
+ canvasInert: rootElement.querySelector(".cd-shell-canvas")?.hasAttribute("inert") ?? false,
2882
+ legalVisible: legal ? tools.visible(legal) : false
2883
+ };
2884
+ };
2885
+ var focusOwnerProbe = ({ root }) => {
2886
+ const tools = window.__modelerShellConformance;
2887
+ const active = document.activeElement;
2888
+ const rootElement = document.querySelector(root);
2889
+ if (!active || active === document.body || active === document.documentElement) {
2890
+ return { owner: "body", description: "<body>" };
2891
+ }
2892
+ const description = tools.describe(active);
2893
+ if (!rootElement?.contains(active)) return { owner: "outside", description };
2894
+ if (active.closest(".djs-container")) return { owner: "canvas", description };
2895
+ const island = active.closest("[data-chrome]")?.getAttribute("data-chrome");
2896
+ return { owner: island ?? "other", description };
2897
+ };
2898
+ var blurProbe = (_) => {
2899
+ document.activeElement?.blur?.();
2900
+ };
2901
+ var toastsProbe = ({ root }) => {
2902
+ const tools = window.__modelerShellConformance;
2903
+ const rootElement = document.querySelector(root);
2904
+ if (!rootElement) return null;
2905
+ const stack = rootElement.querySelector('[data-chrome="toasts"]');
2906
+ const open = (element) => {
2907
+ try {
2908
+ return element.matches(":popover-open");
2909
+ } catch {
2910
+ return false;
2911
+ }
2912
+ };
2913
+ const toasts2 = stack ? [...stack.querySelectorAll(":scope > [data-status]")].filter(
2914
+ (toast) => tools.visible(toast)
2915
+ ) : [];
2916
+ const name2 = (element) => (element.getAttribute("aria-label") ?? element.textContent ?? "").replace(/\s+/g, " ").trim();
2917
+ const centre = (element) => {
2918
+ const rect = element.getBoundingClientRect();
2919
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
2920
+ };
2921
+ return {
2922
+ stack: stack ? {
2923
+ visible: tools.visible(stack),
2924
+ box: tools.box(stack, rootElement),
2925
+ popover: stack.getAttribute("popover"),
2926
+ popoverOpen: open(stack)
2927
+ } : null,
2928
+ toasts: toasts2.map((toast) => {
2929
+ const point = centre(toast);
2930
+ const hit = document.elementFromPoint(point.x, point.y);
2931
+ return {
2932
+ text: (toast.textContent ?? "").replace(/\s+/g, " ").trim(),
2933
+ role: toast.getAttribute("role"),
2934
+ status: toast.getAttribute("data-status"),
2935
+ box: tools.box(toast, rootElement),
2936
+ client: point,
2937
+ buttons: [...toast.querySelectorAll("button")].filter((button) => tools.visible(button)).map((button) => ({
2938
+ name: name2(button),
2939
+ text: (button.textContent ?? "").replace(/\s+/g, " ").trim(),
2940
+ client: centre(button)
2941
+ })),
2942
+ topmost: hit !== null && toast.contains(hit),
2943
+ focused: toast.contains(document.activeElement)
2944
+ };
2945
+ }),
2946
+ strays: [...document.querySelectorAll(".cd-shell-toast")].filter((element) => !stack?.contains(element) && tools.visible(element)).map((element) => tools.describe(element)),
2947
+ live: [...rootElement.querySelectorAll(".cd-shell-live [role], .cd-shell-live [aria-live]")].map(
2948
+ (region) => ({
2949
+ role: region.getAttribute("role"),
2950
+ live: region.getAttribute("aria-live"),
2951
+ text: (region.textContent ?? "").replace(/\s+/g, " ").trim()
2952
+ })
2953
+ )
2954
+ };
2955
+ };
2956
+ var focusToastButtonProbe = ({
2957
+ root,
2958
+ text,
2959
+ name: name2
2960
+ }) => {
2961
+ const stack = document.querySelector(root)?.querySelector('[data-chrome="toasts"]');
2962
+ const toast = [...stack?.querySelectorAll(":scope > [data-status]") ?? []].find(
2963
+ (candidate) => (candidate.textContent ?? "").includes(text)
2964
+ );
2965
+ const button = [...toast?.querySelectorAll("button") ?? []].find(
2966
+ (candidate) => (candidate.getAttribute("aria-label") ?? candidate.textContent ?? "").trim() === name2
2967
+ );
2968
+ if (!button) return false;
2969
+ button.focus();
2970
+ return document.activeElement === button;
2971
+ };
2972
+ var watchToastStackProbe = ({ root }) => {
2973
+ const tools = window.__modelerShellConformance;
2974
+ const stack = document.querySelector(root)?.querySelector('[data-chrome="toasts"]');
2975
+ if (!stack) return false;
2976
+ const seen = [];
2977
+ const listener = (event) => seen.push(event.newState);
2978
+ stack.addEventListener("beforetoggle", listener);
2979
+ tools.cleanups.push(() => stack.removeEventListener("beforetoggle", listener));
2980
+ tools.toastToggles = seen;
2981
+ return true;
2982
+ };
2983
+ var toastStackTogglesProbe = ({ root }) => {
2984
+ const tools = window.__modelerShellConformance;
2985
+ const stack = document.querySelector(root)?.querySelector('[data-chrome="toasts"]');
2986
+ let open = false;
2987
+ try {
2988
+ open = stack?.matches(":popover-open") ?? false;
2989
+ } catch {
2990
+ open = false;
2991
+ }
2992
+ return { toggles: [...tools.toastToggles ?? []], open };
2993
+ };
2994
+ var showModalProbe = (_) => {
2995
+ const tools = window.__modelerShellConformance;
2996
+ const dialog = document.createElement("dialog");
2997
+ dialog.setAttribute("data-modeler-shell-conformance", "show-modal");
2998
+ dialog.setAttribute("aria-label", "Conformance stand-in");
2999
+ dialog.style.cssText = "width:60vw;height:60vh;max-width:none;max-height:none;";
3000
+ document.body.append(dialog);
3001
+ tools.injected.push(dialog);
3002
+ dialog.showModal();
3003
+ try {
3004
+ return dialog.matches(":modal");
3005
+ } catch {
3006
+ return dialog.open;
3007
+ }
3008
+ };
3009
+ var popoverProbe = ({ root }) => {
3010
+ const tools = window.__modelerShellConformance;
3011
+ const rootElement = document.querySelector(root);
3012
+ if (!rootElement) return null;
3013
+ const pill = rootElement.querySelector('[data-chrome="problems"]');
3014
+ const controls = pill?.getAttribute("aria-controls");
3015
+ const popover2 = (controls ? document.getElementById(controls) : null) ?? rootElement.querySelector('[data-chrome="problems-popover"]');
3016
+ const attributes = (element) => {
3017
+ const all = {};
3018
+ for (const name2 of element.getAttributeNames()) all[name2] = element.getAttribute(name2) ?? "";
3019
+ return all;
3020
+ };
3021
+ const labelledBy = popover2?.getAttribute("aria-labelledby");
3022
+ const active = document.activeElement;
3023
+ const rows = popover2 ? [...popover2.querySelectorAll(".cd-shell-problem")].filter((row) => tools.visible(row)) : [];
3024
+ const pillRect = pill?.getBoundingClientRect();
3025
+ return {
3026
+ pill: pill && pillRect ? {
3027
+ attributes: attributes(pill),
3028
+ box: tools.box(pill, rootElement),
3029
+ client: { x: pillRect.left + pillRect.width / 2, y: pillRect.top + pillRect.height / 2 },
3030
+ visible: tools.visible(pill)
3031
+ } : null,
3032
+ popover: popover2 ? {
3033
+ attributes: attributes(popover2),
3034
+ box: tools.box(popover2, rootElement),
3035
+ visible: tools.visible(popover2),
3036
+ title: labelledBy ? labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim() ?? "").join(" ").trim() : null
3037
+ } : null,
3038
+ rows: rows.map((row) => {
3039
+ const target = row.matches("button") ? row : row.querySelector("button");
3040
+ const rect = (target ?? row).getBoundingClientRect();
3041
+ return {
3042
+ button: target !== null,
3043
+ height: row.getBoundingClientRect().height,
3044
+ icon: row.querySelector("svg") !== null,
3045
+ text: (row.textContent ?? "").replace(/\s+/g, " ").trim(),
3046
+ client: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
3047
+ };
3048
+ }),
3049
+ focus: !active || active === document.body ? "body" : pill && active === pill ? "pill" : popover2?.contains(active) ? "popover" : active.closest(".djs-container") ? "canvas" : "other",
3050
+ focused: !active || active === document.body ? "<body>" : tools.describe(active),
3051
+ focusedRow: (() => {
3052
+ const index = active ? rows.findIndex((row2) => row2 === active || row2.contains(active)) : -1;
3053
+ const row = rows[index];
3054
+ if (!row) return null;
3055
+ let scroller = row.parentElement;
3056
+ while (scroller && scroller !== popover2 && scroller.scrollHeight <= scroller.clientHeight) {
3057
+ scroller = scroller.parentElement;
3058
+ }
3059
+ const view = (scroller ?? popover2 ?? row).getBoundingClientRect();
3060
+ const box = row.getBoundingClientRect();
3061
+ return { index, inView: box.top >= view.top - 1 && box.bottom <= view.bottom + 1 };
3062
+ })()
3063
+ };
3064
+ };
3065
+ var dragProbe = async (spec) => {
3066
+ const tools = window.__modelerShellConformance;
3067
+ const rootElement = document.querySelector(spec.root);
3068
+ if (!rootElement) return null;
3069
+ const transfer = new DataTransfer();
3070
+ for (const file of spec.files) {
3071
+ transfer.items.add(new File([file.text], file.name, { type: file.type }));
3072
+ }
3073
+ for (const text of spec.strings ?? []) transfer.items.add(text, "text/plain");
3074
+ const rect = rootElement.getBoundingClientRect();
3075
+ const clientX = rect.left + rect.width / 2;
3076
+ const clientY = rect.top + rect.height / 2;
3077
+ const target = document.elementFromPoint(clientX, clientY) ?? rootElement;
3078
+ const events = [];
3079
+ for (const type of spec.steps) {
3080
+ const event = new DragEvent(type, {
3081
+ bubbles: true,
3082
+ cancelable: true,
3083
+ composed: true,
3084
+ dataTransfer: transfer,
3085
+ clientX,
3086
+ clientY
3087
+ });
3088
+ const effects = { dropEffect: "none", effectAllowed: "all" };
3089
+ const recorded = new Proxy(transfer, {
3090
+ get(target2, key) {
3091
+ if (key === "dropEffect" || key === "effectAllowed") return effects[key];
3092
+ const value = Reflect.get(target2, key, target2);
3093
+ return typeof value === "function" ? value.bind(target2) : value;
3094
+ },
3095
+ set(target2, key, value) {
3096
+ if (key === "dropEffect" || key === "effectAllowed") {
3097
+ effects[key] = String(value);
3098
+ return true;
3099
+ }
3100
+ return Reflect.set(target2, key, value, target2);
3101
+ }
3102
+ });
3103
+ Object.defineProperty(event, "dataTransfer", { configurable: true, value: recorded });
3104
+ target.dispatchEvent(event);
3105
+ events.push({ type, defaultPrevented: event.defaultPrevented, dropEffect: effects.dropEffect ?? "none" });
3106
+ }
3107
+ await tools.settle();
3108
+ const resolve = (role) => {
3109
+ const probe = document.createElement("span");
3110
+ probe.setAttribute("data-modeler-shell-conformance", "role-probe");
3111
+ probe.style.cssText = `position:absolute;visibility:hidden;background-color:var(${role});`;
3112
+ rootElement.append(probe);
3113
+ const color = getComputedStyle(probe).backgroundColor;
3114
+ probe.remove();
3115
+ return color;
3116
+ };
3117
+ const resolveColor = (role) => {
3118
+ const probe = document.createElement("span");
3119
+ probe.setAttribute("data-modeler-shell-conformance", "role-probe");
3120
+ probe.style.cssText = `position:absolute;visibility:hidden;color:var(${role});`;
3121
+ rootElement.append(probe);
3122
+ const color = getComputedStyle(probe).color;
3123
+ probe.remove();
3124
+ return color;
3125
+ };
3126
+ const zone = rootElement.querySelector('[data-chrome="drop-zone"]');
3127
+ const chip = zone?.querySelector(".cd-shell-drop-zone-chip") ?? null;
3128
+ const zoneStyle = zone ? getComputedStyle(zone) : null;
3129
+ return {
3130
+ state: rootElement.getAttribute("data-drop"),
3131
+ events,
3132
+ zone: zone && zoneStyle ? {
3133
+ visible: tools.visible(zone),
3134
+ box: tools.box(zone, rootElement),
3135
+ ariaHidden: zone.getAttribute("aria-hidden"),
3136
+ background: zoneStyle.backgroundColor,
3137
+ borderStyle: zoneStyle.borderTopStyle,
3138
+ borderWidth: zoneStyle.borderTopWidth,
3139
+ borderColor: zoneStyle.borderTopColor,
3140
+ radius: zoneStyle.borderTopLeftRadius,
3141
+ cursor: zoneStyle.cursor
3142
+ } : null,
3143
+ chip: chip ? {
3144
+ text: (chip.textContent ?? "").replace(/\s+/g, " ").trim(),
3145
+ background: getComputedStyle(chip).backgroundColor
3146
+ } : null,
3147
+ roles: {
3148
+ hover: resolve("--cd-ui-hover"),
3149
+ interaktion: resolveColor("--cd-ui-interaktion"),
3150
+ dangerFlaeche: resolve("--cd-ui-danger-flaeche"),
3151
+ danger: resolveColor("--cd-ui-danger"),
3152
+ flaecheHoch: resolve("--cd-ui-flaeche-hoch")
3153
+ }
3154
+ };
3155
+ };
3156
+
3157
+ // src/testing/web-cases.ts
3158
+ var WIDEST_STEP = 1280;
3159
+ var NARROWEST_STEP = 400;
3160
+ var PHONE_WIDTH = 320;
3161
+ var STEP_PX = 24;
3162
+ var TOAST_MARGIN_MS = 900;
3163
+ var TOAST_RESUME_MS = 1500;
3164
+ var TOAST_ANNOUNCE_WAIT_MS = 1e3;
3165
+ var DROP_WAIT_MS = 2e3;
3166
+ var START_SCREEN_TABS = 10;
3167
+ var TOAST_STATUSES = ["success", "info", "warning", "danger"];
3168
+ var documentLangProbe = (_) => document.documentElement.lang;
3169
+ function sizeOf(harness) {
3170
+ return (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
3171
+ }
3172
+ function skipFor(hook, what) {
3173
+ return {
3174
+ skipped: [
3175
+ {
3176
+ hook,
3177
+ reason: `${hook === "drop" ? "harness.drop" : `show.${hook}`} is undefined: ${what} not verified`
3178
+ }
3179
+ ]
3180
+ };
3181
+ }
3182
+ async function aria(page, root) {
3183
+ return inPage(page, ariaProbe, { root });
3184
+ }
3185
+ async function rowSamples(page, root, height) {
3186
+ const samples = [];
3187
+ const measurements = [];
3188
+ for (let width = WIDEST_STEP; width >= NARROWEST_STEP; width -= STEP_PX) {
3189
+ await page.setViewportSize({ width, height });
3190
+ await settle(page);
3191
+ const sample = await inPage(page, topRowProbe, { root });
3192
+ if (sample) samples.push(sample);
3193
+ measurements.push(await measureChrome(page, root));
3194
+ }
3195
+ return { samples, measurements };
3196
+ }
3197
+ async function primaryTooltipAt(page, root, width, height) {
3198
+ await page.setViewportSize({ width, height });
3199
+ await settle(page);
3200
+ const primary = await findTarget(page, root, "primary");
3201
+ if (!primary) return null;
3202
+ await page.mouse.move(PARK.x, PARK.y);
3203
+ await page.mouse.move(primary.client.x, primary.client.y);
3204
+ await page.waitForTimeout(TOOLTIP_DELAY_MS + HOVER_MARGIN_MS);
3205
+ await settle(page);
3206
+ const tooltip = await findOverlay(page, root, "tooltip");
3207
+ await page.mouse.move(PARK.x, PARK.y);
3208
+ return tooltip?.text ?? null;
3209
+ }
3210
+ async function primaryActivation(page, root) {
3211
+ const primary = await findTarget(page, root, "primary");
3212
+ if (!primary) return [];
3213
+ await inPage(page, countClicksProbe, { root, selector: '[data-chrome="primary"]' });
3214
+ await page.mouse.click(primary.client.x, primary.client.y);
3215
+ await settle(page);
3216
+ const pointer = await inPage(page, clicksProbe, null);
3217
+ if (!await focusTarget(page, root, "primary")) {
3218
+ await inPage(page, removeInjectedProbe, null);
3219
+ return ["primary: the primary action cannot take the focus"];
3220
+ }
3221
+ await page.keyboard.press("Enter");
3222
+ await settle(page);
3223
+ const enter = await inPage(page, clicksProbe, null) - pointer;
3224
+ await inPage(page, removeInjectedProbe, null);
3225
+ await page.mouse.move(PARK.x, PARK.y);
3226
+ return primaryActivationProblems({ pointer, enter });
3227
+ }
3228
+ async function fileEntryProblems(page, root) {
3229
+ const entries = (await menuEntries(page, root)).filter((entry) => entry.group === "file");
3230
+ const problems = [];
3231
+ for (const entry of entries) {
3232
+ if (!entry.menuId || !await openMenuByClick(page, root)) continue;
3233
+ const item = await inPage(page, fileItemProbe, { root, menuId: entry.menuId });
3234
+ if (!item) {
3235
+ await closeByEscape(page);
3236
+ continue;
3237
+ }
3238
+ await page.mouse.click(item.x, item.y);
3239
+ await settle(page);
3240
+ const menu = await inPage(page, menuProbe, { root });
3241
+ if (menu?.menu || menu?.expanded === "true") {
3242
+ problems.push(
3243
+ `menu: File entry "${entry.name}" leaves the menu open; an action closes it first (guide \xA75.4)`
3244
+ );
3245
+ await closeByEscape(page);
3246
+ }
3247
+ if (entry.name.endsWith("\u2026")) await closeByEscape(page);
3248
+ }
3249
+ return problems;
3250
+ }
3251
+ var fileItemProbe = ({
3252
+ root,
3253
+ menuId
3254
+ }) => {
3255
+ const item = document.querySelector(root)?.querySelector(`[role="menu"] [data-menu-id="${menuId}"]`);
3256
+ if (!item) return null;
3257
+ const rect = item.getBoundingClientRect();
3258
+ return rect.width > 0 ? { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 } : null;
3259
+ };
3260
+ async function lockedFileProblems(page, harness, root) {
3261
+ const invalid = harness.show?.invalid;
3262
+ if (!invalid) return [];
3263
+ await invalid(page);
3264
+ await page.waitForTimeout(INVALID_STATUS_DELAY_MS + HOVER_MARGIN_MS);
3265
+ return (await menuEntries(page, root)).filter((entry) => entry.group === "file" && entry.disabled).map(
3266
+ (entry) => `menu: File entry "${entry.name}" is aria-disabled on the read-only canvas (guide \xA713.5)`
3267
+ );
3268
+ }
3269
+ var webChromeCase = {
3270
+ name: "webChrome",
3271
+ async run(page, harness) {
3272
+ const root = harness.root ?? DEFAULT_ROOT;
3273
+ const size = sizeOf(harness);
3274
+ await openHarness(page, harness, { size, requested: LIGHT });
3275
+ await settle(page);
3276
+ const problems = [];
3277
+ if (harness.host !== "web") {
3278
+ const measurement = await measureChrome(page, root);
3279
+ for (const island of ["primary", "legal", "empty-state", "drop-zone"]) {
3280
+ if (measurement.islands[island]) {
3281
+ problems.push(
3282
+ `${island}: visible in the ${harness.host} host; it belongs to the web app (guide \xA75.2)`
3283
+ );
3284
+ }
3285
+ }
3286
+ return finish("webChrome", harness.host, problems, []);
3287
+ }
3288
+ const facts = await aria(page, root);
3289
+ if (facts) problems.push(...legalContentProblems(facts.legalLinks));
3290
+ const name2 = (facts?.primary?.attributes["aria-label"] ?? "").trim();
3291
+ problems.push(...await primaryActivation(page, root));
3292
+ const { samples, measurements } = await rowSamples(page, root, size.height);
3293
+ for (const measurement of measurements) {
3294
+ problems.push(
3295
+ ...webRowProblems(measurement, "web").map(
3296
+ (problem) => `[${Math.round(measurement.container.width)}px wide] ${problem}`
3297
+ ),
3298
+ ...zoomRowBottomProblems(
3299
+ measurement.islands.zoom,
3300
+ measurement.open.zoomRaised,
3301
+ measurement.container,
3302
+ measurement.islands.legal?.height
3303
+ )
3304
+ );
3305
+ }
3306
+ if (samples.some((sample) => sample.primary)) {
3307
+ const withLabel = labelledWidth(samples);
3308
+ if (withLabel === void 0) {
3309
+ problems.push(
3310
+ `primary: never shows its label between ${WIDEST_STEP}px and ${NARROWEST_STEP}px; it collapses only when the top row gets tight (guide \xA75.15)`
3311
+ );
3312
+ } else {
3313
+ problems.push(...topRowProblems(samples, withLabel, name2));
3314
+ }
3315
+ const collapsed = samples.find((sample) => sample.primary?.collapsed);
3316
+ const labelled = samples.find((sample) => sample.primary && !sample.primary.collapsed);
3317
+ for (const sample of [labelled, collapsed]) {
3318
+ if (!sample?.primary) continue;
3319
+ const width = sample.container.width;
3320
+ const tooltip = await primaryTooltipAt(page, root, width, size.height);
3321
+ problems.push(...primaryTooltipProblems(sample.primary.collapsed, tooltip, name2, width));
3322
+ }
3323
+ }
3324
+ await page.setViewportSize({ width: PHONE_WIDTH, height: size.height });
3325
+ await settle(page);
3326
+ const legalLine = await inPage(page, legalLineProbe, { root });
3327
+ if (legalLine) problems.push(...legalReachProblems(legalLine));
3328
+ await page.setViewportSize({ width: size.width, height: size.height });
3329
+ await settle(page);
3330
+ problems.push(...await fileEntryProblems(page, root));
3331
+ problems.push(...await lockedFileProblems(page, harness, root));
3332
+ return finish("webChrome", harness.host, [...new Set(problems)], []);
3333
+ }
3334
+ };
3335
+ function startScreen(page, root) {
3336
+ return inPage(page, emptyStateProbe, { root, work: WORK_CHROME });
3337
+ }
3338
+ async function toastFocusOnStartScreen(page, root, show) {
3339
+ const text = "Conformance: a toast on the start screen";
3340
+ await show(page, { status: "danger", text });
3341
+ await settle(page);
3342
+ const closeName = (await toasts(page, root))?.toasts.find((toast) => toast.text.includes(text))?.buttons.at(-1)?.name;
3343
+ if (!closeName || !await inPage(page, focusToastButtonProbe, { root, text, name: closeName })) return [];
3344
+ await page.keyboard.press("Enter");
3345
+ await settle(page);
3346
+ const focus = await inPage(page, focusOwnerProbe, { root });
3347
+ return focus.owner === "empty-state" ? [] : [
3348
+ `empty-state: closing a focused toast on the start screen leaves the focus on ${focus.description}; it goes to the card's "New diagram" (WCAG 2.4.3)`
3349
+ ];
3350
+ }
3351
+ var emptyStateCase = {
3352
+ name: "emptyState",
3353
+ async run(page, harness) {
3354
+ assertHooksConsistent("emptyState", harness, ["emptyState"]);
3355
+ const root = harness.root ?? DEFAULT_ROOT;
3356
+ const size = sizeOf(harness);
3357
+ const show = harness.show?.emptyState;
3358
+ if (harness.host !== "web") {
3359
+ await openHarness(page, harness, { size, requested: LIGHT });
3360
+ if (show) await show(page);
3361
+ await settle(page);
3362
+ const facts2 = await startScreen(page, root);
3363
+ const problems2 = [];
3364
+ if (facts2?.card?.visible || facts2?.shown) {
3365
+ problems2.push(
3366
+ `empty-state: a start screen in the ${harness.host} host; it shows the empty-file hint instead (guide \xA713.5)`
3367
+ );
3368
+ }
3369
+ return finish("emptyState", harness.host, problems2, []);
3370
+ }
3371
+ if (!show) {
3372
+ return unfed(harness, "emptyState") ? skipFor("emptyState", "the start screen") : void 0;
3373
+ }
3374
+ await openHarness(page, harness, { size, requested: LIGHT });
3375
+ await settle(page);
3376
+ const lang = await inPage(page, documentLangProbe, null);
3377
+ await show(page);
3378
+ await settle(page);
3379
+ const container = (await measureChrome(page, root)).container;
3380
+ const facts = await startScreen(page, root);
3381
+ if (!facts) return finish("emptyState", harness.host, [`no element matches "${root}"`], []);
3382
+ const problems = emptyStateProblems(facts, container, lang);
3383
+ await inPage(page, blurProbe, null);
3384
+ const stops = [];
3385
+ for (let press = 0; press < START_SCREEN_TABS; press++) {
3386
+ await page.keyboard.press("Tab");
3387
+ stops.push(await inPage(page, focusOwnerProbe, { root }));
3388
+ }
3389
+ problems.push(...emptyStateTabProblems(stops));
3390
+ await inPage(page, blurProbe, null);
3391
+ for (const key of ["Control+Equal", "Control+Minus", "Shift+Digit1"]) {
3392
+ await inPage(page, clearKeysProbe, null);
3393
+ const before = (await inPage(page, afterPressProbe, { root })).zoom;
3394
+ await page.keyboard.press(key);
3395
+ const after = await inPage(page, afterPressProbe, { root });
3396
+ problems.push(...emptyStateKeyProblems(key, before, after.zoom, after.keys));
3397
+ }
3398
+ const showToast = harness.show?.toast;
3399
+ if (showToast) problems.push(...await toastFocusOnStartScreen(page, root, showToast));
3400
+ const start = facts.buttons[0];
3401
+ if (start) {
3402
+ await page.mouse.click(start.client.x, start.client.y);
3403
+ await settle(page);
3404
+ const after = await pollUntil(
3405
+ page,
3406
+ () => startScreen(page, root),
3407
+ (state) => state === null || !state.shown,
3408
+ 1e3
3409
+ );
3410
+ if (after) problems.push(...afterStartProblems(after, await inPage(page, focusOwnerProbe, { root })));
3411
+ }
3412
+ return finish("emptyState", harness.host, problems, []);
3413
+ }
3414
+ };
3415
+ function toasts(page, root) {
3416
+ return inPage(page, toastsProbe, { root });
3417
+ }
3418
+ async function dismissAll(page, root) {
3419
+ for (let attempt = 0; attempt < 6; attempt++) {
3420
+ const close = (await toasts(page, root))?.toasts[0]?.buttons.at(-1);
3421
+ if (!close) break;
3422
+ await page.mouse.click(close.client.x, close.client.y);
3423
+ await settle(page);
3424
+ }
3425
+ await page.mouse.move(PARK.x, PARK.y);
3426
+ }
3427
+ async function presentAt(page, root, since, ms, texts) {
3428
+ const wait = since + ms - Date.now();
3429
+ if (wait > 0) await page.waitForTimeout(wait);
3430
+ await settle(page);
3431
+ const shown = (await toasts(page, root))?.toasts ?? [];
3432
+ return new Set(texts.filter((text) => shown.some((toast) => toast.text.includes(text))));
3433
+ }
3434
+ async function toastAnatomy(page, root, show, lang) {
3435
+ const problems = [];
3436
+ const before = (await toasts(page, root))?.live ?? [];
3437
+ for (const status of TOAST_STATUSES) {
3438
+ const text = `Conformance: a ${status} toast`;
3439
+ await show(page, { status, text });
3440
+ await settle(page);
3441
+ const toast = (await toasts(page, root))?.toasts.find((candidate) => candidate.text.includes(text));
3442
+ if (!toast) {
3443
+ problems.push(`toasts: show.toast with a ${status} toast shows nothing`);
3444
+ continue;
3445
+ }
3446
+ const persistent = toastLiveRegion(before, status) !== void 0;
3447
+ const reached = (facts) => toastLiveRegion(facts?.live ?? [], status)?.text.includes(text) ?? false;
3448
+ const announced = persistent && reached(await pollUntil(page, () => toasts(page, root), reached, TOAST_ANNOUNCE_WAIT_MS));
3449
+ problems.push(...toastAnatomyProblems(toast, status, lang, { persistent, announced }));
3450
+ const close = toast.buttons.at(-1);
3451
+ if (close) {
3452
+ await page.mouse.click(close.client.x, close.client.y);
3453
+ await page.mouse.move(PARK.x, PARK.y);
3454
+ await settle(page);
3455
+ const still = (await toasts(page, root))?.toasts.some((candidate) => candidate.text.includes(text));
3456
+ if (still) problems.push(`toasts: Close does not close the ${status} toast`);
3457
+ }
3458
+ }
3459
+ await dismissAll(page, root);
3460
+ return problems;
3461
+ }
3462
+ async function toastStack(page, root, show) {
3463
+ const texts = ["A", "B", "C", "D"].map((letter) => `Conformance: stacked ${letter}`);
3464
+ const [first, second, third, fourth] = texts;
3465
+ await show(page, { status: "info", text: first, actionLabel: "Undo" });
3466
+ for (const text of [second, third, fourth]) await show(page, { status: "success", text });
3467
+ await settle(page);
3468
+ const problems = toastStackProblems((await toasts(page, root))?.toasts ?? [], texts, [
3469
+ first,
3470
+ third,
3471
+ fourth
3472
+ ]);
3473
+ await show(page, { status: "success", text: third });
3474
+ await settle(page);
3475
+ problems.push(...repeatedToastProblems((await toasts(page, root))?.toasts ?? [], third));
3476
+ await dismissAll(page, root);
3477
+ return problems;
3478
+ }
3479
+ async function toastTiming(page, root, show) {
3480
+ const lifetime = TOAST_DURATION_MS.success;
3481
+ const plain = "Conformance: a plain toast";
3482
+ const hovered = "Conformance: a hovered toast";
3483
+ const focused = "Conformance: a focused toast";
3484
+ const texts = [plain, hovered, focused];
3485
+ for (const text of texts) await show(page, { status: "success", text });
3486
+ const shownAt = Date.now();
3487
+ await settle(page);
3488
+ const target = (await toasts(page, root))?.toasts.find((toast) => toast.text.includes(hovered));
3489
+ if (target) await page.mouse.move(target.client.x, target.client.y);
3490
+ const focusTarget2 = (await toasts(page, root))?.toasts.find((toast) => toast.text.includes(focused));
3491
+ const closeName = focusTarget2?.buttons.at(-1)?.name ?? "";
3492
+ await inPage(page, focusToastButtonProbe, { root, text: focused, name: closeName });
3493
+ const timings = [];
3494
+ const record = (shown, after, expect) => {
3495
+ for (const [text, expectation, condition] of expect) {
3496
+ timings.push({ text, expectation, after, present: shown.has(text), condition });
3497
+ }
3498
+ };
3499
+ const early = lifetime - 800;
3500
+ record(await presentAt(page, root, shownAt, early, texts), early, [[plain, "present", "plain"]]);
3501
+ const late = lifetime + TOAST_MARGIN_MS;
3502
+ record(await presentAt(page, root, shownAt, late, texts), late, [
3503
+ [plain, "gone", "plain"],
3504
+ [hovered, "present", "hovered"],
3505
+ [focused, "present", "focused"]
3506
+ ]);
3507
+ await page.mouse.move(PARK.x, PARK.y);
3508
+ await inPage(page, blurProbe, null);
3509
+ const leftAt = Date.now();
3510
+ const resumed = TOAST_RESUME_MS - 500;
3511
+ record(await presentAt(page, root, leftAt, resumed, texts), resumed, [
3512
+ [hovered, "present", "resumed"],
3513
+ [focused, "present", "resumed"]
3514
+ ]);
3515
+ const gone = lifetime + TOAST_MARGIN_MS;
3516
+ record(await presentAt(page, root, leftAt, gone, texts), gone, [
3517
+ [hovered, "gone", "resumed"],
3518
+ [focused, "gone", "resumed"]
3519
+ ]);
3520
+ const action = "Conformance: a toast with an action";
3521
+ await show(page, { status: "info", text: action, actionLabel: "Undo" });
3522
+ const actionAt = Date.now();
3523
+ const actionWait = TOAST_DURATION_MS.info + TOAST_MARGIN_MS;
3524
+ record(await presentAt(page, root, actionAt, actionWait, [action]), actionWait, [
3525
+ [action, "present", "action"]
3526
+ ]);
3527
+ await dismissAll(page, root);
3528
+ return toastTimingProblems(timings);
3529
+ }
3530
+ async function toastAboveModal(page, root, show) {
3531
+ const before = "Conformance: a toast before the dialog";
3532
+ const text = "Conformance: a toast above the dialog";
3533
+ await show(page, { status: "info", text: before, actionLabel: "Undo" });
3534
+ await settle(page);
3535
+ const problems = [];
3536
+ if ((await toasts(page, root))?.stack?.popover !== "manual") {
3537
+ problems.push(
3538
+ `toasts: the web app's stack is no popover="manual"; it stays above a showModal() dialog only in the top layer (API.md, Toasts)`
3539
+ );
3540
+ } else if (await inPage(page, watchToastStackProbe, { root })) {
3541
+ if (await inPage(page, showModalProbe, null)) {
3542
+ await show(page, { status: "success", text });
3543
+ await settle(page);
3544
+ const shown = (await toasts(page, root))?.toasts.some((toast) => toast.text.includes(text)) ?? false;
3545
+ const { toggles, open } = await inPage(page, toastStackTogglesProbe, { root });
3546
+ if (!shown) problems.push("toasts: show.toast with a showModal() dialog open shows nothing");
3547
+ else if (!open || toggles.at(-1) !== "open") {
3548
+ problems.push(
3549
+ "toasts: a toast shown with a showModal() dialog open does not raise the stack above it; the stack is shown again with every toast (API.md, Toasts)"
3550
+ );
3551
+ }
3552
+ }
3553
+ }
3554
+ await inPage(page, removeInjectedProbe, null);
3555
+ await settle(page);
3556
+ await dismissAll(page, root);
3557
+ return problems;
3558
+ }
3559
+ var toastsCase = {
3560
+ name: "toasts",
3561
+ async run(page, harness) {
3562
+ assertHooksConsistent("toasts", harness, ["toast"]);
3563
+ const root = harness.root ?? DEFAULT_ROOT;
3564
+ const size = sizeOf(harness);
3565
+ const show = harness.show?.toast;
3566
+ if (harness.host === "ide") {
3567
+ await openHarness(page, harness, { size, requested: LIGHT });
3568
+ if (show) await show(page, { status: "success", text: "Conformance: a toast in the IDE host" });
3569
+ await settle(page);
3570
+ const facts = await toasts(page, root);
3571
+ const problems2 = [];
3572
+ if (facts?.stack) {
3573
+ problems2.push(
3574
+ 'toasts: the IDE host renders a toast stack [data-chrome="toasts"]; it shows no toasts (guide \xA713.5)'
3575
+ );
3576
+ }
3577
+ if ((facts?.toasts.length ?? 0) > 0 || (facts?.strays.length ?? 0) > 0) {
3578
+ problems2.push(
3579
+ "toasts: show.toast shows a toast in the IDE host; the extension shows the host's notification (guide \xA713.5)"
3580
+ );
3581
+ }
3582
+ return finish("toasts", harness.host, problems2, []);
3583
+ }
3584
+ if (!show) return unfed(harness, "toast") ? skipFor("toast", "the toasts") : void 0;
3585
+ await openHarness(page, harness, { size, requested: LIGHT });
3586
+ await settle(page);
3587
+ const lang = await inPage(page, documentLangProbe, null);
3588
+ await dismissAll(page, root);
3589
+ const problems = [
3590
+ ...await toastAnatomy(page, root, show, lang),
3591
+ ...await toastStack(page, root, show),
3592
+ ...await toastTiming(page, root, show)
3593
+ ];
3594
+ if (harness.host === "web") problems.push(...await toastAboveModal(page, root, show));
3595
+ return finish("toasts", harness.host, problems, []);
3596
+ }
3597
+ };
3598
+ function popover(page, root) {
3599
+ return inPage(page, popoverProbe, { root });
3600
+ }
3601
+ async function clickPill(page, root) {
3602
+ const pill = (await popover(page, root))?.pill;
3603
+ if (!pill?.visible) return null;
3604
+ await page.mouse.click(pill.client.x, pill.client.y);
3605
+ await settle(page);
3606
+ return popover(page, root);
3607
+ }
3608
+ async function keyboardRows(page, root) {
3609
+ if (!await focusTarget(page, root, "pill")) return ["problems: the pill cannot take the focus"];
3610
+ await page.keyboard.press("Enter");
3611
+ await settle(page);
3612
+ const opened = await popover(page, root);
3613
+ const count = opened?.rows.length ?? 0;
3614
+ if (!opened?.popover?.visible || count === 0) {
3615
+ await closeByEscape(page);
3616
+ return opened?.popover?.visible ? [] : ["problems-popover: Enter on the pill does not open the list"];
3617
+ }
3618
+ const steps = [{ key: "Enter on the pill", expected: 0, focused: opened.focusedRow }];
3619
+ for (let index = 1; index < count; index++) {
3620
+ await page.keyboard.press("ArrowDown");
3621
+ steps.push({
3622
+ key: "ArrowDown",
3623
+ expected: index,
3624
+ focused: (await popover(page, root))?.focusedRow ?? null
3625
+ });
3626
+ }
3627
+ if (count > 1) {
3628
+ await page.keyboard.press("Home");
3629
+ steps.push({ key: "Home", expected: 0, focused: (await popover(page, root))?.focusedRow ?? null });
3630
+ await page.keyboard.press("End");
3631
+ steps.push({ key: "End", expected: count - 1, focused: (await popover(page, root))?.focusedRow ?? null });
3632
+ }
3633
+ await closeByEscape(page);
3634
+ return popoverKeyboardProblems(steps, count);
3635
+ }
3636
+ var problemsListCase = {
3637
+ name: "problemsList",
3638
+ async run(page, harness) {
3639
+ assertHooksConsistent("problemsList", harness, ["problems"]);
3640
+ const root = harness.root ?? DEFAULT_ROOT;
3641
+ const size = sizeOf(harness);
3642
+ const show = harness.show?.problems;
3643
+ if (!show) return unfed(harness, "problems") ? skipFor("problems", "the problems list") : void 0;
3644
+ await openHarness(page, harness, { size, requested: LIGHT });
3645
+ await settle(page);
3646
+ await show(page);
3647
+ await settle(page);
3648
+ const before = await popover(page, root);
3649
+ const problems = [];
3650
+ if (!before?.pill?.visible) {
3651
+ return finish(
3652
+ "problemsList",
3653
+ harness.host,
3654
+ ["problems: the pill is not visible after show.problems"],
3655
+ []
3656
+ );
3657
+ }
3658
+ const opensList = harness.host === "web" || before.pill.attributes["aria-haspopup"] === "dialog";
3659
+ if (!opensList) {
3660
+ const after = await clickPill(page, root);
3661
+ if (after?.popover?.visible) {
3662
+ problems.push(
3663
+ `problems-popover: the ${harness.host} host shows a problems list; its pill opens the host's own view (guide \xA75.10)`
3664
+ );
3665
+ }
3666
+ return finish("problemsList", harness.host, problems, []);
3667
+ }
3668
+ const lang = await inPage(page, documentLangProbe, null);
3669
+ const container = (await measureChrome(page, root)).container;
3670
+ const opened = await clickPill(page, root);
3671
+ if (!opened) return finish("problemsList", harness.host, ["problems: the pill cannot be clicked"], []);
3672
+ problems.push(...openPopoverProblems(opened, container, lang));
3673
+ await closeByEscape(page);
3674
+ const escaped = await popover(page, root);
3675
+ if (escaped) problems.push(...closedPopoverProblems(escaped, "Escape"));
3676
+ await clickPill(page, root);
3677
+ await page.mouse.click(PARK.x, PARK.y);
3678
+ await settle(page);
3679
+ const outside = await popover(page, root);
3680
+ if (outside) problems.push(...closedPopoverProblems(outside, "an outside click"));
3681
+ problems.push(...await keyboardRows(page, root));
3682
+ const skipped = [];
3683
+ const reopened = await clickPill(page, root);
3684
+ const row = reopened?.rows.find((candidate) => candidate.button);
3685
+ if (row) {
3686
+ await page.mouse.click(row.client.x, row.client.y);
3687
+ await settle(page);
3688
+ const selected = await popover(page, root);
3689
+ if (selected) problems.push(...closedPopoverProblems(selected, "a row with an element"));
3690
+ } else {
3691
+ await closeByEscape(page);
3692
+ if ((reopened?.rows.length ?? 0) > 0 && !harness.absent?.includes("problemSelection")) {
3693
+ skipped.push({
3694
+ hook: "problems",
3695
+ reason: 'show.problems gives no item with an elementId: a row that selects its element was not verified (absent: ["problemSelection"] for a modeler whose problems never point at an element)'
3696
+ });
3697
+ }
3698
+ }
3699
+ return finish("problemsList", harness.host, problems, skipped);
3700
+ }
3701
+ };
3702
+ function drag(page, spec) {
3703
+ return inPage(page, dragProbe, spec);
3704
+ }
3705
+ function secondFile(file) {
3706
+ return { ...file, name: `second-${file.name}` };
3707
+ }
3708
+ var dropZoneCase = {
3709
+ name: "dropZone",
3710
+ async run(page, harness) {
3711
+ assertHooksConsistent("dropZone", harness, ["drop", "emptyState"]);
3712
+ const root = harness.root ?? DEFAULT_ROOT;
3713
+ const size = sizeOf(harness);
3714
+ const fixtures = harness.drop;
3715
+ if (harness.host === "web" && !fixtures) {
3716
+ return harness.absent?.includes("drop") ? void 0 : skipFor("drop", "the drop zone");
3717
+ }
3718
+ await openHarness(page, harness, { size, requested: LIGHT });
3719
+ await settle(page);
3720
+ const problems = [];
3721
+ const probeFile = { name: "conformance.txt", type: "text/plain", text: "conformance" };
3722
+ if (harness.host !== "web" || !fixtures) {
3723
+ const facts = await drag(page, { root, files: [probeFile], steps: ["dragenter", "dragover"] });
3724
+ if (facts && (facts.state !== null || facts.zone?.visible)) {
3725
+ problems.push(
3726
+ `drop-zone: a file drag shows a drop zone in the ${harness.host} host; its files belong to the host (guide \xA75.2)`
3727
+ );
3728
+ }
3729
+ await drag(page, { root, files: [probeFile], steps: ["dragleave"] });
3730
+ return finish("dropZone", harness.host, problems, []);
3731
+ }
3732
+ const container = (await measureChrome(page, root)).container;
3733
+ const { accepted, rejected } = fixtures;
3734
+ const check = async (files, strings, expected, context) => {
3735
+ const over = await drag(page, { root, files, strings, steps: ["dragenter", "dragover"] });
3736
+ if (over) problems.push(...dropStateProblems(over, expected, context, container));
3737
+ const left = await drag(page, { root, files, strings, steps: ["dragleave"] });
3738
+ if (left && expected !== "none") {
3739
+ problems.push(...dropStateProblems(left, "none", `${context}, once it left the page,`, container));
3740
+ }
3741
+ };
3742
+ await check([accepted], [], "over", `a drag of "${accepted.name}"`);
3743
+ await check(
3744
+ [accepted, secondFile(accepted)],
3745
+ [],
3746
+ fixtures.multiple ? "over" : "forbidden",
3747
+ fixtures.multiple ? "a drag of two files (multiple)" : "a drag of two files"
3748
+ );
3749
+ if (await inPage(page, injectProbe, { root, kind: "modal" })) {
3750
+ await check([accepted], [], "forbidden", "a file drag with a modal dialog open");
3751
+ }
3752
+ await inPage(page, removeInjectedProbe, null);
3753
+ await check([], ["Conformance text"], "none", "a text drag");
3754
+ const emptyState = harness.show?.emptyState;
3755
+ if (emptyState) {
3756
+ await emptyState(page);
3757
+ await settle(page);
3758
+ }
3759
+ const onStartScreen = async () => (await inPage(page, emptyStateProbe, { root, work: [] }))?.shown ?? false;
3760
+ const started = await onStartScreen();
3761
+ if (rejected) {
3762
+ await drag(page, { root, files: [rejected], steps: ["dragenter", "dragover", "drop"] });
3763
+ const shown = await pollUntil(
3764
+ page,
3765
+ () => toasts(page, root),
3766
+ (facts) => facts?.toasts.some((toast) => toast.status === "danger") ?? false,
3767
+ DROP_WAIT_MS
3768
+ );
3769
+ if (!shown?.toasts.some((toast) => toast.status === "danger")) {
3770
+ problems.push(
3771
+ `drop-zone: dropping "${rejected.name}" shows no danger toast (guide \xA79, or the modeler's onReject)`
3772
+ );
3773
+ }
3774
+ if (started && !await onStartScreen()) {
3775
+ problems.push(
3776
+ `drop-zone: dropping "${rejected.name}" opened it; a file outside accept opens nothing`
3777
+ );
3778
+ }
3779
+ await dismissAll(page, root);
3780
+ }
3781
+ const dropped = await drag(page, { root, files: [accepted], steps: ["dragenter", "dragover", "drop"] });
3782
+ if (dropped?.events.find((event) => event.type === "drop")?.defaultPrevented === false) {
3783
+ problems.push(
3784
+ `drop-zone: the drop of "${accepted.name}" is not prevented, so the browser would open the file`
3785
+ );
3786
+ }
3787
+ if (started) {
3788
+ const opened = await pollUntil(page, onStartScreen, (shown) => !shown, DROP_WAIT_MS);
3789
+ if (opened)
3790
+ problems.push(
3791
+ `drop-zone: dropping "${accepted.name}" on the start screen does not open it (the card stays)`
3792
+ );
3793
+ }
3794
+ const after = await drag(page, { root, files: [], steps: [] });
3795
+ if (after && (after.state !== null || after.zone?.visible)) {
3796
+ problems.push("drop-zone: the drop zone still shows after the drop");
3797
+ }
3798
+ return finish("dropZone", harness.host, problems, []);
3799
+ }
3800
+ };
3801
+ function createWebCases() {
3802
+ return [webChromeCase, emptyStateCase, toastsCase, problemsListCase, dropZoneCase];
3803
+ }
3804
+
3805
+ // src/testing/cases.ts
3806
+ var LOCKED_DRAG_PX = 40;
3807
+ var CREATE_WAIT_MS = 1e3;
3808
+ var READ_ONLY_WAIT_MS = 1e3;
3809
+ var COLOR_WAIT_MS = 600;
3810
+ var REQUESTED_APPEARANCES = [
3811
+ { theme: "dark" },
3812
+ { theme: "dark", kontrast: "hoch" },
3813
+ { theme: "light", kontrast: "hoch" }
3814
+ ];
3815
+ var TEXT_FIELDS = [
3816
+ ["input", "a text field"],
3817
+ ["textarea", "a textarea"],
3818
+ ["contenteditable", "an editable label"]
3819
+ ];
3820
+ var UNDO_REDO_PRESSES = [
3821
+ "Control+KeyZ",
3822
+ "Meta+KeyZ",
3823
+ "Control+Shift+KeyZ",
3824
+ "Meta+Shift+KeyZ",
3825
+ "Control+KeyY"
3826
+ ];
3827
+ var ONE_STEP_UP = 135;
3828
+ var TWO_STEPS_UP = 182;
3829
+ var ZOOM_LIMIT_MAX = 400;
3830
+ var ZOOM_LIMIT_MIN = 20;
3831
+ function appearanceLabel(appearance) {
3832
+ return appearance.kontrast ? `${appearance.theme} + ${appearance.kontrast}` : appearance.theme;
3833
+ }
3834
+ function missingHooks(harness, what) {
3835
+ return SHOWN_ISLANDS.filter((island) => unfed(harness, island)).map((island) => ({
3836
+ hook: island,
3837
+ reason: `show.${island} is undefined: ${what} of the ${island} island not verified`
3838
+ }));
3839
+ }
3840
+ function visibleIslands(facts) {
3841
+ const islands = /* @__PURE__ */ new Set();
3842
+ for (const island of SHOWN_ISLANDS) if (facts[island]?.visible) islands.add(island);
3843
+ return islands;
3844
+ }
3845
+ async function popupGeometry(page, root, measurement) {
3846
+ const problems = [];
3847
+ if (!await openMenuByClick(page, root)) return ["dropdown: no visible menu trigger to open it"];
3848
+ const dropdown = await findOverlay(page, root, "menu");
3849
+ if (!dropdown) problems.push('dropdown: no visible role="menu" after clicking the trigger');
3850
+ else problems.push(...dropdownProblems(dropdown.box, measurement.container));
3851
+ await closeByEscape(page);
3852
+ const rows = [
3853
+ ["menu", "top"],
3854
+ ["palette-entry", measurement.paletteRow === "second-row" ? "second-row" : "top"],
3855
+ ["zoom-in", "bottom"]
3856
+ ];
3857
+ for (const [target, row] of rows) {
3858
+ const anchor = await hover(page, root, target);
3859
+ if (!anchor) {
3860
+ problems.push(`tooltip: no visible ${target} to hover`);
3861
+ continue;
3862
+ }
3863
+ const tooltip = await findOverlay(page, root, "tooltip");
3864
+ if (!tooltip) {
3865
+ problems.push(`tooltip: none visible ${TOOLTIP_DELAY_MS + HOVER_MARGIN_MS}ms after hovering ${target}`);
3866
+ continue;
3867
+ }
3868
+ if (tooltip.count > 1) problems.push(`tooltip: ${tooltip.count} tooltips visible at once`);
3869
+ if (anchor.label && !tooltip.text.startsWith(anchor.label)) {
3870
+ problems.push(`tooltip: hovering ${target} ("${anchor.label}") shows "${tooltip.text}"`);
3871
+ }
3872
+ const legalHeight = legalLineHeight(measurement.islands.legal?.height);
3873
+ const bottomRow = measurement.open.zoomRaised ? SHELL.edge + legalHeight + SHELL.gap : SHELL.edge;
3874
+ problems.push(
3875
+ ...tooltipProblems(tooltip.box, anchor.box, row, measurement.container, void 0, bottomRow).map(
3876
+ (problem) => `${problem} (on ${target})`
3877
+ )
3878
+ );
3879
+ }
3880
+ await page.mouse.move(PARK.x, PARK.y);
3881
+ return problems;
3882
+ }
3883
+ var GEOMETRY_TOAST = {
3884
+ status: "info",
3885
+ text: "Conformance: the toast stack's geometry",
3886
+ actionLabel: "Undo"
3887
+ };
3888
+ async function withProblemsListOpen(page, root, check) {
3889
+ const pill = await findTarget(page, root, "pill");
3890
+ if (!pill) return;
3891
+ await page.mouse.click(pill.client.x, pill.client.y);
3892
+ await settle(page);
3893
+ await check();
3894
+ await closeByEscape(page);
3895
+ }
3896
+ var geometryCase = {
3897
+ name: "geometry",
3898
+ async run(page, harness) {
3899
+ assertHooksConsistent("geometry", harness, [...SHOWN_ISLANDS, "toast", "emptyState"]);
3900
+ const root = harness.root ?? DEFAULT_ROOT;
3901
+ const problems = [];
3902
+ for (const size of harness.sizes ?? DEFAULT_SIZES) {
3903
+ const at = sizeLabel(size);
3904
+ await openHarness(page, harness, { size, requested: LIGHT });
3905
+ await settle(page);
3906
+ const check = async (state) => {
3907
+ const measurement = await measureChrome(page, root);
3908
+ problems.push(
3909
+ ...[
3910
+ ...geometryProblems(measurement, harness.host),
3911
+ ...absentIslandProblems(new Set(Object.keys(measurement.islands)), harness.absent)
3912
+ ].map((problem) => `[${at}, ${state}] ${problem}`)
3913
+ );
3914
+ return measurement;
3915
+ };
3916
+ const opened = await check("as opened");
3917
+ problems.push(...(await popupGeometry(page, root, opened)).map((problem) => `[${at}] ${problem}`));
3918
+ const toast = harness.show?.toast;
3919
+ if (toast) {
3920
+ await toast(page, GEOMETRY_TOAST);
3921
+ await settle(page);
3922
+ await check("with a toast");
3923
+ }
3924
+ for (const island of SHOWN_ISLANDS) {
3925
+ const show = harness.show?.[island];
3926
+ if (!show) continue;
3927
+ await show(page);
3928
+ await settle(page);
3929
+ await check(`after show.${island}`);
3930
+ if (island === "problems" && harness.host !== "ide") {
3931
+ await withProblemsListOpen(page, root, () => check("with the problems list open"));
3932
+ }
3933
+ }
3934
+ const emptyState = harness.show?.emptyState;
3935
+ if (emptyState) {
3936
+ await emptyState(page);
3937
+ await settle(page);
3938
+ await check("after show.emptyState");
3939
+ }
3940
+ }
3941
+ return finish("geometry", harness.host, problems, [
3942
+ ...missingHooks(harness, "the geometry"),
3943
+ ...webHookSkips(harness, {
3944
+ toast: "the geometry of the toast stack",
3945
+ emptyState: "the geometry of the start screen"
3946
+ })
3947
+ ]);
3948
+ }
3949
+ };
3950
+ async function menuState(page, root) {
3951
+ const facts = await inPage(page, menuProbe, { root });
3952
+ if (!facts) throw new Error(`@miragon/modeler-shell conformance: no element matches "${root}"`);
3953
+ return facts;
3954
+ }
3955
+ async function menuKeyboardProblems(page, root) {
3956
+ const problems = [];
3957
+ const openWith = async (key, focus) => {
3958
+ if (!await focusTarget(page, root, "menu")) {
3959
+ problems.push("menu: the trigger cannot take focus");
3960
+ return void 0;
3961
+ }
3962
+ await page.keyboard.press(key);
3963
+ await settle(page);
3964
+ const opened2 = await menuState(page, root);
3965
+ problems.push(...menuOpenProblems(opened2, focus).map((problem) => `${key} on the trigger: ${problem}`));
3966
+ return opened2;
3967
+ };
3968
+ const close = async (key) => {
3969
+ await closeByEscape(page);
3970
+ problems.push(
3971
+ ...menuClosedProblems(await menuState(page, root), true).map(
3972
+ (problem) => `${key}, then Escape: ${problem}`
3973
+ )
3974
+ );
3975
+ };
3976
+ const opened = await openWith("Enter", "first");
3977
+ if (opened && opened.items.length > 1) {
3978
+ const moves = [
3979
+ ["ArrowUp", (count) => count - 1],
3980
+ ["ArrowDown", () => 0],
3981
+ ["End", (count) => count - 1],
3982
+ ["Home", () => 0]
3983
+ ];
3984
+ await inPage(page, clearKeysProbe, null);
3985
+ for (const [key, target] of moves) {
3986
+ await page.keyboard.press(key);
3987
+ const moved = await menuState(page, root);
3988
+ const expected = target(moved.items.length);
3989
+ if (moved.active !== expected) {
3990
+ problems.push(
3991
+ `menu: ${key} moves the focus to entry ${moved.active + 1}, expected entry ${expected + 1} of ${moved.items.length}`
3992
+ );
3993
+ }
3994
+ }
3995
+ const { documentKeys } = await inPage(page, afterPressProbe, { root });
3996
+ problems.push(
3997
+ ...menuKeysAtDocumentProblems(
3998
+ moves.map(([key]) => key),
3999
+ documentKeys
4000
+ )
4001
+ );
4002
+ }
4003
+ if (opened) await close("Enter");
4004
+ if (await openWith("ArrowDown", "first")) await close("ArrowDown");
4005
+ if (await openWith("ArrowUp", "last")) await close("ArrowUp");
4006
+ return problems;
4007
+ }
4008
+ async function paletteKeyboardProblems(page, root) {
4009
+ const before = await inPage(page, paletteFocusProbe, { root });
4010
+ if (!before || before.entries.length < 2) return [];
4011
+ if (!await focusTarget(page, root, "palette-tab-stop")) {
4012
+ return ['palette: no entry with tabindex="0" takes the focus'];
4013
+ }
4014
+ const problems = [];
4015
+ const start = before.entries.indexOf(before.tabStops[0] ?? "");
4016
+ const step = start < before.entries.length - 1 ? 1 : -1;
4017
+ const moves = [
4018
+ [step === 1 ? "ArrowRight" : "ArrowLeft", start + step],
4019
+ ["End", before.entries.length - 1],
4020
+ ["Home", 0]
4021
+ ];
4022
+ for (const [key, index] of moves) {
4023
+ await page.keyboard.press(key);
4024
+ const after = await inPage(page, paletteFocusProbe, { root });
4025
+ const expected = before.entries[index];
4026
+ if (!after || after.active !== expected) {
4027
+ problems.push(`palette: ${key} focuses "${after?.active ?? "nothing"}", expected "${expected}"`);
4028
+ } else if (after.tabStops.length !== 1 || after.tabStops[0] !== expected) {
4029
+ problems.push(
4030
+ `palette: after ${key} tabindex="0" is on ${after.tabStops.join(", ") || "no entry"}, expected "${expected}"`
4031
+ );
4032
+ }
4033
+ }
4034
+ return problems;
4035
+ }
4036
+ async function paletteShortcutWalk(page, root) {
4037
+ const before = await inPage(page, paletteFocusProbe, { root });
4038
+ if (!before || before.entries.length === 0) return [];
4039
+ if (!await focusTarget(page, root, "palette-tab-stop")) return [];
4040
+ if (before.entries.length > 1) await page.keyboard.press("End");
4041
+ await page.keyboard.press("Home");
4042
+ const seen = [];
4043
+ for (let index = 0; index < before.entries.length; index++) {
4044
+ const facts = await inPage(page, paletteShortcutProbe, { root });
4045
+ if (facts) seen.push(facts);
4046
+ await page.keyboard.press("ArrowRight");
4047
+ }
4048
+ return paletteShortcutProblems(seen);
4049
+ }
4050
+ async function tabSequenceFromMenu(page, root, primary) {
4051
+ if (!await focusTarget(page, root, "menu")) return [];
4052
+ const stops = [];
4053
+ for (let step = 0; step < 3; step++) {
4054
+ await page.keyboard.press("Tab");
4055
+ const stop = await inPage(page, activeStopProbe, { root });
4056
+ stops.push({ description: stop.description, kind: stop.kind });
4057
+ }
4058
+ return tabSequenceProblems(stops, primary);
4059
+ }
4060
+ async function zoomLimitProblems(page, root) {
4061
+ const problems = [];
4062
+ const limits = [
4063
+ ["in", "zoom-in", ZOOM_LIMIT_MAX, 12, "out"],
4064
+ ["out", "zoom-out", ZOOM_LIMIT_MIN, 16, "in"]
4065
+ ];
4066
+ for (const [button, target, limit, maxClicks, other] of limits) {
4067
+ const control = await findTarget(page, root, target);
4068
+ if (!control) continue;
4069
+ let buttons = await inPage(page, zoomButtonsProbe, { root });
4070
+ for (let click = 0; click < maxClicks && buttons[button]?.attributes["aria-disabled"] !== "true"; click++) {
4071
+ await page.mouse.click(control.client.x, control.client.y);
4072
+ await settle(page);
4073
+ buttons = await inPage(page, zoomButtonsProbe, { root });
4074
+ }
4075
+ const { zoom } = await inPage(page, afterPressProbe, { root });
4076
+ const reached = buttons[button];
4077
+ if (reached?.attributes["aria-disabled"] !== "true") {
4078
+ problems.push(
4079
+ `zoom: [data-zoom="${button}"] is not aria-disabled after ${maxClicks} clicks (readout ${zoom ?? "?"}%)`
4080
+ );
4081
+ } else {
4082
+ if (zoom !== limit) {
4083
+ problems.push(`zoom: [data-zoom="${button}"] is disabled at ${zoom ?? "?"}%, expected at ${limit}%`);
4084
+ }
4085
+ if (reached.disabled) problems.push(`zoom: [data-zoom="${button}"] is natively disabled at the limit`);
4086
+ }
4087
+ if (buttons[other]?.attributes["aria-disabled"] === "true") {
4088
+ problems.push(`zoom: [data-zoom="${other}"] is aria-disabled at ${zoom ?? "?"}%`);
4089
+ }
4090
+ if (buttons.actual?.attributes["aria-disabled"] === "true") {
4091
+ problems.push("zoom: the readout is disabled at a limit");
4092
+ }
4093
+ }
4094
+ await page.mouse.move(PARK.x, PARK.y);
4095
+ return problems;
4096
+ }
4097
+ var ariaCase = {
4098
+ name: "aria",
4099
+ async run(page, harness) {
4100
+ assertHooksConsistent("aria", harness, SHOWN_ISLANDS);
4101
+ const root = harness.root ?? DEFAULT_ROOT;
4102
+ const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
4103
+ await openHarness(page, harness, { size, requested: LIGHT });
4104
+ await settle(page);
4105
+ const facts = await inPage(page, ariaProbe, { root });
4106
+ if (!facts) fail("aria", harness.host, [`no element matches the shell root selector "${root}"`]);
4107
+ const problems = ariaProblems(facts, harness.host);
4108
+ problems.push(...menuGroupProblems(await menuEntries(page, root), harness.host, harness.themes));
4109
+ problems.push(...await menuKeyboardProblems(page, root));
4110
+ problems.push(...await paletteKeyboardProblems(page, root));
4111
+ problems.push(...await paletteShortcutWalk(page, root));
4112
+ problems.push(
4113
+ ...await tabSequenceFromMenu(page, root, harness.host === "web" && Boolean(facts.primary?.visible))
4114
+ );
4115
+ problems.push(...await zoomLimitProblems(page, root));
4116
+ const shown = /* @__PURE__ */ new Set();
4117
+ for (const island of SHOWN_ISLANDS) {
4118
+ const show = harness.show?.[island];
4119
+ if (!show) continue;
4120
+ await show(page);
4121
+ shown.add(island);
4122
+ }
4123
+ let final = facts;
4124
+ if (shown.size > 0) {
4125
+ await settle(page);
4126
+ const withIslands = await inPage(page, ariaProbe, { root });
4127
+ if (withIslands) {
4128
+ final = withIslands;
4129
+ problems.push(...optionalIslandProblems(withIslands, shown));
4130
+ if (shown.has("problems")) problems.push(...pillPopupAriaProblems(withIslands, harness.host));
4131
+ const known = new Set(problems);
4132
+ problems.push(
4133
+ ...ariaProblems(withIslands, harness.host).filter((problem) => !known.has(problem)).map((problem) => `with every island shown: ${problem}`)
4134
+ );
4135
+ }
4136
+ }
4137
+ problems.push(...absentIslandProblems(visibleIslands(final), harness.absent));
4138
+ return finish("aria", harness.host, problems, missingHooks(harness, "the roles and names"));
4139
+ }
4140
+ };
4141
+ var keysCase = {
4142
+ name: "keys",
4143
+ async run(page, harness) {
4144
+ const root = harness.root ?? DEFAULT_ROOT;
4145
+ const host = harness.host;
4146
+ const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
4147
+ await openHarness(page, harness, { size, requested: LIGHT });
4148
+ await settle(page);
4149
+ const problems = [];
4150
+ if (!await focusTarget(page, root, "canvas")) {
4151
+ fail("keys", host, ["canvas: the diagram-js <svg> (.djs-container > svg) cannot take focus"]);
4152
+ }
4153
+ await inPage(page, clearKeysProbe, null);
4154
+ let current = (await inPage(page, afterPressProbe, { root })).zoom;
4155
+ if (current === null) fail("keys", host, ['zoom: no readout [data-zoom="actual"] showing a percentage']);
4156
+ const press = async (key, reach, zoom, context = "on the canvas") => {
4157
+ const before = current;
4158
+ await page.keyboard.press(key);
4159
+ let after = await inPage(page, afterPressProbe, { root });
4160
+ const where = `${key} ${context}`;
4161
+ if (reach) {
4162
+ const problem = keyReachProblem(where, reach, host, after);
4163
+ if (problem) problems.push(problem);
4164
+ }
4165
+ if (zoom === void 0) return;
4166
+ const expected = zoom === "unchanged" ? before : zoom;
4167
+ for (let poll = 0; after.zoom === before && expected !== before && poll < 10; poll++) {
4168
+ await page.waitForTimeout(50);
4169
+ after = await inPage(page, afterPressProbe, { root });
4170
+ }
4171
+ if (after.zoom !== expected) {
4172
+ problems.push(
4173
+ zoom === "unchanged" ? `${where} changed the zoom from ${before}% to ${after.zoom ?? "?"}%; it must not act` : `${where} zoomed to ${after.zoom ?? "?"}% from ${before}%, expected ${expected}% (exactly one step)`
4174
+ );
4175
+ }
4176
+ current = after.zoom ?? current;
4177
+ };
4178
+ const inside = async (kind, context, steps) => {
4179
+ if (await inPage(page, injectProbe, { root, kind })) await steps();
4180
+ else problems.push(`cannot focus ${context} stand-in inside the root`);
4181
+ await inPage(page, removeInjectedProbe, null);
4182
+ };
4183
+ await press("Control+Digit0", "handled", 100);
4184
+ const pairs = [
4185
+ ["Control+Equal", "Control+Minus"],
4186
+ ["Control+Shift+Equal", "Control+Shift+Minus"],
4187
+ ["Meta+Equal", "Meta+Minus"],
4188
+ ["Meta+Shift+Equal", "Meta+Shift+Minus"],
4189
+ ["Control+NumpadAdd", "Control+NumpadSubtract"],
4190
+ ["Meta+NumpadAdd", "Meta+NumpadSubtract"]
4191
+ ];
4192
+ for (const [zoomIn, zoomOut] of pairs) {
4193
+ await press(zoomIn, "handled", ONE_STEP_UP);
4194
+ await press(zoomOut, "handled", 100);
4195
+ }
4196
+ for (const zoomToActual of ["Meta+Digit0", "Control+Numpad0"]) {
4197
+ await press("Control+Equal", "handled", ONE_STEP_UP);
4198
+ await press(zoomToActual, "handled", 100);
4199
+ }
4200
+ for (const key of ["Equal", "Minus", "Shift+Equal", "Shift+Minus", "NumpadAdd", "NumpadSubtract"]) {
4201
+ await press(key, "free", "unchanged", "on the canvas without Cmd/Ctrl");
4202
+ }
4203
+ for (const key of ["Digit0", "Control+Alt+Equal", "Control+Alt+Minus", "Control+Shift+Digit1"]) {
4204
+ await press(key, void 0, "unchanged");
4205
+ }
4206
+ await press("F1", "free", void 0);
4207
+ await press("Escape", "free", void 0, "on the canvas without a menu or dialog");
4208
+ if (await inPage(page, canvasHandlesProbe, { root, key: "F9" })) {
4209
+ await press("F9", "canvas", void 0, "handled by the canvas");
4210
+ }
4211
+ await inPage(page, removeInjectedProbe, null);
4212
+ if (host === "ide") {
4213
+ for (const key of UNDO_REDO_PRESSES) {
4214
+ await press(key, "free", void 0, "on the canvas (undo and redo belong to the VS Code document)");
4215
+ }
4216
+ }
4217
+ for (const key of ["Control+KeyA", "Meta+KeyA"]) {
4218
+ await press(key, "canvas", void 0, "on the canvas (select all acts on the canvas only)");
4219
+ }
4220
+ await press("Control+Equal", "handled", ONE_STEP_UP);
4221
+ for (const [kind, field] of TEXT_FIELDS) {
4222
+ await inside(kind, field, async () => {
4223
+ await press("Control+Equal", "blocked", "unchanged", `in ${field}`);
4224
+ await press("Control+Minus", "blocked", "unchanged", `in ${field}`);
4225
+ await press("Shift+Digit1", "free", "unchanged", `in ${field}`);
4226
+ await press("Control+KeyZ", "blocked", void 0, `in ${field}`);
4227
+ await press("Meta+Shift+KeyZ", "blocked", void 0, `in ${field}`);
4228
+ await press("Control+KeyY", "blocked", void 0, `in ${field}`);
4229
+ });
4230
+ }
4231
+ await inside("modal", "a modal dialog", async () => {
4232
+ await press("Control+Equal", "blocked", "unchanged", "with a modal dialog open");
4233
+ await press("Shift+Digit1", "free", "unchanged", "with a modal dialog open");
4234
+ await press("Escape", "blocked", void 0, "with a modal dialog open");
4235
+ });
4236
+ await inside("popover", "a popover", async () => {
4237
+ await press("Control+Minus", "handled", 100, "in a popover");
4238
+ await press("Control+Equal", "handled", ONE_STEP_UP, "in a popover");
4239
+ await press("Escape", "blocked", void 0, "with a popover open");
4240
+ });
4241
+ if (await openMenuByClick(page, root)) {
4242
+ await inPage(page, clearKeysProbe, null);
4243
+ await press("Escape", "handled", void 0, "with the menu open");
4244
+ if (await findOverlay(page, root, "menu")) problems.push("Escape with the menu open did not close it");
4245
+ }
4246
+ await focusTarget(page, root, "canvas");
4247
+ await press("Control+Equal", "handled", TWO_STEPS_UP);
4248
+ await page.keyboard.press("Shift+Digit1");
4249
+ let fitted = await inPage(page, afterPressProbe, { root });
4250
+ const reachProblem = keyReachProblem("Shift+Digit1 on the canvas", "handled", host, fitted);
4251
+ if (reachProblem) problems.push(reachProblem);
4252
+ for (let poll = 0; (fitted.zoom === null || fitted.zoom > 100) && poll < 20; poll++) {
4253
+ await page.waitForTimeout(50);
4254
+ fitted = await inPage(page, afterPressProbe, { root });
4255
+ }
4256
+ if (fitted.zoom === null || fitted.zoom > 100) {
4257
+ problems.push(
4258
+ `Shift+Digit1 on the canvas left the zoom at ${fitted.zoom ?? "?"}%, expected a fit (at most 100%)`
4259
+ );
4260
+ }
4261
+ return finish("keys", host, problems, []);
4262
+ }
4263
+ };
4264
+ async function appearanceFacts(page, root) {
4265
+ const probe = () => inPage(page, appearanceProbe, { root, roles: MODE_ROLES });
4266
+ await hover(page, root, "zoom-in");
4267
+ const withTooltip = await probe();
4268
+ const opened = await openMenuByClick(page, root);
4269
+ const withMenu = await probe();
4270
+ if (opened) await closeByEscape(page);
4271
+ await page.mouse.move(PARK.x, PARK.y);
4272
+ if (!withTooltip || !withMenu) return withTooltip ?? withMenu;
4273
+ const names = new Set(withTooltip.parts.map((part) => part.name));
4274
+ return {
4275
+ ...withMenu,
4276
+ parts: [...withTooltip.parts, ...withMenu.parts.filter((part) => !names.has(part.name))]
4277
+ };
4278
+ }
4279
+ async function paletteColors(page, root) {
4280
+ const read = async () => {
4281
+ const facts = await inPage(page, paletteColorProbe, { root });
4282
+ return facts ? paletteColorProblems(facts) : [];
4283
+ };
4284
+ return pollUntil(page, read, (problems) => problems.length === 0, COLOR_WAIT_MS);
4285
+ }
4286
+ async function paletteColorWalk(page, root) {
4287
+ const problems = [];
4288
+ await page.mouse.move(PARK.x, PARK.y);
4289
+ problems.push(...await paletteColors(page, root));
4290
+ const tools = (await inPage(page, paletteEntriesProbe, { root })).filter(
4291
+ (entry) => isToolEntry(entry) && !entry.disabled
4292
+ );
4293
+ for (const tool of tools) {
4294
+ await page.mouse.move(tool.client.x, tool.client.y);
4295
+ problems.push(...await paletteColors(page, root));
4296
+ await page.mouse.move(PARK.x, PARK.y);
4297
+ }
4298
+ const armed = tools.find((tool) => tool.action === "lasso-tool") ?? tools.find((tool) => tool.action.endsWith("-tool"));
4299
+ if (armed) {
4300
+ await page.mouse.click(armed.client.x, armed.client.y);
4301
+ await settle(page);
4302
+ const pressed = (await inPage(page, paletteEntriesProbe, { root })).find(
4303
+ (entry) => entry.action === armed.action
4304
+ );
4305
+ if (!pressed?.pressed) {
4306
+ problems.push(`palette: clicking tool "${armed.action}" does not set aria-pressed="true" (guide \xA75.5)`);
4307
+ }
4308
+ problems.push(...await paletteColors(page, root));
4309
+ await page.mouse.move(PARK.x, PARK.y);
4310
+ problems.push(...await paletteColors(page, root));
4311
+ await closeByEscape(page);
4312
+ }
4313
+ return [...new Set(problems)];
4314
+ }
4315
+ function webMode(page) {
4316
+ return inPage(page, webModeProbe, { key: APPEARANCE_STORAGE_KEY });
4317
+ }
4318
+ var APPEARANCE_LABELS = {
4319
+ system: "System",
4320
+ light: "Light",
4321
+ dark: "Dark"
4322
+ };
4323
+ async function appearanceSwitchProblems(page, harness, root, requested) {
4324
+ const problems = [];
4325
+ const system = await webMode(page);
4326
+ const media = (scheme) => system.themeColors.find((meta) => meta.media?.replace(/\s+/g, "") === `(prefers-color-scheme:${scheme})`)?.content;
4327
+ const light = media("light");
4328
+ const dark = media("dark");
4329
+ const colors = light && dark ? { light, dark } : void 0;
4330
+ for (const choice of ["dark", "light", "system"]) {
4331
+ const label = APPEARANCE_LABELS[choice];
4332
+ if (!await openMenuByClick(page, root)) {
4333
+ problems.push(`menu: no visible menu trigger to choose ${label}`);
4334
+ break;
4335
+ }
4336
+ const option = await inPage(page, targetProbe, {
4337
+ root,
4338
+ selector: `[data-menu-id="appearance"] [data-menu-value="${choice}"]`
4339
+ });
4340
+ if (!option) {
4341
+ problems.push(`menu: no Appearance option "${choice}" to choose (guide \xA713.3)`);
4342
+ await closeByEscape(page);
4343
+ break;
4344
+ }
4345
+ await inPage(page, watchModeProbe, null);
4346
+ await page.mouse.click(option.client.x, option.client.y);
4347
+ await settle(page);
4348
+ problems.push(...modeSwitchProblems(await inPage(page, modeChangesProbe, null), label));
4349
+ problems.push(
4350
+ ...appearanceChoiceProblems(await inPage(page, menuEntriesProbe, { root }) ?? [], choice)
4351
+ );
4352
+ await closeByEscape(page);
4353
+ problems.push(...webModeProblems(await webMode(page), harness.themes, choice, colors));
4354
+ const facts = await appearanceFacts(page, root);
4355
+ if (facts) {
4356
+ const expected = expectedWebMode(requested, harness.themes, choice);
4357
+ problems.push(
4358
+ ...appearanceProblems(facts, "web", requested, harness.themes, expected).problems.map(
4359
+ (problem) => `[chose ${label}] ${problem}`
4360
+ )
4361
+ );
4362
+ }
4363
+ }
4364
+ return problems;
4365
+ }
4366
+ var appearanceCase = {
4367
+ name: "appearance",
4368
+ async run(page, harness) {
4369
+ const root = harness.root ?? DEFAULT_ROOT;
4370
+ const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
4371
+ const problems = [];
4372
+ const skipped = [];
4373
+ const web = harness.host === "web";
4374
+ const emulated = !web || page.emulateMedia !== void 0;
4375
+ if (!emulated) {
4376
+ skipped.push({
4377
+ hook: "open",
4378
+ reason: "page.emulateMedia is undefined: the web app's mode under a dark system and under high contrast was not verified"
4379
+ });
4380
+ }
4381
+ let switched = false;
4382
+ for (const requested of emulated ? REQUESTED_APPEARANCES : [LIGHT]) {
4383
+ const at = `[requested ${appearanceLabel(requested)}]`;
4384
+ await openHarness(page, harness, { size, requested });
4385
+ if (harness.host === "embedded") {
4386
+ await inPage(page, hostPageModeProbe, {
4387
+ theme: requested.theme,
4388
+ kontrast: requested.kontrast ?? null
4389
+ });
4390
+ }
4391
+ await settle(page);
4392
+ const facts = await appearanceFacts(page, root);
4393
+ if (!facts) {
4394
+ problems.push(`${at} no element matches the shell root selector "${root}"`);
4395
+ continue;
4396
+ }
4397
+ const expected = web ? expectedWebMode(requested, harness.themes, "system") : void 0;
4398
+ const verdict = appearanceProblems(facts, harness.host, requested, harness.themes, expected);
4399
+ problems.push(...verdict.problems.map((problem) => `${at} ${problem}`));
4400
+ const surround = await inPage(page, surroundProbe, { root });
4401
+ if (surround) problems.push(...surroundProblems(surround).map((problem) => `${at} ${problem}`));
4402
+ problems.push(...(await paletteColorWalk(page, root)).map((problem) => `${at} ${problem}`));
4403
+ if (web) {
4404
+ problems.push(
4405
+ ...webModeProblems(await webMode(page), harness.themes, "system", void 0).map(
4406
+ (problem) => `${at} ${problem}`
4407
+ ),
4408
+ ...lightOnlyMenuProblems(await menuEntries(page, root), harness.themes).map(
4409
+ (problem) => `${at} ${problem}`
4410
+ )
4411
+ );
4412
+ if (harness.themes.includes("dark") && !switched && requested.theme === "dark") {
4413
+ switched = true;
4414
+ problems.push(
4415
+ ...(await appearanceSwitchProblems(page, harness, root, requested)).map(
4416
+ (problem) => `${at} ${problem}`
4417
+ )
4418
+ );
4419
+ }
4420
+ }
4421
+ }
4422
+ return finish("appearance", harness.host, problems, skipped);
4423
+ }
4424
+ };
4425
+ async function focusPaletteEntry(page, root, action) {
4426
+ const before = await inPage(page, paletteFocusProbe, { root });
4427
+ if (!before || !before.entries.includes(action)) return false;
4428
+ if (!await focusTarget(page, root, "palette-tab-stop")) return false;
4429
+ await page.keyboard.press("Home");
4430
+ for (let step = 0; step <= before.entries.length; step++) {
4431
+ if ((await inPage(page, paletteFocusProbe, { root }))?.active === action) return true;
4432
+ await page.keyboard.press("ArrowRight");
4433
+ }
4434
+ return false;
4435
+ }
4436
+ function firstCreatingEntry(entries) {
4437
+ return entries.find((entry) => !isToolEntry(entry));
4438
+ }
4439
+ async function enterOnEntry(page, root, entry, before) {
4440
+ if (!await focusPaletteEntry(page, root, entry)) return void 0;
4441
+ await page.keyboard.press("Enter");
4442
+ await settle(page);
4443
+ const known = new Set(before.map((shape) => shape.id));
4444
+ return pollUntil(
4445
+ page,
4446
+ () => shapes(page, root),
4447
+ (after) => after.some((shape) => !known.has(shape.id)),
4448
+ CREATE_WAIT_MS
4449
+ );
4450
+ }
4451
+ var createCase = {
4452
+ name: "create",
4453
+ async run(page, harness) {
4454
+ const root = harness.root ?? DEFAULT_ROOT;
4455
+ const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
4456
+ await openHarness(page, harness, { size, requested: LIGHT });
4457
+ await settle(page);
4458
+ const entry = firstCreatingEntry(await inPage(page, paletteEntriesProbe, { root }));
4459
+ if (!entry) return void 0;
4460
+ await page.mouse.move(PARK.x, PARK.y);
4461
+ const free = freeRectangle(await measureChrome(page, root), harness.host);
4462
+ const before = await shapes(page, root);
4463
+ const after = await enterOnEntry(page, root, entry.action, before);
4464
+ if (!after) {
4465
+ fail("create", harness.host, [`create: the palette's roving focus does not reach "${entry.action}"`]);
4466
+ }
4467
+ const canvas = await inPage(page, canvasStateProbe, { root });
4468
+ const problems = createProblems({ entry: entry.action, before, after, canvas, free });
4469
+ await closeByEscape(page);
4470
+ return finish("create", harness.host, problems, []);
4471
+ }
4472
+ };
4473
+ async function reachableShape(page, root, id) {
4474
+ const all = await shapes(page, root);
4475
+ return id === void 0 ? all.find((shape) => shape.hit) : all.find((shape) => shape.id === id);
4476
+ }
4477
+ async function lockedShapeChecks(page, root, shape) {
4478
+ await page.mouse.click(shape.client.x, shape.client.y);
4479
+ await settle(page);
4480
+ const selected = await inPage(page, canvasStateProbe, { root });
4481
+ const before = await reachableShape(page, root, shape.id) ?? shape;
4482
+ await page.mouse.move(before.client.x, before.client.y);
4483
+ await page.mouse.down();
4484
+ for (let step = 1; step <= 4; step++) {
4485
+ await page.mouse.move(before.client.x + LOCKED_DRAG_PX * step / 4, before.client.y);
4486
+ }
4487
+ await page.mouse.up();
4488
+ await settle(page);
4489
+ const dragged = await reachableShape(page, root, shape.id);
4490
+ const target = dragged ?? before;
4491
+ await page.mouse.dblclick(target.client.x, target.client.y);
4492
+ await settle(page);
4493
+ const { textFields } = await inPage(page, canvasStateProbe, { root });
4494
+ await closeByEscape(page);
4495
+ const kept = { Delete: true, Backspace: true };
4496
+ for (const key of ["Delete", "Backspace"]) {
4497
+ const current = await reachableShape(page, root, shape.id);
4498
+ if (!current) break;
4499
+ if (!current.selected) await page.mouse.click(current.client.x, current.client.y);
4500
+ await focusTarget(page, root, "canvas");
4501
+ await page.keyboard.press(key);
4502
+ await settle(page);
4503
+ kept[key] = await reachableShape(page, root, shape.id) !== void 0;
4504
+ }
4505
+ await page.mouse.move(PARK.x, PARK.y);
4506
+ return lockedShapeProblems({
4507
+ contextPadEntries: selected.contextPadEntries,
4508
+ dragged: { before, after: dragged },
4509
+ textFields,
4510
+ kept
4511
+ });
4512
+ }
4513
+ var readOnlyCase = {
4514
+ name: "readOnly",
4515
+ async run(page, harness) {
4516
+ assertHooksConsistent("readOnly", harness, ["invalid"]);
4517
+ const invalid = harness.show?.invalid;
4518
+ if (!invalid) {
4519
+ if (harness.absent?.includes("invalid")) return void 0;
4520
+ return {
4521
+ skipped: [
4522
+ {
4523
+ hook: "invalid",
4524
+ reason: "show.invalid is undefined: the read-only canvas of an invalid file was not verified"
4525
+ }
4526
+ ]
4527
+ };
4528
+ }
4529
+ const root = harness.root ?? DEFAULT_ROOT;
4530
+ const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
4531
+ const problems = [];
4532
+ const skipped = [];
4533
+ await openHarness(page, harness, { size, requested: LIGHT });
4534
+ await settle(page);
4535
+ const menuBefore = await menuEntries(page, root);
4536
+ await invalid(page);
4537
+ await page.waitForTimeout(INVALID_STATUS_DELAY_MS);
4538
+ const state = await pollUntil(
4539
+ page,
4540
+ () => inPage(page, readOnlyStateProbe, { root }),
4541
+ (facts) => facts === null || readOnlyStateProblems(facts).length === 0,
4542
+ READ_ONLY_WAIT_MS
4543
+ );
4544
+ if (!state) fail("readOnly", harness.host, [`no element matches the shell root selector "${root}"`]);
4545
+ await settle(page);
4546
+ problems.push(...readOnlyStateProblems(state));
4547
+ const entries = await inPage(page, paletteEntriesProbe, { root });
4548
+ problems.push(...readOnlyPaletteProblems(entries));
4549
+ problems.push(...await paletteColors(page, root));
4550
+ const creating = firstCreatingEntry(entries);
4551
+ if (creating) {
4552
+ const before = await shapes(page, root);
4553
+ const after = await enterOnEntry(page, root, creating.action, before);
4554
+ const known = new Set(before.map((shape2) => shape2.id));
4555
+ const added = (after ?? []).filter((shape2) => !known.has(shape2.id));
4556
+ if (added.length > 0) {
4557
+ problems.push(
4558
+ `read-only: Enter on palette entry "${creating.action}" added ${added.map((shape2) => `"${shape2.id}"`).join(", ")} (guide \xA713.5)`
4559
+ );
4560
+ }
4561
+ await closeByEscape(page);
4562
+ }
4563
+ problems.push(...readOnlyMenuProblems(menuBefore, await menuEntries(page, root)));
4564
+ const shape = await reachableShape(page, root);
4565
+ if (shape) {
4566
+ problems.push(...await lockedShapeChecks(page, root, shape));
4567
+ } else {
4568
+ skipped.push({
4569
+ hook: "open",
4570
+ reason: "the opened document shows no shape a pointer reaches: context pad, move, delete and label editor of the read-only canvas not verified"
4571
+ });
4572
+ }
4573
+ return finish("readOnly", harness.host, problems, skipped);
4574
+ }
4575
+ };
4576
+ var cleanupCase = {
4577
+ name: "cleanup",
4578
+ async run(page, harness) {
4579
+ if (!harness.destroy) {
4580
+ return {
4581
+ skipped: [
4582
+ {
4583
+ hook: "destroy",
4584
+ reason: "harness.destroy is undefined: the cleanup after destroy was not verified"
4585
+ }
4586
+ ]
4587
+ };
4588
+ }
4589
+ const root = harness.root ?? DEFAULT_ROOT;
4590
+ const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
4591
+ await openHarness(page, harness, { size, requested: LIGHT });
4592
+ await settle(page);
4593
+ const armed = await inPage(page, armCleanupProbe, { root });
4594
+ if (!armed) fail("cleanup", harness.host, [`no element matches the shell root selector "${root}"`]);
4595
+ const problems = [];
4596
+ const zoomIn = await findTarget(page, root, "zoom-in");
4597
+ if (zoomIn) {
4598
+ await page.mouse.move(PARK.x, PARK.y);
4599
+ await page.mouse.move(zoomIn.client.x, zoomIn.client.y);
4600
+ }
4601
+ try {
4602
+ await harness.destroy(page);
4603
+ } catch (error) {
4604
+ problems.push(`destroy threw: ${error instanceof Error ? error.message : String(error)}`);
4605
+ }
4606
+ await page.waitForTimeout(Math.max(INVALID_STATUS_DELAY_MS, TOOLTIP_DELAY_MS) + HOVER_MARGIN_MS);
4607
+ await page.mouse.move(PARK.x, PARK.y);
4608
+ await page.setViewportSize({ width: size.width - 40, height: size.height - 40 });
4609
+ await settle(page);
4610
+ await inPage(page, injectProbe, { root, kind: "focus-target" });
4611
+ await inPage(page, clearKeysProbe, null);
4612
+ for (const key of ["Control+Equal", "Control+Minus", "Control+Digit0", "Shift+Digit1"]) {
4613
+ await page.keyboard.press(key);
4614
+ const { keys } = await inPage(page, afterPressProbe, { root });
4615
+ const problem = afterDestroyKeyProblem(key, keys);
4616
+ if (problem) problems.push(problem);
4617
+ }
4618
+ await inPage(page, removeInjectedProbe, null);
4619
+ problems.push(...leftoverProblems(await inPage(page, leftoverProbe, null), harness.host));
4620
+ return finish("cleanup", harness.host, problems, []);
4621
+ }
4622
+ };
4623
+ function createBaseCases() {
4624
+ return [geometryCase, ariaCase, keysCase, createCase, readOnlyCase, appearanceCase, cleanupCase];
4625
+ }
4626
+ function createCases() {
4627
+ return [...createBaseCases(), ...createWebCases()];
4628
+ }
4629
+
4630
+ // src/testing/harness.ts
4631
+ function shellConformanceCases() {
4632
+ return createCases();
4633
+ }
4634
+ function vsCodeStubSearch(requested, extra = {}) {
4635
+ const query = new URLSearchParams({ theme: requested.theme });
4636
+ if (requested.kontrast) query.set("kontrast", requested.kontrast);
4637
+ if (extra.colorTheme) query.set("colorTheme", extra.colorTheme);
4638
+ if (extra.reduceMotion) query.set("reduceMotion", "");
4639
+ return query.toString();
4640
+ }
4641
+ function registerShellConformance(harness, test, options = {}) {
4642
+ const onSkip = options.onSkip ?? ((caseName, skips) => {
4643
+ console.warn(
4644
+ `@miragon/modeler-shell conformance: SKIPPED "${caseName}" (${harness.host} host): ${skips.map((skip) => skip.reason).join("; ")}`
4645
+ );
4646
+ });
4647
+ for (const conformanceCase of shellConformanceCases()) {
4648
+ test(`shell conformance: ${conformanceCase.name}`, async ({ page }) => {
4649
+ const outcome = await conformanceCase.run(page, harness);
4650
+ if (outcome?.skipped && outcome.skipped.length > 0) onSkip(conformanceCase.name, outcome.skipped);
4651
+ });
4652
+ }
4653
+ }
4654
+ // Annotate the CommonJS export names for ESM import in node:
4655
+ 0 && (module.exports = {
4656
+ GEOMETRY_TOLERANCE_PX,
4657
+ expectedChromeBoxes,
4658
+ measureChrome,
4659
+ registerShellConformance,
4660
+ shellConformanceCases,
4661
+ vsCodeStubSearch
4662
+ });
4663
+ //# sourceMappingURL=testing.cjs.map