@miragon/modeler-shell 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +1902 -0
- package/LICENSE +21 -0
- package/README.md +435 -2
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/dist/appearance-D3FUcbu6.d.cts +53 -0
- package/dist/appearance-D3FUcbu6.d.ts +53 -0
- package/dist/chunk-5JSZ4VGA.js +551 -0
- package/dist/chunk-5JSZ4VGA.js.map +1 -0
- package/dist/chunk-6LQOFACK.js +1576 -0
- package/dist/chunk-6LQOFACK.js.map +1 -0
- package/dist/chunk-N4QXFF3A.js +23 -0
- package/dist/chunk-N4QXFF3A.js.map +1 -0
- package/dist/chunk-RJRFR6N3.js +136 -0
- package/dist/chunk-RJRFR6N3.js.map +1 -0
- package/dist/core.cjs +619 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +171 -0
- package/dist/core.d.ts +171 -0
- package/dist/core.js +91 -0
- package/dist/core.js.map +1 -0
- package/dist/diagram-js.cjs +1115 -0
- package/dist/diagram-js.cjs.map +1 -0
- package/dist/diagram-js.d.cts +163 -0
- package/dist/diagram-js.d.ts +163 -0
- package/dist/diagram-js.js +867 -0
- package/dist/diagram-js.js.map +1 -0
- package/dist/index.cjs +2175 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +103 -0
- package/dist/index.js.map +1 -0
- package/dist/keymap-CObQE6q6.d.cts +151 -0
- package/dist/keymap-DORzHnK-.d.ts +151 -0
- package/dist/protocol-DhYBakWL.d.cts +15 -0
- package/dist/protocol-IeW8bf4f.d.ts +15 -0
- package/dist/shell.css +914 -0
- package/dist/testing-vscode-stub.cjs +332 -0
- package/dist/testing-vscode-stub.cjs.map +1 -0
- package/dist/testing-vscode-stub.d.cts +65 -0
- package/dist/testing-vscode-stub.d.ts +65 -0
- package/dist/testing-vscode-stub.js +154 -0
- package/dist/testing-vscode-stub.js.map +1 -0
- package/dist/testing.cjs +2364 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +165 -0
- package/dist/testing.d.ts +165 -0
- package/dist/testing.js +2297 -0
- package/dist/testing.js.map +1 -0
- package/dist/types-B6NfkiUG.d.cts +66 -0
- package/dist/types-B6NfkiUG.d.ts +66 -0
- package/dist/types-DnZn9CPw.d.ts +284 -0
- package/dist/types-OuFlqZp0.d.cts +284 -0
- package/dist/vscode-extension.cjs +215 -0
- package/dist/vscode-extension.cjs.map +1 -0
- package/dist/vscode-extension.d.cts +103 -0
- package/dist/vscode-extension.d.ts +103 -0
- package/dist/vscode-extension.js +24 -0
- package/dist/vscode-extension.js.map +1 -0
- package/dist/vscode-webview.cjs +2198 -0
- package/dist/vscode-webview.cjs.map +1 -0
- package/dist/vscode-webview.d.cts +82 -0
- package/dist/vscode-webview.d.ts +82 -0
- package/dist/vscode-webview.js +250 -0
- package/dist/vscode-webview.js.map +1 -0
- package/package.json +145 -4
- package/src/core/appearance.ts +104 -0
- package/src/core/fit.ts +205 -0
- package/src/core/index.ts +84 -0
- package/src/core/keymap.ts +275 -0
- package/src/core/layout.ts +69 -0
- package/src/core/messages.ts +88 -0
- package/src/core/sizes.ts +85 -0
- package/src/core/types.ts +84 -0
- package/src/core/zoom.ts +74 -0
- package/src/core.ts +2 -0
- package/src/diagram-js/connect.ts +75 -0
- package/src/diagram-js/index.ts +16 -0
- package/src/diagram-js/module.ts +20 -0
- package/src/diagram-js/palette-skin.ts +267 -0
- package/src/diagram-js/palette.css +269 -0
- package/src/diagram-js/service.ts +240 -0
- package/src/diagram-js/types.ts +67 -0
- package/src/diagram-js/viewport.ts +564 -0
- package/src/diagram-js.ts +2 -0
- package/src/dom/element.ts +51 -0
- package/src/dom/hint.ts +51 -0
- package/src/dom/icons.ts +309 -0
- package/src/dom/index.ts +29 -0
- package/src/dom/inspector.ts +35 -0
- package/src/dom/keys.ts +59 -0
- package/src/dom/ledger.ts +87 -0
- package/src/dom/legend.ts +85 -0
- package/src/dom/menu.ts +393 -0
- package/src/dom/mount.ts +461 -0
- package/src/dom/problems.ts +72 -0
- package/src/dom/shell-dom.css +966 -0
- package/src/dom/status.ts +72 -0
- package/src/dom/tooltip.ts +237 -0
- package/src/dom/types.ts +277 -0
- package/src/dom/zoom-panel.ts +154 -0
- package/src/index.ts +3 -0
- package/src/shell.css +6 -0
- package/src/testing/cases.ts +1012 -0
- package/src/testing/checks.ts +832 -0
- package/src/testing/facts.ts +587 -0
- package/src/testing/geometry.ts +472 -0
- package/src/testing/harness.ts +138 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/page-tools.ts +434 -0
- package/src/testing/page.ts +17 -0
- package/src/testing/vscode-stub.ts +237 -0
- package/src/testing-vscode-stub.ts +2 -0
- package/src/testing.ts +2 -0
- package/src/vscode/extension/html.ts +107 -0
- package/src/vscode/extension/index.ts +8 -0
- package/src/vscode/extension/reveal.ts +9 -0
- package/src/vscode/extension/theme-bridge.ts +85 -0
- package/src/vscode/extension/types.ts +36 -0
- package/src/vscode/protocol.ts +31 -0
- package/src/vscode/webview/appearance.ts +141 -0
- package/src/vscode/webview/index.ts +8 -0
- package/src/vscode/webview/mount.ts +76 -0
- package/src/vscode/webview/state.ts +116 -0
- package/src/vscode-extension.ts +2 -0
- package/src/vscode-webview.ts +2 -0
package/dist/testing.js
ADDED
|
@@ -0,0 +1,2297 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CHROME_ISLANDS,
|
|
3
|
+
INVALID_STATUS_DELAY_MS,
|
|
4
|
+
PALETTE_CLEARANCE,
|
|
5
|
+
SHELL,
|
|
6
|
+
TOOLTIP_DELAY_MS
|
|
7
|
+
} from "./chunk-5JSZ4VGA.js";
|
|
8
|
+
|
|
9
|
+
// src/testing/page-tools.ts
|
|
10
|
+
var installPageTools = (_) => {
|
|
11
|
+
const scope = window;
|
|
12
|
+
if (scope.__modelerShellConformance) return;
|
|
13
|
+
const modifierKeys = ["Control", "Meta", "Shift", "Alt", "AltGraph", "CapsLock", "Fn"];
|
|
14
|
+
const frame = () => new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
15
|
+
const borders = (element) => {
|
|
16
|
+
const style = getComputedStyle(element);
|
|
17
|
+
return {
|
|
18
|
+
left: parseFloat(style.borderLeftWidth) || 0,
|
|
19
|
+
top: parseFloat(style.borderTopWidth) || 0,
|
|
20
|
+
right: parseFloat(style.borderRightWidth) || 0,
|
|
21
|
+
bottom: parseFloat(style.borderBottomWidth) || 0
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
const visible = (element) => {
|
|
25
|
+
if (!element.isConnected) return false;
|
|
26
|
+
if (typeof element.checkVisibility === "function") {
|
|
27
|
+
if (!element.checkVisibility({ visibilityProperty: true })) return false;
|
|
28
|
+
} else {
|
|
29
|
+
if (element.closest("[hidden]")) return false;
|
|
30
|
+
for (let node = element; node; node = node.parentElement) {
|
|
31
|
+
if (getComputedStyle(node).display === "none") return false;
|
|
32
|
+
}
|
|
33
|
+
if (getComputedStyle(element).visibility === "hidden") return false;
|
|
34
|
+
}
|
|
35
|
+
const rect = element.getBoundingClientRect();
|
|
36
|
+
return rect.width > 0 && rect.height > 0;
|
|
37
|
+
};
|
|
38
|
+
const describe = (element) => {
|
|
39
|
+
const id = element.id ? `#${element.id}` : "";
|
|
40
|
+
const classes = [...element.classList].slice(0, 3).map((name2) => `.${name2}`).join("");
|
|
41
|
+
const chrome = element.getAttribute("data-chrome");
|
|
42
|
+
return `${element.tagName.toLowerCase()}${id}${classes}${chrome ? `[data-chrome="${chrome}"]` : ""}`;
|
|
43
|
+
};
|
|
44
|
+
const tools = {
|
|
45
|
+
keys: [],
|
|
46
|
+
documentKeys: [],
|
|
47
|
+
errors: [],
|
|
48
|
+
injected: [],
|
|
49
|
+
cleanups: [],
|
|
50
|
+
frame,
|
|
51
|
+
async settle() {
|
|
52
|
+
await frame();
|
|
53
|
+
await frame();
|
|
54
|
+
const running = typeof document.getAnimations === "function" ? document.getAnimations().filter((animation) => {
|
|
55
|
+
const end = animation.effect?.getComputedTiming().endTime;
|
|
56
|
+
return animation.playState === "running" && typeof end === "number" && Number.isFinite(end);
|
|
57
|
+
}) : [];
|
|
58
|
+
if (running.length > 0) {
|
|
59
|
+
await Promise.race([
|
|
60
|
+
Promise.allSettled(running.map((animation) => animation.finished)),
|
|
61
|
+
new Promise((resolve) => setTimeout(resolve, 2e3))
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
await frame();
|
|
65
|
+
},
|
|
66
|
+
visible,
|
|
67
|
+
container(root) {
|
|
68
|
+
const rect = root.getBoundingClientRect();
|
|
69
|
+
const border = borders(root);
|
|
70
|
+
return {
|
|
71
|
+
width: rect.width - border.left - border.right,
|
|
72
|
+
height: rect.height - border.top - border.bottom
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
box(element, root) {
|
|
76
|
+
const rect = element.getBoundingClientRect();
|
|
77
|
+
const origin = root.getBoundingClientRect();
|
|
78
|
+
const border = borders(root);
|
|
79
|
+
return {
|
|
80
|
+
left: rect.left - origin.left - border.left,
|
|
81
|
+
top: rect.top - origin.top - border.top,
|
|
82
|
+
width: rect.width,
|
|
83
|
+
height: rect.height
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
describe,
|
|
87
|
+
facts(element) {
|
|
88
|
+
const attributes = {};
|
|
89
|
+
for (const name2 of element.getAttributeNames()) attributes[name2] = element.getAttribute(name2) ?? "";
|
|
90
|
+
return {
|
|
91
|
+
tag: element.tagName.toLowerCase(),
|
|
92
|
+
description: describe(element),
|
|
93
|
+
attributes,
|
|
94
|
+
text: (element.textContent ?? "").replace(/\s+/g, " ").trim(),
|
|
95
|
+
visible: visible(element),
|
|
96
|
+
disabled: Boolean(element.disabled)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const recorder = (into) => (event) => {
|
|
101
|
+
if (modifierKeys.includes(event.key)) return;
|
|
102
|
+
into.push({
|
|
103
|
+
key: event.key,
|
|
104
|
+
code: event.code,
|
|
105
|
+
ctrlKey: event.ctrlKey,
|
|
106
|
+
metaKey: event.metaKey,
|
|
107
|
+
shiftKey: event.shiftKey,
|
|
108
|
+
altKey: event.altKey,
|
|
109
|
+
defaultPrevented: event.defaultPrevented
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
document.addEventListener("keydown", recorder(tools.documentKeys));
|
|
113
|
+
window.addEventListener("keydown", recorder(tools.keys));
|
|
114
|
+
window.addEventListener("error", (event) => {
|
|
115
|
+
tools.errors.push(event.message || String(event.error));
|
|
116
|
+
});
|
|
117
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
118
|
+
tools.errors.push(`unhandled rejection: ${String(event.reason)}`);
|
|
119
|
+
});
|
|
120
|
+
scope.__modelerShellConformance = tools;
|
|
121
|
+
};
|
|
122
|
+
async function inPage(page, pageFunction, arg) {
|
|
123
|
+
await page.evaluate(installPageTools, null);
|
|
124
|
+
return page.evaluate(pageFunction, arg);
|
|
125
|
+
}
|
|
126
|
+
var settleInPage = (_) => window.__modelerShellConformance.settle();
|
|
127
|
+
function settle(page) {
|
|
128
|
+
return inPage(page, settleInPage, null);
|
|
129
|
+
}
|
|
130
|
+
var CHROME_TARGET_SELECTORS = {
|
|
131
|
+
menu: '[data-chrome="menu"]',
|
|
132
|
+
"palette-entry": '[data-chrome="palette"] [data-action]',
|
|
133
|
+
"palette-tab-stop": '[data-chrome="palette"] [data-action][tabindex="0"]',
|
|
134
|
+
"zoom-out": '[data-chrome="zoom"] [data-zoom="out"]',
|
|
135
|
+
"zoom-actual": '[data-chrome="zoom"] [data-zoom="actual"]',
|
|
136
|
+
"zoom-in": '[data-chrome="zoom"] [data-zoom="in"]',
|
|
137
|
+
"zoom-fit": '[data-chrome="zoom"] [data-zoom="fit"]',
|
|
138
|
+
canvas: ".djs-container > svg"
|
|
139
|
+
};
|
|
140
|
+
var targetProbe = ({ root, selector }) => {
|
|
141
|
+
const tools = window.__modelerShellConformance;
|
|
142
|
+
const rootElement = document.querySelector(root);
|
|
143
|
+
if (!rootElement) return null;
|
|
144
|
+
const element = [...rootElement.querySelectorAll(selector)].find((candidate) => tools.visible(candidate));
|
|
145
|
+
if (!element) return null;
|
|
146
|
+
const rect = element.getBoundingClientRect();
|
|
147
|
+
return {
|
|
148
|
+
client: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
149
|
+
box: tools.box(element, rootElement),
|
|
150
|
+
label: (element.getAttribute("aria-label") ?? element.textContent ?? "").replace(/\s+/g, " ").trim()
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
async function findTarget(page, root, target) {
|
|
154
|
+
return inPage(page, targetProbe, { root, selector: CHROME_TARGET_SELECTORS[target] });
|
|
155
|
+
}
|
|
156
|
+
var focusProbe = ({ root, selector }) => {
|
|
157
|
+
const tools = window.__modelerShellConformance;
|
|
158
|
+
const element = document.querySelector(root)?.querySelector(selector);
|
|
159
|
+
if (!element) return null;
|
|
160
|
+
element.focus();
|
|
161
|
+
return document.activeElement === element ? tools.describe(element) : null;
|
|
162
|
+
};
|
|
163
|
+
async function focusTarget(page, root, target) {
|
|
164
|
+
return inPage(page, focusProbe, { root, selector: CHROME_TARGET_SELECTORS[target] });
|
|
165
|
+
}
|
|
166
|
+
var overlayProbe = ({ root, role }) => {
|
|
167
|
+
const tools = window.__modelerShellConformance;
|
|
168
|
+
const rootElement = document.querySelector(root);
|
|
169
|
+
if (!rootElement) return null;
|
|
170
|
+
const shown = [...rootElement.querySelectorAll(`[role="${role}"]`)].filter(
|
|
171
|
+
(element) => tools.visible(element)
|
|
172
|
+
);
|
|
173
|
+
const first = shown[0];
|
|
174
|
+
if (!first) return null;
|
|
175
|
+
return {
|
|
176
|
+
box: tools.box(first, rootElement),
|
|
177
|
+
text: (first.textContent ?? "").replace(/\s+/g, " ").trim(),
|
|
178
|
+
count: shown.length,
|
|
179
|
+
id: first.id
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
async function findOverlay(page, root, role) {
|
|
183
|
+
return inPage(page, overlayProbe, { root, role });
|
|
184
|
+
}
|
|
185
|
+
var afterPressProbe = async ({ root }) => {
|
|
186
|
+
const tools = window.__modelerShellConformance;
|
|
187
|
+
await tools.settle();
|
|
188
|
+
const readout = document.querySelector(root)?.querySelector('[data-chrome="zoom"] [data-zoom="actual"]')?.textContent;
|
|
189
|
+
const digits = (readout ?? "").replace(/[^0-9]/g, "");
|
|
190
|
+
return {
|
|
191
|
+
zoom: digits ? Number(digits) : null,
|
|
192
|
+
keys: tools.keys.splice(0),
|
|
193
|
+
documentKeys: tools.documentKeys.splice(0)
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
var clearKeysProbe = (_) => {
|
|
197
|
+
const tools = window.__modelerShellConformance;
|
|
198
|
+
tools.keys.splice(0);
|
|
199
|
+
tools.documentKeys.splice(0);
|
|
200
|
+
};
|
|
201
|
+
var injectProbe = ({ root, kind }) => {
|
|
202
|
+
const tools = window.__modelerShellConformance;
|
|
203
|
+
const parent = document.querySelector(root) ?? tools.armedRoot ?? document.body;
|
|
204
|
+
const host = parent.isConnected ? parent : document.body;
|
|
205
|
+
const element = kind === "input" || kind === "textarea" ? document.createElement(kind) : document.createElement("div");
|
|
206
|
+
element.setAttribute("data-modeler-shell-conformance", kind);
|
|
207
|
+
element.style.cssText = "position:absolute;left:0;top:0;width:40px;height:20px;";
|
|
208
|
+
if (kind === "modal" || kind === "popover") {
|
|
209
|
+
element.setAttribute("role", "dialog");
|
|
210
|
+
element.setAttribute("aria-label", "Conformance stand-in");
|
|
211
|
+
if (kind === "modal") element.setAttribute("aria-modal", "true");
|
|
212
|
+
}
|
|
213
|
+
if (kind === "contenteditable") element.setAttribute("contenteditable", "true");
|
|
214
|
+
else if (kind !== "input" && kind !== "textarea") element.tabIndex = -1;
|
|
215
|
+
host.append(element);
|
|
216
|
+
tools.injected.push(element);
|
|
217
|
+
element.focus();
|
|
218
|
+
return document.activeElement === element;
|
|
219
|
+
};
|
|
220
|
+
var removeInjectedProbe = (_) => {
|
|
221
|
+
const tools = window.__modelerShellConformance;
|
|
222
|
+
for (const element of tools.injected.splice(0)) element.remove();
|
|
223
|
+
for (const cleanup of tools.cleanups.splice(0)) cleanup();
|
|
224
|
+
};
|
|
225
|
+
var canvasHandlesProbe = ({ root, key }) => {
|
|
226
|
+
const tools = window.__modelerShellConformance;
|
|
227
|
+
const svg = document.querySelector(root)?.querySelector(".djs-container > svg");
|
|
228
|
+
if (!svg) return false;
|
|
229
|
+
const listener = (event) => {
|
|
230
|
+
if (event.key === key) event.preventDefault();
|
|
231
|
+
};
|
|
232
|
+
svg.addEventListener("keydown", listener);
|
|
233
|
+
tools.cleanups.push(() => svg.removeEventListener("keydown", listener));
|
|
234
|
+
return true;
|
|
235
|
+
};
|
|
236
|
+
var armCleanupProbe = ({ root }) => {
|
|
237
|
+
const tools = window.__modelerShellConformance;
|
|
238
|
+
const rootElement = document.querySelector(root);
|
|
239
|
+
if (rootElement) tools.armedRoot = rootElement;
|
|
240
|
+
tools.errors.splice(0);
|
|
241
|
+
tools.keys.splice(0);
|
|
242
|
+
tools.documentKeys.splice(0);
|
|
243
|
+
return Boolean(rootElement);
|
|
244
|
+
};
|
|
245
|
+
var leftoverProbe = (_) => {
|
|
246
|
+
const tools = window.__modelerShellConformance;
|
|
247
|
+
const ours = (element) => element.hasAttribute("data-modeler-shell-conformance");
|
|
248
|
+
const shellClass = (name2) => name2.startsWith("cd-shell");
|
|
249
|
+
const root = tools.armedRoot;
|
|
250
|
+
return {
|
|
251
|
+
classedElements: [...document.querySelectorAll("[class]")].filter((element) => element !== document.documentElement && !ours(element)).filter((element) => [...element.classList].some(shellClass)).map((element) => tools.describe(element)),
|
|
252
|
+
documentClasses: [...document.documentElement.classList].filter(shellClass),
|
|
253
|
+
chromeElements: [...document.querySelectorAll("[data-chrome]")].map((element) => tools.describe(element)),
|
|
254
|
+
rootClasses: root ? [...root.classList].filter(shellClass) : [],
|
|
255
|
+
rootAttributes: root ? root.getAttributeNames() : [],
|
|
256
|
+
errors: [...tools.errors]
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
var hostPageModeProbe = ({ theme, kontrast }) => {
|
|
260
|
+
const html = document.documentElement;
|
|
261
|
+
html.setAttribute("data-cd-theme", theme);
|
|
262
|
+
if (kontrast) html.setAttribute("data-cd-kontrast", kontrast);
|
|
263
|
+
else html.removeAttribute("data-cd-kontrast");
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// src/testing/geometry.ts
|
|
267
|
+
var GEOMETRY_TOLERANCE_PX = 1;
|
|
268
|
+
var DEFAULT_ROOT = ".cd-shell";
|
|
269
|
+
var ZOOM_PANEL_WIDTH = 173;
|
|
270
|
+
var LEGAL_LINE = 20;
|
|
271
|
+
var LEGEND_MIN_WIDTH = 184;
|
|
272
|
+
var STATUS_MAX_WIDTH = 560;
|
|
273
|
+
var DROPDOWN_MAX_WIDTH = 320;
|
|
274
|
+
var TOOLTIP_MAX_WIDTH = 280;
|
|
275
|
+
var SHEET_MAX_HEIGHT_SHARE = 0.45;
|
|
276
|
+
var measureProbe = ({ root, islands }) => {
|
|
277
|
+
const tools = window.__modelerShellConformance;
|
|
278
|
+
const rootElement = document.querySelector(root);
|
|
279
|
+
if (!rootElement) return { error: `no element matches the shell root selector "${root}"` };
|
|
280
|
+
const boxes = {};
|
|
281
|
+
const duplicates = [];
|
|
282
|
+
for (const element of rootElement.querySelectorAll("[data-chrome]")) {
|
|
283
|
+
const name2 = element.getAttribute("data-chrome") ?? "";
|
|
284
|
+
if (!islands.includes(name2) || !tools.visible(element)) continue;
|
|
285
|
+
if (boxes[name2]) {
|
|
286
|
+
duplicates.push(name2);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
boxes[name2] = tools.box(element, rootElement);
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
container: tools.container(rootElement),
|
|
293
|
+
paletteRow: rootElement.getAttribute("data-palette") === "second-row" ? "second-row" : "top",
|
|
294
|
+
islands: boxes,
|
|
295
|
+
open: {
|
|
296
|
+
legend: rootElement.getAttribute("data-legend") === "open",
|
|
297
|
+
inspector: rootElement.getAttribute("data-inspector") === "open",
|
|
298
|
+
problems: rootElement.getAttribute("data-problems") === "shown"
|
|
299
|
+
},
|
|
300
|
+
duplicates
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
async function measureChrome(page, root = DEFAULT_ROOT) {
|
|
304
|
+
const result = await inPage(page, measureProbe, { root, islands: CHROME_ISLANDS });
|
|
305
|
+
if ("error" in result) throw new Error(`@miragon/modeler-shell conformance: ${result.error}`);
|
|
306
|
+
if (result.duplicates.length > 0) {
|
|
307
|
+
const names = [...new Set(result.duplicates)].map((name2) => `[data-chrome="${name2}"]`).join(", ");
|
|
308
|
+
throw new Error(`@miragon/modeler-shell conformance: more than one visible ${names}`);
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
container: result.container,
|
|
312
|
+
paletteRow: result.paletteRow,
|
|
313
|
+
islands: result.islands,
|
|
314
|
+
open: result.open
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function isNarrow(container) {
|
|
318
|
+
return container.width <= SHELL.narrow;
|
|
319
|
+
}
|
|
320
|
+
function isVeryNarrow(container) {
|
|
321
|
+
return container.width <= SHELL.veryNarrow;
|
|
322
|
+
}
|
|
323
|
+
function sheetOpen(measurement) {
|
|
324
|
+
return measurement.open.inspector && isNarrow(measurement.container);
|
|
325
|
+
}
|
|
326
|
+
function chromeExpectations(measurement, host) {
|
|
327
|
+
const { container, islands, open, paletteRow } = measurement;
|
|
328
|
+
const { edge, gap, row, free, inspectorWidth } = SHELL;
|
|
329
|
+
const web = host === "web";
|
|
330
|
+
const secondRow = paletteRow === "second-row";
|
|
331
|
+
const sheet = sheetOpen(measurement);
|
|
332
|
+
const sideInspector = open.inspector && !isNarrow(container);
|
|
333
|
+
const boxes = {};
|
|
334
|
+
const limits = {};
|
|
335
|
+
const hidden = {};
|
|
336
|
+
const required = ["menu", "palette"];
|
|
337
|
+
boxes.menu = { left: edge, top: edge, width: row, height: row };
|
|
338
|
+
boxes.palette = { top: secondRow ? free : edge, height: row, centerX: container.width / 2 };
|
|
339
|
+
const menu = islands.menu;
|
|
340
|
+
limits.palette = secondRow ? { maxWidth: container.width - 2 * edge } : menu ? { minLeft: menu.left + menu.width + PALETTE_CLEARANCE } : {};
|
|
341
|
+
if (sheet) {
|
|
342
|
+
const reason = "hidden while the inspector is a bottom sheet (guide \xA75.11)";
|
|
343
|
+
hidden.zoom = reason;
|
|
344
|
+
hidden.problems = reason;
|
|
345
|
+
hidden.legend = reason;
|
|
346
|
+
} else {
|
|
347
|
+
required.push("zoom");
|
|
348
|
+
boxes.zoom = { right: edge, bottom: edge, width: ZOOM_PANEL_WIDTH, height: row };
|
|
349
|
+
const stackBase = web ? edge + LEGAL_LINE + gap : edge;
|
|
350
|
+
if (islands.problems) boxes.problems = { left: edge, bottom: stackBase, height: row };
|
|
351
|
+
if (islands.legend) {
|
|
352
|
+
const besidePill = open.problems ? stackBase + row + gap : stackBase;
|
|
353
|
+
const bottom = isVeryNarrow(container) ? Math.max(besidePill, edge + row + gap) : besidePill;
|
|
354
|
+
boxes.legend = { left: edge, bottom };
|
|
355
|
+
limits.legend = { minWidth: LEGEND_MIN_WIDTH };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (islands.inspector) {
|
|
359
|
+
if (sheet) {
|
|
360
|
+
boxes.inspector = { left: edge, right: edge, bottom: edge };
|
|
361
|
+
limits.inspector = { maxHeight: container.height * SHEET_MAX_HEIGHT_SHARE };
|
|
362
|
+
} else {
|
|
363
|
+
boxes.inspector = { top: free, right: edge, width: inspectorWidth };
|
|
364
|
+
limits.inspector = { minBottom: free };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (islands.status) {
|
|
368
|
+
const strip = sideInspector ? container.width - edge - inspectorWidth : container.width;
|
|
369
|
+
boxes.status = { top: secondRow ? free + row + edge : free, centerX: strip / 2 };
|
|
370
|
+
limits.status = {
|
|
371
|
+
minLeft: edge,
|
|
372
|
+
maxWidth: Math.min(STATUS_MAX_WIDTH, container.width - 2 * edge)
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (web) {
|
|
376
|
+
if (islands.primary) boxes.primary = { top: edge, right: edge, height: row };
|
|
377
|
+
} else {
|
|
378
|
+
hidden.primary = "the top right stays empty in IDE and embedded hosts (guide \xA75.7)";
|
|
379
|
+
}
|
|
380
|
+
return { boxes, limits, required, hidden };
|
|
381
|
+
}
|
|
382
|
+
function expectedChromeBoxes(measurement, host) {
|
|
383
|
+
return chromeExpectations(measurement, host).boxes;
|
|
384
|
+
}
|
|
385
|
+
var round = (value) => Math.round(value * 10) / 10;
|
|
386
|
+
function sides(box, container) {
|
|
387
|
+
return {
|
|
388
|
+
left: box.left,
|
|
389
|
+
top: box.top,
|
|
390
|
+
right: container.width - box.left - box.width,
|
|
391
|
+
bottom: container.height - box.top - box.height,
|
|
392
|
+
width: box.width,
|
|
393
|
+
height: box.height,
|
|
394
|
+
centerX: box.left + box.width / 2
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function boxMismatches(name2, box, expected, container, tolerance = GEOMETRY_TOLERANCE_PX) {
|
|
398
|
+
const actual = sides(box, container);
|
|
399
|
+
return Object.keys(expected).flatMap((field) => {
|
|
400
|
+
const want = expected[field];
|
|
401
|
+
if (want === void 0) return [];
|
|
402
|
+
const have = actual[field];
|
|
403
|
+
return Math.abs(have - want) > tolerance + 1e-6 ? [`${name2}: ${field} is ${round(have)}px, expected ${round(want)}px (\xB1${tolerance}px)`] : [];
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
function limitViolations(name2, box, limits, container, tolerance = GEOMETRY_TOLERANCE_PX) {
|
|
407
|
+
const actual = sides(box, container);
|
|
408
|
+
const problems = [];
|
|
409
|
+
const atLeast = (field, have, limit) => {
|
|
410
|
+
if (limit !== void 0 && have < limit - tolerance - 1e-6) {
|
|
411
|
+
problems.push(`${name2}: ${field} is ${round(have)}px, at least ${round(limit)}px expected`);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const atMost = (field, have, limit) => {
|
|
415
|
+
if (limit !== void 0 && have > limit + tolerance + 1e-6) {
|
|
416
|
+
problems.push(`${name2}: ${field} is ${round(have)}px, at most ${round(limit)}px expected`);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
atLeast("width", actual.width, limits.minWidth);
|
|
420
|
+
atMost("width", actual.width, limits.maxWidth);
|
|
421
|
+
atMost("height", actual.height, limits.maxHeight);
|
|
422
|
+
atLeast("left", actual.left, limits.minLeft);
|
|
423
|
+
atLeast("right", actual.right, limits.minRight);
|
|
424
|
+
atLeast("bottom", actual.bottom, limits.minBottom);
|
|
425
|
+
return problems;
|
|
426
|
+
}
|
|
427
|
+
function geometryProblems(measurement, host, tolerance = GEOMETRY_TOLERANCE_PX) {
|
|
428
|
+
const { container, islands, open, paletteRow } = measurement;
|
|
429
|
+
const expectations = chromeExpectations(measurement, host);
|
|
430
|
+
const problems = [];
|
|
431
|
+
const sheet = sheetOpen(measurement);
|
|
432
|
+
for (const name2 of expectations.required) {
|
|
433
|
+
if (!islands[name2]) problems.push(`${name2}: [data-chrome="${name2}"] is missing or not visible`);
|
|
434
|
+
}
|
|
435
|
+
for (const name2 of CHROME_ISLANDS) {
|
|
436
|
+
const box = islands[name2];
|
|
437
|
+
if (!box) continue;
|
|
438
|
+
const expected = expectations.boxes[name2];
|
|
439
|
+
if (!expected) {
|
|
440
|
+
const reason = expectations.hidden[name2] ?? "no anchor of the guide applies";
|
|
441
|
+
problems.push(`${name2}: visible, but ${reason}`);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
problems.push(...boxMismatches(name2, box, expected, container, tolerance));
|
|
445
|
+
const limits = expectations.limits[name2];
|
|
446
|
+
if (limits) problems.push(...limitViolations(name2, box, limits, container, tolerance));
|
|
447
|
+
}
|
|
448
|
+
if (isNarrow(container) && paletteRow !== "second-row") {
|
|
449
|
+
problems.push(
|
|
450
|
+
`palette: data-palette="${paletteRow}" in a ${round(container.width)}px wide container; up to ${SHELL.narrow}px it is "second-row" (guide \xA75.15)`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
const palette = islands.palette;
|
|
454
|
+
const menu = islands.menu;
|
|
455
|
+
if (paletteRow === "second-row" && !isNarrow(container) && palette && menu) {
|
|
456
|
+
const clipped = palette.width >= container.width - 2 * SHELL.edge - tolerance;
|
|
457
|
+
const topRowLeft = (container.width - palette.width) / 2;
|
|
458
|
+
if (!clipped && topRowLeft >= menu.left + menu.width + PALETTE_CLEARANCE + tolerance) {
|
|
459
|
+
problems.push(
|
|
460
|
+
`palette: in the second row although the top row keeps ${round(topRowLeft - menu.left - menu.width)}px to the menu trigger (at least ${PALETTE_CLEARANCE}px suffice, guide \xA75.15)`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const state = (island, flag, attribute2, hiddenBySheet) => {
|
|
465
|
+
const shown = Boolean(islands[island]);
|
|
466
|
+
if (flag && !shown && !hiddenBySheet) {
|
|
467
|
+
problems.push(`${island}: the root carries ${attribute2}, but the island is not visible`);
|
|
468
|
+
}
|
|
469
|
+
if (!flag && shown) problems.push(`${island}: visible, but the root lacks ${attribute2}`);
|
|
470
|
+
};
|
|
471
|
+
state("problems", open.problems, 'data-problems="shown"', sheet);
|
|
472
|
+
state("legend", open.legend, 'data-legend="open"', sheet);
|
|
473
|
+
state("inspector", open.inspector, 'data-inspector="open"', false);
|
|
474
|
+
return problems;
|
|
475
|
+
}
|
|
476
|
+
function expectedFitInsets(measurement, host) {
|
|
477
|
+
const { container, islands, open, paletteRow } = measurement;
|
|
478
|
+
const insets = {
|
|
479
|
+
top: paletteRow === "second-row" ? SHELL.fitInsetTopSecondRow : SHELL.fitInsetTop,
|
|
480
|
+
right: SHELL.fitInsetSide,
|
|
481
|
+
bottom: host === "web" && open.problems ? SHELL.fitInsetBottomWeb : SHELL.fitInsetBottom,
|
|
482
|
+
left: SHELL.fitInsetSide
|
|
483
|
+
};
|
|
484
|
+
if (sheetOpen(measurement)) {
|
|
485
|
+
const sheet = islands.inspector;
|
|
486
|
+
insets.bottom = sheet ? container.height - sheet.top + SHELL.edge : SHELL.fitInsetBottom;
|
|
487
|
+
return insets;
|
|
488
|
+
}
|
|
489
|
+
if (open.inspector) insets.right = SHELL.edge + SHELL.inspectorWidth + SHELL.fitInsetSide;
|
|
490
|
+
const legend = islands.legend;
|
|
491
|
+
if (open.legend && legend && legend.width > 0 && container.width > SHELL.veryNarrow) {
|
|
492
|
+
insets.left = SHELL.edge + legend.width + SHELL.fitInsetSide;
|
|
493
|
+
}
|
|
494
|
+
return insets;
|
|
495
|
+
}
|
|
496
|
+
function freeRectangle(measurement, host) {
|
|
497
|
+
const { container } = measurement;
|
|
498
|
+
const insets = expectedFitInsets(measurement, host);
|
|
499
|
+
const horizontal = container.width - insets.left - insets.right > 0;
|
|
500
|
+
const vertical = container.height - insets.top - insets.bottom > 0;
|
|
501
|
+
return {
|
|
502
|
+
left: horizontal ? insets.left : 0,
|
|
503
|
+
top: vertical ? insets.top : 0,
|
|
504
|
+
right: horizontal ? container.width - insets.right : container.width,
|
|
505
|
+
bottom: vertical ? container.height - insets.bottom : container.height
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function dropdownProblems(box, container, tolerance = GEOMETRY_TOLERANCE_PX) {
|
|
509
|
+
const { edge, gap, row, menuMinWidth } = SHELL;
|
|
510
|
+
return [
|
|
511
|
+
...boxMismatches("dropdown", box, { top: edge + row + gap, left: edge }, container, tolerance),
|
|
512
|
+
...limitViolations(
|
|
513
|
+
"dropdown",
|
|
514
|
+
box,
|
|
515
|
+
{
|
|
516
|
+
minWidth: menuMinWidth,
|
|
517
|
+
maxWidth: Math.min(DROPDOWN_MAX_WIDTH, container.width - 2 * edge),
|
|
518
|
+
minBottom: edge
|
|
519
|
+
},
|
|
520
|
+
container,
|
|
521
|
+
tolerance
|
|
522
|
+
)
|
|
523
|
+
];
|
|
524
|
+
}
|
|
525
|
+
function tooltipProblems(tooltip, anchor, row, container, tolerance = GEOMETRY_TOLERANCE_PX) {
|
|
526
|
+
const { edge, gap, row: rowHeight, free } = SHELL;
|
|
527
|
+
const clamp = (centred) => Math.max(edge, Math.min(centred, container.width - edge - tooltip.width));
|
|
528
|
+
const expectedAt = (left) => row === "bottom" ? { left, bottom: edge + rowHeight + gap } : { left, top: (row === "top" ? edge : free) + rowHeight + gap };
|
|
529
|
+
let mismatches = boxMismatches(
|
|
530
|
+
"tooltip",
|
|
531
|
+
tooltip,
|
|
532
|
+
expectedAt(clamp(anchor.left + anchor.width / 2 - tooltip.width / 2)),
|
|
533
|
+
container,
|
|
534
|
+
tolerance
|
|
535
|
+
);
|
|
536
|
+
if (row === "second-row" && mismatches.length > 0) {
|
|
537
|
+
const inContainer = boxMismatches(
|
|
538
|
+
"tooltip",
|
|
539
|
+
tooltip,
|
|
540
|
+
expectedAt(clamp(container.width / 2 - tooltip.width / 2)),
|
|
541
|
+
container,
|
|
542
|
+
tolerance
|
|
543
|
+
);
|
|
544
|
+
if (inContainer.length === 0) mismatches = inContainer;
|
|
545
|
+
}
|
|
546
|
+
return [
|
|
547
|
+
...mismatches,
|
|
548
|
+
...limitViolations(
|
|
549
|
+
"tooltip",
|
|
550
|
+
tooltip,
|
|
551
|
+
{ minLeft: edge, minRight: edge, maxWidth: Math.min(TOOLTIP_MAX_WIDTH, container.width - 2 * edge) },
|
|
552
|
+
container,
|
|
553
|
+
tolerance
|
|
554
|
+
)
|
|
555
|
+
];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/testing/facts.ts
|
|
559
|
+
var DOM_ORDER = [
|
|
560
|
+
"menu",
|
|
561
|
+
"primary",
|
|
562
|
+
"palette",
|
|
563
|
+
"canvas",
|
|
564
|
+
"status",
|
|
565
|
+
"hint",
|
|
566
|
+
"inspector",
|
|
567
|
+
"problems",
|
|
568
|
+
"legend",
|
|
569
|
+
"zoom"
|
|
570
|
+
];
|
|
571
|
+
var ariaProbe = ({ root }) => {
|
|
572
|
+
const tools = window.__modelerShellConformance;
|
|
573
|
+
const rootElement = document.querySelector(root);
|
|
574
|
+
if (!rootElement) return null;
|
|
575
|
+
const find = (selector) => rootElement.querySelector(selector);
|
|
576
|
+
const factsOf = (element) => element ? tools.facts(element) : null;
|
|
577
|
+
const navigatorLike = navigator;
|
|
578
|
+
const trigger = find('[data-chrome="menu"]');
|
|
579
|
+
const controlsId = trigger?.getAttribute("aria-controls");
|
|
580
|
+
const palette = find('[data-chrome="palette"]');
|
|
581
|
+
const toolbar = palette?.querySelector('[role="toolbar"]') ?? palette?.querySelector(".djs-palette-entries");
|
|
582
|
+
const zoom = find('[data-chrome="zoom"]');
|
|
583
|
+
const zoomButtons = {};
|
|
584
|
+
for (const name2 of ["out", "actual", "in", "fit"]) {
|
|
585
|
+
const button = zoom?.querySelector(`[data-zoom="${name2}"]`);
|
|
586
|
+
if (button) zoomButtons[name2] = tools.facts(button);
|
|
587
|
+
}
|
|
588
|
+
const legend = find('[data-chrome="legend"]');
|
|
589
|
+
const labelledBy = legend?.getAttribute("aria-labelledby");
|
|
590
|
+
const parts = [
|
|
591
|
+
["menu", trigger],
|
|
592
|
+
["primary", find('[data-chrome="primary"]')],
|
|
593
|
+
["palette", palette],
|
|
594
|
+
["canvas", find(".djs-container > svg")],
|
|
595
|
+
["status", find('[data-chrome="status"]')],
|
|
596
|
+
["hint", find(".cd-shell-hint")],
|
|
597
|
+
["inspector", find('[data-chrome="inspector"]')],
|
|
598
|
+
["problems", find('[data-chrome="problems"]')],
|
|
599
|
+
["legend", legend],
|
|
600
|
+
["zoom", zoom]
|
|
601
|
+
];
|
|
602
|
+
const present = parts.filter((part) => part[1] !== null);
|
|
603
|
+
present.sort(([, a], [, b]) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
|
|
604
|
+
const titledSelector = [
|
|
605
|
+
'[data-chrome="menu"][title]',
|
|
606
|
+
'[data-chrome="palette"] [data-action][title]',
|
|
607
|
+
'[data-chrome="zoom"] [title]',
|
|
608
|
+
'[data-chrome="problems"][title]'
|
|
609
|
+
].join(", ");
|
|
610
|
+
return {
|
|
611
|
+
platform: /mac/i.test(navigatorLike.userAgentData?.platform ?? navigatorLike.platform ?? "") ? "mac" : "other",
|
|
612
|
+
documentLang: document.documentElement.lang,
|
|
613
|
+
rootLang: rootElement.getAttribute("lang"),
|
|
614
|
+
trigger: factsOf(trigger),
|
|
615
|
+
controlled: factsOf(controlsId ? document.getElementById(controlsId) : null),
|
|
616
|
+
palette: factsOf(palette),
|
|
617
|
+
toolbar: factsOf(toolbar),
|
|
618
|
+
canvas: factsOf(find(".djs-container > svg")),
|
|
619
|
+
entries: [...palette?.querySelectorAll("[data-action]") ?? []].map((entry) => ({
|
|
620
|
+
...tools.facts(entry),
|
|
621
|
+
group: entry.closest("[data-group]")?.getAttribute("data-group") ?? null
|
|
622
|
+
})),
|
|
623
|
+
zoom: factsOf(zoom),
|
|
624
|
+
zoomButtons,
|
|
625
|
+
order: present.map(([name2]) => name2),
|
|
626
|
+
positiveTabindex: [...rootElement.querySelectorAll("[tabindex]")].filter((element) => Number(element.getAttribute("tabindex")) > 0).map((element) => `${tools.describe(element)} (tabindex="${element.getAttribute("tabindex")}")`),
|
|
627
|
+
titled: [...rootElement.querySelectorAll(titledSelector)].map((element) => tools.describe(element)),
|
|
628
|
+
problems: factsOf(find('[data-chrome="problems"]')),
|
|
629
|
+
status: factsOf(find('[data-chrome="status"]')),
|
|
630
|
+
legend: factsOf(legend),
|
|
631
|
+
legendTitle: labelledBy ? labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim() ?? "").join(" ").trim() : null,
|
|
632
|
+
legendClose: factsOf(legend?.querySelector("button")),
|
|
633
|
+
inspector: factsOf(find('[data-chrome="inspector"]'))
|
|
634
|
+
};
|
|
635
|
+
};
|
|
636
|
+
var menuProbe = ({ root }) => {
|
|
637
|
+
const tools = window.__modelerShellConformance;
|
|
638
|
+
const rootElement = document.querySelector(root);
|
|
639
|
+
if (!rootElement) return null;
|
|
640
|
+
const trigger = rootElement.querySelector('[data-chrome="menu"]');
|
|
641
|
+
const menu = [...rootElement.querySelectorAll('[role="menu"]')].find((element) => tools.visible(element)) ?? null;
|
|
642
|
+
const items = menu ? [
|
|
643
|
+
...menu.querySelectorAll('[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]')
|
|
644
|
+
].filter((item) => tools.visible(item)) : [];
|
|
645
|
+
return {
|
|
646
|
+
expanded: trigger?.getAttribute("aria-expanded") ?? null,
|
|
647
|
+
controls: trigger?.getAttribute("aria-controls") ?? null,
|
|
648
|
+
menu: menu ? tools.facts(menu) : null,
|
|
649
|
+
items: items.map((item) => {
|
|
650
|
+
const group = item.closest('[role="group"]');
|
|
651
|
+
return {
|
|
652
|
+
role: item.getAttribute("role") ?? "",
|
|
653
|
+
name: (item.getAttribute("aria-label") ?? item.textContent ?? "").replace(/\s+/g, " ").trim(),
|
|
654
|
+
checked: item.getAttribute("aria-checked"),
|
|
655
|
+
disabled: item.getAttribute("aria-disabled"),
|
|
656
|
+
haspopup: item.getAttribute("aria-haspopup"),
|
|
657
|
+
group: group && menu?.contains(group) ? group.getAttribute("aria-label") ?? "" : null
|
|
658
|
+
};
|
|
659
|
+
}),
|
|
660
|
+
active: items.findIndex((item) => item === document.activeElement),
|
|
661
|
+
activeIsTrigger: Boolean(trigger) && document.activeElement === trigger
|
|
662
|
+
};
|
|
663
|
+
};
|
|
664
|
+
var paletteFocusProbe = ({ root }) => {
|
|
665
|
+
const palette = document.querySelector(root)?.querySelector('[data-chrome="palette"]');
|
|
666
|
+
if (!palette) return null;
|
|
667
|
+
const entries = [...palette.querySelectorAll("[data-action]")];
|
|
668
|
+
const active = document.activeElement;
|
|
669
|
+
return {
|
|
670
|
+
entries: entries.map((entry) => entry.getAttribute("data-action") ?? ""),
|
|
671
|
+
tabStops: entries.filter((entry) => entry.getAttribute("tabindex") === "0").map((entry) => entry.getAttribute("data-action") ?? ""),
|
|
672
|
+
active: active && entries.includes(active) ? active.getAttribute("data-action") : null
|
|
673
|
+
};
|
|
674
|
+
};
|
|
675
|
+
var zoomButtonsProbe = ({ root }) => {
|
|
676
|
+
const tools = window.__modelerShellConformance;
|
|
677
|
+
const zoom = document.querySelector(root)?.querySelector('[data-chrome="zoom"]');
|
|
678
|
+
const buttons = {};
|
|
679
|
+
for (const name2 of ["out", "actual", "in", "fit"]) {
|
|
680
|
+
const button = zoom?.querySelector(`[data-zoom="${name2}"]`);
|
|
681
|
+
if (button) buttons[name2] = tools.facts(button);
|
|
682
|
+
}
|
|
683
|
+
return buttons;
|
|
684
|
+
};
|
|
685
|
+
var MODE_ROLES = ["--cd-ui-grund", "--cd-ui-text", "--cd-ui-flaeche-hoch", "--cd-ui-linie"];
|
|
686
|
+
var appearanceProbe = ({
|
|
687
|
+
root,
|
|
688
|
+
roles
|
|
689
|
+
}) => {
|
|
690
|
+
const tools = window.__modelerShellConformance;
|
|
691
|
+
const rootElement = document.querySelector(root);
|
|
692
|
+
if (!rootElement) return null;
|
|
693
|
+
const mode = (name2, element) => {
|
|
694
|
+
const style = getComputedStyle(element);
|
|
695
|
+
const resolved = {};
|
|
696
|
+
for (const role of roles) resolved[role] = style.getPropertyValue(role).trim();
|
|
697
|
+
return { name: name2, description: tools.describe(element), colorScheme: style.colorScheme, roles: resolved };
|
|
698
|
+
};
|
|
699
|
+
const parts = [mode("root", rootElement)];
|
|
700
|
+
const canvas = rootElement.querySelector(".djs-container");
|
|
701
|
+
if (canvas) parts.push(mode("canvas", canvas));
|
|
702
|
+
for (const island of rootElement.querySelectorAll("[data-chrome]")) {
|
|
703
|
+
if (tools.visible(island)) parts.push(mode(island.getAttribute("data-chrome") ?? "island", island));
|
|
704
|
+
}
|
|
705
|
+
for (const [name2, role] of [
|
|
706
|
+
["dropdown", "menu"],
|
|
707
|
+
["tooltip", "tooltip"]
|
|
708
|
+
]) {
|
|
709
|
+
const popup = [...rootElement.querySelectorAll(`[role="${role}"]`)].find(
|
|
710
|
+
(element) => tools.visible(element)
|
|
711
|
+
);
|
|
712
|
+
if (popup) parts.push(mode(name2, popup));
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
documentClasses: [...document.documentElement.classList],
|
|
716
|
+
themed: [...document.querySelectorAll("[data-cd-theme], [data-cd-kontrast]")].map((element) => ({
|
|
717
|
+
description: tools.describe(element),
|
|
718
|
+
isDocument: element === document.documentElement,
|
|
719
|
+
relation: element === rootElement ? "self" : element.contains(rootElement) ? "ancestor" : rootElement.contains(element) ? "descendant" : "elsewhere",
|
|
720
|
+
theme: element.getAttribute("data-cd-theme"),
|
|
721
|
+
kontrast: element.getAttribute("data-cd-kontrast")
|
|
722
|
+
})),
|
|
723
|
+
parts
|
|
724
|
+
};
|
|
725
|
+
};
|
|
726
|
+
var activeStopProbe = ({ root }) => {
|
|
727
|
+
const tools = window.__modelerShellConformance;
|
|
728
|
+
const active = document.activeElement;
|
|
729
|
+
const rootElement = document.querySelector(root);
|
|
730
|
+
if (!active || active === document.body || !rootElement) return { description: "<body>", kind: "other" };
|
|
731
|
+
const description = tools.describe(active);
|
|
732
|
+
if (!rootElement.contains(active))
|
|
733
|
+
return { description: `${description} outside the shell root`, kind: "other" };
|
|
734
|
+
if (active.matches('[data-chrome="palette"] [data-action]')) return { description, kind: "palette" };
|
|
735
|
+
if (active.matches(".djs-container > svg")) return { description, kind: "canvas" };
|
|
736
|
+
if (active.closest('[data-chrome="primary"]')) return { description, kind: "primary" };
|
|
737
|
+
const tabindex = active.getAttribute("tabindex");
|
|
738
|
+
return {
|
|
739
|
+
description: tabindex === null ? description : `${description}[tabindex="${tabindex}"]`,
|
|
740
|
+
kind: "other"
|
|
741
|
+
};
|
|
742
|
+
};
|
|
743
|
+
var paletteShortcutProbe = ({
|
|
744
|
+
root
|
|
745
|
+
}) => {
|
|
746
|
+
const tools = window.__modelerShellConformance;
|
|
747
|
+
const rootElement = document.querySelector(root);
|
|
748
|
+
const active = document.activeElement;
|
|
749
|
+
if (!rootElement || !active?.matches('[data-chrome="palette"] [data-action]')) return null;
|
|
750
|
+
const tooltip = [...rootElement.querySelectorAll('[role="tooltip"]')].find(
|
|
751
|
+
(element) => tools.visible(element)
|
|
752
|
+
);
|
|
753
|
+
const chip = tooltip ? [...tooltip.querySelectorAll("kbd")].find((element) => tools.visible(element)) : void 0;
|
|
754
|
+
return {
|
|
755
|
+
action: active.getAttribute("data-action") ?? "",
|
|
756
|
+
chip: (chip?.textContent ?? "").trim(),
|
|
757
|
+
keyShortcuts: active.getAttribute("aria-keyshortcuts")
|
|
758
|
+
};
|
|
759
|
+
};
|
|
760
|
+
var surroundProbe = ({ root }) => {
|
|
761
|
+
const tools = window.__modelerShellConformance;
|
|
762
|
+
const rootElement = document.querySelector(root);
|
|
763
|
+
if (!rootElement) return null;
|
|
764
|
+
const transparent = (color) => {
|
|
765
|
+
if (color === "" || color === "transparent") return true;
|
|
766
|
+
const legacy = /^rgba\([^)]*,\s*([\d.]+)\)$/.exec(color);
|
|
767
|
+
if (legacy) return Number(legacy[1]) === 0;
|
|
768
|
+
const modern = /\/\s*([\d.]+)%?\s*\)$/.exec(color);
|
|
769
|
+
return modern ? Number(modern[1]) === 0 : false;
|
|
770
|
+
};
|
|
771
|
+
const start = rootElement.querySelector(".djs-container");
|
|
772
|
+
let painted = null;
|
|
773
|
+
for (let node = start; node && !painted; node = node.parentElement) {
|
|
774
|
+
const color = getComputedStyle(node).backgroundColor;
|
|
775
|
+
if (!transparent(color)) painted = { description: tools.describe(node), color };
|
|
776
|
+
if (node === rootElement) break;
|
|
777
|
+
}
|
|
778
|
+
const probe = document.createElement("span");
|
|
779
|
+
probe.setAttribute("data-modeler-shell-conformance", "role-probe");
|
|
780
|
+
probe.style.cssText = "position:absolute;visibility:hidden;background-color:var(--cd-ui-grund);";
|
|
781
|
+
rootElement.append(probe);
|
|
782
|
+
const grund = getComputedStyle(probe).backgroundColor;
|
|
783
|
+
probe.remove();
|
|
784
|
+
return { container: start !== null, painted, grund };
|
|
785
|
+
};
|
|
786
|
+
var paletteEntriesProbe = ({ root }) => {
|
|
787
|
+
const tools = window.__modelerShellConformance;
|
|
788
|
+
const palette = document.querySelector(root)?.querySelector('[data-chrome="palette"]');
|
|
789
|
+
if (!palette) return [];
|
|
790
|
+
return [...palette.querySelectorAll("[data-action]")].filter((entry) => tools.visible(entry)).map((entry) => {
|
|
791
|
+
const rect = entry.getBoundingClientRect();
|
|
792
|
+
return {
|
|
793
|
+
action: entry.getAttribute("data-action") ?? "",
|
|
794
|
+
group: entry.closest("[data-group]")?.getAttribute("data-group") ?? null,
|
|
795
|
+
pressed: entry.getAttribute("aria-pressed") === "true",
|
|
796
|
+
disabled: entry.getAttribute("aria-disabled") === "true",
|
|
797
|
+
client: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
|
|
798
|
+
};
|
|
799
|
+
});
|
|
800
|
+
};
|
|
801
|
+
var paletteColorProbe = ({ root }) => {
|
|
802
|
+
const tools = window.__modelerShellConformance;
|
|
803
|
+
const palette = document.querySelector(root)?.querySelector('[data-chrome="palette"]');
|
|
804
|
+
if (!palette) return null;
|
|
805
|
+
const resolve = (role) => {
|
|
806
|
+
const probe = document.createElement("span");
|
|
807
|
+
probe.setAttribute("data-modeler-shell-conformance", "role-probe");
|
|
808
|
+
probe.style.cssText = `position:absolute;visibility:hidden;color:var(${role});`;
|
|
809
|
+
palette.append(probe);
|
|
810
|
+
const color = getComputedStyle(probe).color;
|
|
811
|
+
probe.remove();
|
|
812
|
+
return color;
|
|
813
|
+
};
|
|
814
|
+
const iconOf = (entry) => {
|
|
815
|
+
const svg = entry.querySelector("svg");
|
|
816
|
+
if (svg) return { kind: "svg", color: getComputedStyle(svg).color };
|
|
817
|
+
const before = getComputedStyle(entry, "::before");
|
|
818
|
+
if (before.content && before.content !== "none" && before.content !== "normal") {
|
|
819
|
+
return { kind: "::before", color: before.color };
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
};
|
|
823
|
+
return {
|
|
824
|
+
roles: {
|
|
825
|
+
text: resolve("--cd-ui-text"),
|
|
826
|
+
interaktion: resolve("--cd-ui-interaktion"),
|
|
827
|
+
textLeise: resolve("--cd-ui-text-leise")
|
|
828
|
+
},
|
|
829
|
+
entries: [...palette.querySelectorAll("[data-action]")].filter((entry) => tools.visible(entry)).map((entry) => ({
|
|
830
|
+
action: entry.getAttribute("data-action") ?? "",
|
|
831
|
+
group: entry.closest("[data-group]")?.getAttribute("data-group") ?? null,
|
|
832
|
+
pressed: entry.getAttribute("aria-pressed") === "true",
|
|
833
|
+
disabled: entry.getAttribute("aria-disabled") === "true",
|
|
834
|
+
hovered: entry.matches(":hover"),
|
|
835
|
+
color: getComputedStyle(entry).color,
|
|
836
|
+
icon: iconOf(entry)
|
|
837
|
+
}))
|
|
838
|
+
};
|
|
839
|
+
};
|
|
840
|
+
var shapesProbe = ({ root }) => {
|
|
841
|
+
const tools = window.__modelerShellConformance;
|
|
842
|
+
const rootElement = document.querySelector(root);
|
|
843
|
+
if (!rootElement) return [];
|
|
844
|
+
return [...rootElement.querySelectorAll(".djs-container .djs-shape[data-element-id]")].filter((shape) => tools.visible(shape)).map((shape) => {
|
|
845
|
+
const visual = shape.querySelector(".djs-visual") ?? shape;
|
|
846
|
+
const rect = visual.getBoundingClientRect();
|
|
847
|
+
const client = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
848
|
+
const local = typeof visual.getBBox === "function" ? visual.getBBox() : rect;
|
|
849
|
+
const hitElement = document.elementFromPoint(client.x, client.y);
|
|
850
|
+
return {
|
|
851
|
+
id: shape.getAttribute("data-element-id") ?? "",
|
|
852
|
+
selected: shape.classList.contains("selected"),
|
|
853
|
+
transform: shape.getAttribute("transform") ?? "",
|
|
854
|
+
size: { width: local.width, height: local.height },
|
|
855
|
+
box: tools.box(visual, rootElement),
|
|
856
|
+
client,
|
|
857
|
+
hit: hitElement !== null && shape.contains(hitElement)
|
|
858
|
+
};
|
|
859
|
+
});
|
|
860
|
+
};
|
|
861
|
+
var canvasStateProbe = ({ root }) => {
|
|
862
|
+
const tools = window.__modelerShellConformance;
|
|
863
|
+
const container = document.querySelector(root)?.querySelector(".djs-container");
|
|
864
|
+
const active = document.activeElement;
|
|
865
|
+
const ours = (element) => element.closest("[data-modeler-shell-conformance]") !== null;
|
|
866
|
+
const visibleIn = (selector) => container ? [...container.querySelectorAll(selector)].filter(
|
|
867
|
+
(element) => !ours(element) && tools.visible(element)
|
|
868
|
+
) : [];
|
|
869
|
+
return {
|
|
870
|
+
focusInCanvas: Boolean(container && active && container.contains(active)),
|
|
871
|
+
focused: !active || active === document.body ? "<body>" : tools.describe(active),
|
|
872
|
+
contextPadEntries: visibleIn(".djs-context-pad .entry").map(
|
|
873
|
+
(entry) => entry.getAttribute("data-action") ?? tools.describe(entry)
|
|
874
|
+
),
|
|
875
|
+
textFields: visibleIn('input, textarea, [contenteditable]:not([contenteditable="false"])').map(
|
|
876
|
+
(field) => tools.describe(field)
|
|
877
|
+
)
|
|
878
|
+
};
|
|
879
|
+
};
|
|
880
|
+
var readOnlyStateProbe = ({ root }) => {
|
|
881
|
+
const tools = window.__modelerShellConformance;
|
|
882
|
+
const rootElement = document.querySelector(root);
|
|
883
|
+
if (!rootElement) return null;
|
|
884
|
+
const status = rootElement.querySelector('[data-chrome="status"]');
|
|
885
|
+
return { readOnly: rootElement.hasAttribute("data-readonly"), status: status ? tools.facts(status) : null };
|
|
886
|
+
};
|
|
887
|
+
var menuEntriesProbe = ({ root }) => {
|
|
888
|
+
const tools = window.__modelerShellConformance;
|
|
889
|
+
const rootElement = document.querySelector(root);
|
|
890
|
+
const menu = rootElement ? [...rootElement.querySelectorAll('[role="menu"]')].find((element) => tools.visible(element)) : void 0;
|
|
891
|
+
if (!menu) return null;
|
|
892
|
+
const entries = [];
|
|
893
|
+
const selector = '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]';
|
|
894
|
+
for (const element of menu.querySelectorAll(selector)) {
|
|
895
|
+
const role = element.getAttribute("role") ?? "";
|
|
896
|
+
if (!tools.visible(element)) continue;
|
|
897
|
+
entries.push({
|
|
898
|
+
name: (element.getAttribute("aria-label") ?? element.textContent ?? "").replace(/\s+/g, " ").trim(),
|
|
899
|
+
role,
|
|
900
|
+
menuId: element.closest("[data-menu-id]")?.getAttribute("data-menu-id") ?? null,
|
|
901
|
+
disabled: element.getAttribute("aria-disabled") === "true",
|
|
902
|
+
group: element.closest("[data-menu-group]")?.getAttribute("data-menu-group") ?? null
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
return entries;
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
// src/testing/checks.ts
|
|
909
|
+
var normalize = (text) => text.replace(/\s+/g, " ").trim();
|
|
910
|
+
var name = (element) => normalize(element.attributes["aria-label"] ?? element.text);
|
|
911
|
+
var attribute = (element, key) => element.attributes[key];
|
|
912
|
+
function expectedKeyShortcuts(platform) {
|
|
913
|
+
const modifier = platform === "mac" ? "Meta" : "Control";
|
|
914
|
+
return {
|
|
915
|
+
in: `${modifier}+= ${modifier}+Plus`,
|
|
916
|
+
out: `${modifier}+-`,
|
|
917
|
+
actual: `${modifier}+0`,
|
|
918
|
+
fit: "Shift+1"
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
function ariaProblems(facts, host) {
|
|
922
|
+
const problems = [];
|
|
923
|
+
if (host === "embedded") {
|
|
924
|
+
if (!facts.rootLang)
|
|
925
|
+
problems.push("root: an embedded shell root carries the tool's lang itself (guide \xA72, \xA712)");
|
|
926
|
+
} else if (!facts.documentLang) {
|
|
927
|
+
problems.push("<html> has no lang (guide \xA72)");
|
|
928
|
+
}
|
|
929
|
+
const trigger = facts.trigger;
|
|
930
|
+
if (!trigger) {
|
|
931
|
+
problems.push('menu: no [data-chrome="menu"] trigger');
|
|
932
|
+
} else {
|
|
933
|
+
if (trigger.tag !== "button")
|
|
934
|
+
problems.push(`menu: the trigger is a <${trigger.tag}>, expected a <button>`);
|
|
935
|
+
if (!attribute(trigger, "aria-label")) problems.push("menu: the icon-only trigger has no aria-label");
|
|
936
|
+
if (attribute(trigger, "aria-haspopup") !== "menu") {
|
|
937
|
+
problems.push(`menu: aria-haspopup is "${attribute(trigger, "aria-haspopup") ?? ""}", expected "menu"`);
|
|
938
|
+
}
|
|
939
|
+
if (attribute(trigger, "aria-expanded") !== "false") {
|
|
940
|
+
problems.push(
|
|
941
|
+
`menu: aria-expanded is "${attribute(trigger, "aria-expanded") ?? ""}" while closed, expected "false"`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
if (!attribute(trigger, "aria-controls")) {
|
|
945
|
+
problems.push("menu: the trigger has no aria-controls");
|
|
946
|
+
} else if (!facts.controlled) {
|
|
947
|
+
problems.push(`menu: aria-controls="${attribute(trigger, "aria-controls")}" names no element`);
|
|
948
|
+
} else if (attribute(facts.controlled, "role") !== "menu") {
|
|
949
|
+
problems.push(
|
|
950
|
+
`menu: aria-controls names a ${facts.controlled.description} with role "${attribute(facts.controlled, "role") ?? ""}", expected "menu"`
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (!facts.palette) {
|
|
955
|
+
problems.push('palette: no [data-chrome="palette"]');
|
|
956
|
+
} else {
|
|
957
|
+
const toolbar = facts.toolbar;
|
|
958
|
+
if (!toolbar || attribute(toolbar, "role") !== "toolbar") {
|
|
959
|
+
problems.push('palette: .djs-palette-entries has no role="toolbar"');
|
|
960
|
+
} else if (!attribute(toolbar, "aria-label")) {
|
|
961
|
+
problems.push("palette: the toolbar has no aria-label");
|
|
962
|
+
}
|
|
963
|
+
for (const entry of facts.entries) {
|
|
964
|
+
const action = attribute(entry, "data-action") ?? entry.description;
|
|
965
|
+
if (attribute(entry, "role") !== "button")
|
|
966
|
+
problems.push(`palette: entry "${action}" has no role="button"`);
|
|
967
|
+
if (!attribute(entry, "aria-label"))
|
|
968
|
+
problems.push(`palette: entry "${action}" has no aria-label (title)`);
|
|
969
|
+
const tabindex = attribute(entry, "tabindex");
|
|
970
|
+
if (tabindex !== "0" && tabindex !== "-1") {
|
|
971
|
+
problems.push(`palette: entry "${action}" has tabindex "${tabindex ?? ""}", expected roving 0/-1`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const stops = facts.entries.filter((entry) => attribute(entry, "tabindex") === "0");
|
|
975
|
+
if (facts.entries.length > 0 && stops.length !== 1) {
|
|
976
|
+
problems.push(
|
|
977
|
+
`palette: ${stops.length} entries with tabindex="0", expected exactly one (roving tabindex)`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
const tools = facts.entries.filter(
|
|
981
|
+
(entry) => entry.group === "tools" && (attribute(entry, "data-action") ?? "").endsWith("-tool")
|
|
982
|
+
);
|
|
983
|
+
for (const tool of tools) {
|
|
984
|
+
const pressed2 = attribute(tool, "aria-pressed");
|
|
985
|
+
if (pressed2 !== "true" && pressed2 !== "false") {
|
|
986
|
+
problems.push(`palette: tool "${attribute(tool, "data-action")}" has no aria-pressed`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const pressed = tools.filter((tool) => attribute(tool, "aria-pressed") === "true");
|
|
990
|
+
if (pressed.length > 1) problems.push(`palette: ${pressed.length} tools are aria-pressed at once`);
|
|
991
|
+
}
|
|
992
|
+
const zoom = facts.zoom;
|
|
993
|
+
if (!zoom) {
|
|
994
|
+
problems.push('zoom: no [data-chrome="zoom"]');
|
|
995
|
+
} else {
|
|
996
|
+
if (attribute(zoom, "role") !== "group") problems.push('zoom: the panel has no role="group"');
|
|
997
|
+
if (!attribute(zoom, "aria-label")) problems.push("zoom: the panel has no aria-label");
|
|
998
|
+
const shortcuts = expectedKeyShortcuts(facts.platform);
|
|
999
|
+
for (const button of ["out", "actual", "in", "fit"]) {
|
|
1000
|
+
const control = facts.zoomButtons[button];
|
|
1001
|
+
if (!control) {
|
|
1002
|
+
problems.push(`zoom: no [data-zoom="${button}"] button`);
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
if (control.tag !== "button") problems.push(`zoom: [data-zoom="${button}"] is a <${control.tag}>`);
|
|
1006
|
+
if (!name(control)) problems.push(`zoom: [data-zoom="${button}"] has no accessible name`);
|
|
1007
|
+
if (attribute(control, "aria-keyshortcuts") !== shortcuts[button]) {
|
|
1008
|
+
problems.push(
|
|
1009
|
+
`zoom: [data-zoom="${button}"] aria-keyshortcuts is "${attribute(control, "aria-keyshortcuts") ?? ""}", expected "${shortcuts[button]}" (${facts.platform})`
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
if (control.disabled) {
|
|
1013
|
+
problems.push(
|
|
1014
|
+
`zoom: [data-zoom="${button}"] is natively disabled; aria-disabled keeps the focus (guide \xA75.8)`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
const readout = facts.zoomButtons.actual;
|
|
1019
|
+
if (readout) {
|
|
1020
|
+
if (!/\d/.test(readout.text))
|
|
1021
|
+
problems.push(`zoom: the readout shows "${readout.text}", expected a percentage`);
|
|
1022
|
+
const label = normalize(attribute(readout, "aria-label") ?? "");
|
|
1023
|
+
if (!label.includes(readout.text)) {
|
|
1024
|
+
problems.push(
|
|
1025
|
+
`zoom: the readout's aria-label "${label}" does not name the current zoom "${readout.text}"`
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
if (attribute(readout, "aria-disabled") === "true")
|
|
1029
|
+
problems.push("zoom: the readout is never disabled");
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
const present = new Set(facts.order);
|
|
1033
|
+
const expectedOrder = DOM_ORDER.filter((part) => present.has(part));
|
|
1034
|
+
if (facts.order.join() !== expectedOrder.join()) {
|
|
1035
|
+
problems.push(
|
|
1036
|
+
`DOM order is ${facts.order.join(", ")}; expected ${expectedOrder.join(", ")} (guide \xA75.14)`
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
if (!present.has("canvas")) problems.push("canvas: no diagram-js <svg> (.djs-container > svg) in the root");
|
|
1040
|
+
const canvas = facts.canvas;
|
|
1041
|
+
if (canvas && !normalize(attribute(canvas, "aria-label") ?? "") && !attribute(canvas, "aria-labelledby")) {
|
|
1042
|
+
problems.push(
|
|
1043
|
+
'canvas: the focusable <svg> has no accessible name (WCAG 4.1.2); shellModule names it role="application" with messages.canvas'
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
for (const element of facts.positiveTabindex)
|
|
1047
|
+
problems.push(`positive tabindex on ${element} (guide \xA75.14)`);
|
|
1048
|
+
for (const element of facts.titled) {
|
|
1049
|
+
problems.push(`${element} has a native title; the chrome tooltip replaces it (guide \xA75.6)`);
|
|
1050
|
+
}
|
|
1051
|
+
return problems;
|
|
1052
|
+
}
|
|
1053
|
+
function optionalIslandProblems(facts, shown) {
|
|
1054
|
+
const problems = [];
|
|
1055
|
+
if (shown.has("problems")) {
|
|
1056
|
+
const pill = facts.problems;
|
|
1057
|
+
if (!pill?.visible) {
|
|
1058
|
+
problems.push("problems: the pill is not visible after show.problems");
|
|
1059
|
+
} else {
|
|
1060
|
+
if (pill.tag !== "button") problems.push(`problems: the pill is a <${pill.tag}>, expected a <button>`);
|
|
1061
|
+
const label = normalize(attribute(pill, "aria-label") ?? "");
|
|
1062
|
+
if (!label) problems.push("problems: the pill has no aria-label (count and errors)");
|
|
1063
|
+
else if (!label.includes(pill.text)) {
|
|
1064
|
+
problems.push(`problems: the aria-label "${label}" does not contain the visible text "${pill.text}"`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (shown.has("status")) {
|
|
1069
|
+
const status = facts.status;
|
|
1070
|
+
if (!status?.visible) {
|
|
1071
|
+
problems.push("status: the status box is not visible after show.status");
|
|
1072
|
+
} else {
|
|
1073
|
+
const role = attribute(status, "role");
|
|
1074
|
+
const severity = attribute(status, "data-severity");
|
|
1075
|
+
const expected = severity === "danger" ? "alert" : "status";
|
|
1076
|
+
if (role !== expected) {
|
|
1077
|
+
problems.push(
|
|
1078
|
+
`status: role is "${role ?? ""}" for data-severity="${severity ?? ""}", expected "${expected}"`
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (shown.has("legend")) {
|
|
1084
|
+
const legend = facts.legend;
|
|
1085
|
+
if (!legend?.visible) {
|
|
1086
|
+
problems.push("legend: the legend is not visible after show.legend");
|
|
1087
|
+
} else {
|
|
1088
|
+
if (!facts.legendTitle)
|
|
1089
|
+
problems.push("legend: the card is not labelled by its title (aria-labelledby)");
|
|
1090
|
+
if (!facts.legendClose || !attribute(facts.legendClose, "aria-label")) {
|
|
1091
|
+
problems.push("legend: the close button has no aria-label");
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (shown.has("inspector")) {
|
|
1096
|
+
const inspector = facts.inspector;
|
|
1097
|
+
if (!inspector?.visible) {
|
|
1098
|
+
problems.push("inspector: the inspector is not visible after show.inspector");
|
|
1099
|
+
} else {
|
|
1100
|
+
if (inspector.tag !== "aside") problems.push(`inspector: a <${inspector.tag}>, expected an <aside>`);
|
|
1101
|
+
if (!attribute(inspector, "aria-label")) problems.push("inspector: the <aside> has no aria-label");
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return problems;
|
|
1105
|
+
}
|
|
1106
|
+
function tabSequenceProblems(stops) {
|
|
1107
|
+
const [first, second, third] = stops;
|
|
1108
|
+
const sequence = first?.kind === "primary" ? [second, third] : [first, second];
|
|
1109
|
+
const [palette, canvas] = sequence;
|
|
1110
|
+
const problems = [];
|
|
1111
|
+
if (palette?.kind !== "palette") {
|
|
1112
|
+
problems.push(
|
|
1113
|
+
`tab order: Tab from the menu trigger lands on ${palette?.description ?? "nothing"}, expected the palette's roving entry (guide \xA75.14)`
|
|
1114
|
+
);
|
|
1115
|
+
} else if (canvas?.kind !== "canvas") {
|
|
1116
|
+
problems.push(
|
|
1117
|
+
`tab order: Tab from the palette lands on ${canvas?.description ?? "nothing"}, expected the canvas <svg> (an unexpected tab stop, guide \xA75.14)`
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
return problems;
|
|
1121
|
+
}
|
|
1122
|
+
function paletteShortcutProblems(entries) {
|
|
1123
|
+
return entries.filter((entry) => entry.chip && !entry.keyShortcuts).map(
|
|
1124
|
+
(entry) => `palette: entry "${entry.action}" shows the shortcut "${entry.chip}" in its tooltip but has no aria-keyshortcuts (guide \xA75.6)`
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
function menuKeysAtDocumentProblems(pressed, documentKeys) {
|
|
1128
|
+
const reached = new Set(documentKeys.map((key) => key.key));
|
|
1129
|
+
return pressed.filter((key) => reached.has(key)).map(
|
|
1130
|
+
(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)`
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
function menuOpenProblems(facts, focus) {
|
|
1134
|
+
const problems = [];
|
|
1135
|
+
if (facts.expanded !== "true") {
|
|
1136
|
+
problems.push(`menu: aria-expanded is "${facts.expanded ?? ""}" while open, expected "true"`);
|
|
1137
|
+
}
|
|
1138
|
+
const menu = facts.menu;
|
|
1139
|
+
if (!menu) return [...problems, 'menu: no visible role="menu" after opening'];
|
|
1140
|
+
if (!attribute(menu, "aria-label")) problems.push("menu: the dropdown has no aria-label");
|
|
1141
|
+
if (facts.controls && attribute(menu, "id") !== facts.controls) {
|
|
1142
|
+
problems.push(
|
|
1143
|
+
`menu: the visible dropdown is #${attribute(menu, "id") ?? ""}, the trigger controls #${facts.controls}`
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
if (facts.items.length === 0) problems.push("menu: the dropdown has no menuitem");
|
|
1147
|
+
facts.items.forEach((item, index) => {
|
|
1148
|
+
const label = item.name || `item ${index + 1}`;
|
|
1149
|
+
if (!item.name) problems.push(`menu: ${label} has no accessible name`);
|
|
1150
|
+
if (item.role !== "menuitem" && item.checked !== "true" && item.checked !== "false") {
|
|
1151
|
+
problems.push(`menu: "${label}" (${item.role}) has aria-checked "${item.checked ?? ""}"`);
|
|
1152
|
+
}
|
|
1153
|
+
if (item.role === "menuitemradio" && !item.group) {
|
|
1154
|
+
problems.push(`menu: radio "${label}" is not in a labelled role="group"`);
|
|
1155
|
+
}
|
|
1156
|
+
if (item.haspopup === "dialog" && !item.name.endsWith("\u2026")) {
|
|
1157
|
+
problems.push(`menu: "${label}" opens a dialog, so its label ends with "\u2026" (U+2026)`);
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
if (focus && facts.items.length > 0) {
|
|
1161
|
+
const expected = focus === "first" ? 0 : facts.items.length - 1;
|
|
1162
|
+
if (facts.active !== expected) {
|
|
1163
|
+
problems.push(
|
|
1164
|
+
`menu: focus is on ${facts.active < 0 ? "no entry" : `entry ${facts.active + 1}`}, expected the ${focus} entry`
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return problems;
|
|
1169
|
+
}
|
|
1170
|
+
function menuClosedProblems(facts, focusOnTrigger) {
|
|
1171
|
+
const problems = [];
|
|
1172
|
+
if (facts.expanded !== "false")
|
|
1173
|
+
problems.push(`menu: aria-expanded is "${facts.expanded ?? ""}" after closing`);
|
|
1174
|
+
if (facts.menu) problems.push("menu: the dropdown is still visible after Escape");
|
|
1175
|
+
if (focusOnTrigger && !facts.activeIsTrigger)
|
|
1176
|
+
problems.push("menu: Escape did not return the focus to the trigger");
|
|
1177
|
+
return problems;
|
|
1178
|
+
}
|
|
1179
|
+
function keyReachProblem(press, reach, host, seen) {
|
|
1180
|
+
const shouldPrevent = reach === "handled" || reach === "canvas";
|
|
1181
|
+
if (host === "ide" && reach !== "free") {
|
|
1182
|
+
if (seen.keys.length > 0) {
|
|
1183
|
+
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)`;
|
|
1184
|
+
}
|
|
1185
|
+
const atDocument = seen.documentKeys ?? [];
|
|
1186
|
+
const prevented2 = atDocument.length === 1 ? atDocument[0]?.defaultPrevented : void 0;
|
|
1187
|
+
if (prevented2 === true && !shouldPrevent) {
|
|
1188
|
+
return `${press} was prevented; the shell only stops it, so the field, dialog or browser still acts (guide \xA713.5)`;
|
|
1189
|
+
}
|
|
1190
|
+
if (prevented2 === false && shouldPrevent) {
|
|
1191
|
+
return `${press} was stopped but not prevented; a handled key also calls preventDefault`;
|
|
1192
|
+
}
|
|
1193
|
+
return void 0;
|
|
1194
|
+
}
|
|
1195
|
+
if (seen.keys.length === 0) {
|
|
1196
|
+
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`;
|
|
1197
|
+
}
|
|
1198
|
+
if (seen.keys.length > 1) return `${press} reached window ${seen.keys.length} times`;
|
|
1199
|
+
const prevented = seen.keys[0]?.defaultPrevented ?? false;
|
|
1200
|
+
if (prevented && !shouldPrevent) {
|
|
1201
|
+
return `${press} arrived prevented; ${reach === "free" ? "nothing may handle it" : "the browser, the field or the dialog must act"}`;
|
|
1202
|
+
}
|
|
1203
|
+
if (!prevented && shouldPrevent) {
|
|
1204
|
+
return `${press} arrived unprevented; a handled key calls preventDefault (the browser would zoom the page)`;
|
|
1205
|
+
}
|
|
1206
|
+
return void 0;
|
|
1207
|
+
}
|
|
1208
|
+
function expectedAppearance(requested, themes) {
|
|
1209
|
+
const fallback = themes[0];
|
|
1210
|
+
if (fallback === void 0) throw new Error("@miragon/modeler-shell conformance: harness.themes is empty");
|
|
1211
|
+
const theme = themes.includes(requested.theme) ? requested.theme : fallback;
|
|
1212
|
+
return requested.kontrast ? { theme, kontrast: requested.kontrast } : { theme };
|
|
1213
|
+
}
|
|
1214
|
+
var describeAppearance = (appearance) => appearance.kontrast ? `${appearance.theme} + ${appearance.kontrast}` : appearance.theme;
|
|
1215
|
+
function expectedEmbeddedAppearance(facts, themes) {
|
|
1216
|
+
const fallback = themes[0];
|
|
1217
|
+
if (fallback === void 0) throw new Error("@miragon/modeler-shell conformance: harness.themes is empty");
|
|
1218
|
+
const host = facts.themed.filter((element) => element.relation === "ancestor").at(-1);
|
|
1219
|
+
return host?.kontrast === "hoch" ? { theme: fallback, kontrast: "hoch" } : { theme: fallback };
|
|
1220
|
+
}
|
|
1221
|
+
function appearanceProblems(facts, host, requested, themes) {
|
|
1222
|
+
const ownDocument = host !== "embedded";
|
|
1223
|
+
const expected = ownDocument ? expectedAppearance(requested, themes) : expectedEmbeddedAppearance(facts, themes);
|
|
1224
|
+
const problems = [];
|
|
1225
|
+
const label = ownDocument ? `requested ${describeAppearance(requested)}, expected ${describeAppearance(expected)}` : `host page ${describeAppearance(requested)}, expected ${describeAppearance(expected)}`;
|
|
1226
|
+
const carrier = ownDocument ? facts.themed.find((element) => element.isDocument && element.theme !== null) : facts.themed.find((element) => element.relation === "self" && element.theme !== null);
|
|
1227
|
+
if (!carrier) {
|
|
1228
|
+
problems.push(
|
|
1229
|
+
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)`
|
|
1230
|
+
);
|
|
1231
|
+
} else {
|
|
1232
|
+
if (carrier.theme !== expected.theme) {
|
|
1233
|
+
problems.push(`${carrier.description} has data-cd-theme="${carrier.theme}" (${label})`);
|
|
1234
|
+
}
|
|
1235
|
+
if ((carrier.kontrast ?? void 0) !== expected.kontrast) {
|
|
1236
|
+
problems.push(
|
|
1237
|
+
`${carrier.description} has ${carrier.kontrast ? `data-cd-kontrast="${carrier.kontrast}"` : "no data-cd-kontrast"} (${label})`
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
for (const element of facts.themed) {
|
|
1242
|
+
if (element === carrier) continue;
|
|
1243
|
+
const stray = ownDocument ? !element.isDocument || element.theme === null : element.relation === "descendant" || element.relation === "self";
|
|
1244
|
+
if (!stray) continue;
|
|
1245
|
+
const values = [
|
|
1246
|
+
element.theme !== null ? `data-cd-theme="${element.theme}"` : "",
|
|
1247
|
+
element.kontrast !== null ? `data-cd-kontrast="${element.kontrast}"` : ""
|
|
1248
|
+
].filter(Boolean).join(" ");
|
|
1249
|
+
const sameMode = carrier !== void 0 && element.theme === carrier.theme && element.kontrast === carrier.kontrast;
|
|
1250
|
+
problems.push(
|
|
1251
|
+
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)`
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
if (ownDocument && !facts.documentClasses.includes("cd-shell-document")) {
|
|
1255
|
+
problems.push(
|
|
1256
|
+
"<html> lacks cd-shell-document in an own document (the reset of the webview's host styles)"
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
if (!ownDocument && facts.documentClasses.includes("cd-shell-document")) {
|
|
1260
|
+
problems.push(
|
|
1261
|
+
"<html> of a foreign page carries cd-shell-document: the own-document reset restyles the host page"
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
const reference = facts.parts[0];
|
|
1265
|
+
if (reference && !reference.roles["--cd-ui-text"]) {
|
|
1266
|
+
problems.push(
|
|
1267
|
+
"--cd-ui-text is empty at the shell root: the vendored cd-tokens.generated.css is not loaded"
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
for (const part of facts.parts) {
|
|
1271
|
+
const schemes = part.colorScheme.split(/\s+/);
|
|
1272
|
+
const dark = schemes.includes("dark");
|
|
1273
|
+
const light = schemes.includes("light");
|
|
1274
|
+
if (expected.theme === "light" && (dark || carrier && !light)) {
|
|
1275
|
+
problems.push(`${part.name}: color-scheme is "${part.colorScheme}" (${label})`);
|
|
1276
|
+
}
|
|
1277
|
+
if (expected.theme === "dark" && (!dark || light)) {
|
|
1278
|
+
problems.push(`${part.name}: color-scheme is "${part.colorScheme}" (${label})`);
|
|
1279
|
+
}
|
|
1280
|
+
if (reference && part !== reference) {
|
|
1281
|
+
for (const [role, value] of Object.entries(part.roles)) {
|
|
1282
|
+
if (value !== reference.roles[role]) {
|
|
1283
|
+
problems.push(
|
|
1284
|
+
`${part.name}: ${role} resolves to "${value}", the root to "${reference.roles[role] ?? ""}" (half-dark)`
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
for (const required of ["dropdown", "tooltip"]) {
|
|
1291
|
+
if (!facts.parts.some((part) => part.name === required)) {
|
|
1292
|
+
problems.push(`${required}: not visible when the kit opened it, so its mode was not checked`);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
return { problems };
|
|
1296
|
+
}
|
|
1297
|
+
var SHELL_ROOT_ATTRIBUTES = [
|
|
1298
|
+
"data-cd-shell-host",
|
|
1299
|
+
"data-palette",
|
|
1300
|
+
"data-legend",
|
|
1301
|
+
"data-inspector",
|
|
1302
|
+
"data-problems",
|
|
1303
|
+
"data-readonly"
|
|
1304
|
+
];
|
|
1305
|
+
var EMBEDDED_ROOT_ATTRIBUTES = ["data-cd-theme", "data-cd-kontrast"];
|
|
1306
|
+
var BENIGN_ERRORS = /ResizeObserver loop/;
|
|
1307
|
+
function leftoverProblems(facts, host) {
|
|
1308
|
+
const problems = [];
|
|
1309
|
+
for (const element of facts.classedElements) problems.push(`${element} is left after destroy`);
|
|
1310
|
+
const ownDocument = host !== "embedded";
|
|
1311
|
+
for (const name2 of facts.documentClasses) {
|
|
1312
|
+
if (ownDocument && name2 === "cd-shell-document") continue;
|
|
1313
|
+
problems.push(`<html> keeps the class ${name2} after destroy`);
|
|
1314
|
+
}
|
|
1315
|
+
for (const element of facts.chromeElements) problems.push(`${element} is left after destroy`);
|
|
1316
|
+
for (const name2 of facts.rootClasses) problems.push(`the root keeps the class ${name2} after destroy`);
|
|
1317
|
+
const written = ownDocument ? SHELL_ROOT_ATTRIBUTES : [...SHELL_ROOT_ATTRIBUTES, ...EMBEDDED_ROOT_ATTRIBUTES];
|
|
1318
|
+
for (const name2 of facts.rootAttributes) {
|
|
1319
|
+
if (written.includes(name2)) problems.push(`the root keeps ${name2} after destroy`);
|
|
1320
|
+
}
|
|
1321
|
+
for (const error of facts.errors) {
|
|
1322
|
+
if (!BENIGN_ERRORS.test(error)) problems.push(`the page reported an error after destroy: ${error}`);
|
|
1323
|
+
}
|
|
1324
|
+
return problems;
|
|
1325
|
+
}
|
|
1326
|
+
function afterDestroyKeyProblem(press, recorded) {
|
|
1327
|
+
if (recorded.length === 0)
|
|
1328
|
+
return `${press} after destroy never reached window: a shell listener still stops it`;
|
|
1329
|
+
if (recorded.some((key) => key.defaultPrevented)) {
|
|
1330
|
+
return `${press} after destroy arrived prevented: a shell listener is still installed`;
|
|
1331
|
+
}
|
|
1332
|
+
return void 0;
|
|
1333
|
+
}
|
|
1334
|
+
var ABSENT_ISLANDS = ["legend", "inspector", "problems", "status"];
|
|
1335
|
+
function absentIslands(absent) {
|
|
1336
|
+
return ABSENT_ISLANDS.filter((island) => absent?.includes(island));
|
|
1337
|
+
}
|
|
1338
|
+
function absentIslandProblems(visible, absent) {
|
|
1339
|
+
return absentIslands(absent).filter((island) => visible.has(island)).map((island) => `${island}: visible, but harness.absent says the modeler has no ${island}`);
|
|
1340
|
+
}
|
|
1341
|
+
function harnessHookProblems(harness, hooks) {
|
|
1342
|
+
return hooks.filter((hook) => harness.absent?.includes(hook) && harness.show?.[hook]).map((hook) => `harness: absent lists "${hook}", but show.${hook} is given`);
|
|
1343
|
+
}
|
|
1344
|
+
function surroundProblems(facts) {
|
|
1345
|
+
if (!facts.container) return ["canvas surround: no .djs-container in the root to check"];
|
|
1346
|
+
if (!facts.painted) {
|
|
1347
|
+
return [
|
|
1348
|
+
"canvas surround: nothing from .djs-container out to the root paints a background colour; the surround is var(--cd-ui-grund) (guide \xA710.1)"
|
|
1349
|
+
];
|
|
1350
|
+
}
|
|
1351
|
+
if (facts.painted.color === facts.grund) return [];
|
|
1352
|
+
return [
|
|
1353
|
+
`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)`
|
|
1354
|
+
];
|
|
1355
|
+
}
|
|
1356
|
+
function isToolEntry(entry) {
|
|
1357
|
+
return entry.group === "tools" || entry.action.endsWith("-tool");
|
|
1358
|
+
}
|
|
1359
|
+
function paletteEntryState(entry) {
|
|
1360
|
+
if (entry.disabled) return "aria-disabled";
|
|
1361
|
+
if (entry.hovered) return entry.pressed ? "pressed and hovered" : "hovered";
|
|
1362
|
+
return entry.pressed ? "pressed" : "at rest";
|
|
1363
|
+
}
|
|
1364
|
+
function paletteColorProblems(facts) {
|
|
1365
|
+
const problems = [];
|
|
1366
|
+
for (const entry of facts.entries) {
|
|
1367
|
+
if (!isToolEntry(entry)) continue;
|
|
1368
|
+
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];
|
|
1369
|
+
const state = paletteEntryState(entry);
|
|
1370
|
+
if (entry.color !== value) {
|
|
1371
|
+
problems.push(
|
|
1372
|
+
`palette: tool "${entry.action}" ${state} has color ${entry.color}, expected ${role} (${value}) (guide \xA75.5)`
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
if (entry.icon && entry.icon.color !== value) {
|
|
1376
|
+
const icon = entry.icon.kind === "svg" ? "<svg> icon" : "::before icon";
|
|
1377
|
+
problems.push(
|
|
1378
|
+
`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)`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return problems;
|
|
1383
|
+
}
|
|
1384
|
+
var round2 = (value) => Math.round(value * 10) / 10;
|
|
1385
|
+
function createProblems(facts) {
|
|
1386
|
+
const known = new Set(facts.before.map((shape2) => shape2.id));
|
|
1387
|
+
const added = facts.after.filter((shape2) => !known.has(shape2.id));
|
|
1388
|
+
const pressed = `Enter on palette entry "${facts.entry}"`;
|
|
1389
|
+
if (added.length === 0) {
|
|
1390
|
+
return [
|
|
1391
|
+
`create: ${pressed} added no element; Enter on a creating entry places it in the centre of the free rectangle (guide \xA75.5)`
|
|
1392
|
+
];
|
|
1393
|
+
}
|
|
1394
|
+
if (added.length > 1) {
|
|
1395
|
+
return [
|
|
1396
|
+
`create: ${pressed} added ${added.length} elements (${added.map((shape2) => shape2.id).join(", ")}), expected exactly one`
|
|
1397
|
+
];
|
|
1398
|
+
}
|
|
1399
|
+
const [shape] = added;
|
|
1400
|
+
const problems = [];
|
|
1401
|
+
if (!shape.selected) problems.push(`create: the new element "${shape.id}" is not selected (guide \xA75.5)`);
|
|
1402
|
+
if (!facts.canvas.focusInCanvas) {
|
|
1403
|
+
problems.push(
|
|
1404
|
+
`create: after ${pressed} the focus is on ${facts.canvas.focused}, expected the canvas (its <svg> or the label editor, guide \xA75.5)`
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
const centre = { x: shape.box.left + shape.box.width / 2, y: shape.box.top + shape.box.height / 2 };
|
|
1408
|
+
const { free } = facts;
|
|
1409
|
+
const tolerance = GEOMETRY_TOLERANCE_PX + 1e-6;
|
|
1410
|
+
const inside = centre.x >= free.left - tolerance && centre.x <= free.right + tolerance && centre.y >= free.top - tolerance && centre.y <= free.bottom + tolerance;
|
|
1411
|
+
if (!inside) {
|
|
1412
|
+
problems.push(
|
|
1413
|
+
`create: the new element "${shape.id}" is centred at (${round2(centre.x)}, ${round2(centre.y)})px, outside the free rectangle ${round2(free.left)}\u2013${round2(free.right)} \xD7 ${round2(free.top)}\u2013${round2(free.bottom)}px (guide \xA75.5, \xA75.9)`
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
return problems;
|
|
1417
|
+
}
|
|
1418
|
+
function readOnlyStateProblems(facts) {
|
|
1419
|
+
const problems = [];
|
|
1420
|
+
const after = `${INVALID_STATUS_DELAY_MS}ms after show.invalid`;
|
|
1421
|
+
if (!facts.readOnly) problems.push(`read-only: the root has no data-readonly ${after}`);
|
|
1422
|
+
const status = facts.status;
|
|
1423
|
+
if (!status?.visible) {
|
|
1424
|
+
problems.push(`read-only: the status box is not visible ${after} (guide \xA75.12)`);
|
|
1425
|
+
} else if (status.attributes.role !== "alert") {
|
|
1426
|
+
problems.push(
|
|
1427
|
+
`read-only: the status box of an invalid file has role "${status.attributes.role ?? ""}", expected "alert" (guide \xA713.5)`
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
return problems;
|
|
1431
|
+
}
|
|
1432
|
+
var READ_ONLY_PALETTE_ACTIONS = ["lasso-tool", "hand-tool"];
|
|
1433
|
+
function readOnlyPaletteProblems(entries) {
|
|
1434
|
+
return entries.flatMap((entry) => {
|
|
1435
|
+
const staysActive = READ_ONLY_PALETTE_ACTIONS.includes(entry.action);
|
|
1436
|
+
if (staysActive && entry.disabled) {
|
|
1437
|
+
return [
|
|
1438
|
+
`palette: "${entry.action}" is aria-disabled on the read-only canvas; Lasso and Hand stay active (guide \xA713.5)`
|
|
1439
|
+
];
|
|
1440
|
+
}
|
|
1441
|
+
if (!staysActive && !entry.disabled) {
|
|
1442
|
+
return [`palette: "${entry.action}" is not aria-disabled on the read-only canvas (guide \xA713.5)`];
|
|
1443
|
+
}
|
|
1444
|
+
return [];
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
var LEGEND_MENU_ID = "legend";
|
|
1448
|
+
function readOnlyMenuProblems(before, after) {
|
|
1449
|
+
const problems = [];
|
|
1450
|
+
const key = (entry) => `${entry.menuId ?? ""}\0${entry.name}`;
|
|
1451
|
+
const enabledBefore = new Set(before.filter((entry) => !entry.disabled).map(key));
|
|
1452
|
+
for (const entry of after) {
|
|
1453
|
+
if (!entry.disabled) continue;
|
|
1454
|
+
if (entry.menuId === LEGEND_MENU_ID && entry.role === "menuitemcheckbox") {
|
|
1455
|
+
problems.push(
|
|
1456
|
+
"menu: the Legend toggle is aria-disabled on the read-only canvas; a view-only toggle stays usable (guide \xA713.5)"
|
|
1457
|
+
);
|
|
1458
|
+
} else if (entry.group !== "model" && enabledBefore.has(key(entry))) {
|
|
1459
|
+
problems.push(
|
|
1460
|
+
`menu: "${entry.name}" is aria-disabled on the read-only canvas; export and help stay usable (guide \xA713.5)`
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return problems;
|
|
1465
|
+
}
|
|
1466
|
+
function lockedShapeProblems(facts) {
|
|
1467
|
+
const problems = [];
|
|
1468
|
+
const { before, after } = facts.dragged;
|
|
1469
|
+
if (facts.contextPadEntries.length > 0) {
|
|
1470
|
+
problems.push(
|
|
1471
|
+
`read-only: selecting "${before.id}" shows the context pad (${facts.contextPadEntries.join(", ")}) (guide \xA713.5)`
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
if (!after) {
|
|
1475
|
+
problems.push(`read-only: a 40px drag removed "${before.id}" from the canvas (guide \xA713.5)`);
|
|
1476
|
+
} 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) {
|
|
1477
|
+
problems.push(
|
|
1478
|
+
`read-only: a 40px drag changed "${before.id}" (transform "${before.transform}" \u2192 "${after.transform}", ${round2(before.size.width)} \xD7 ${round2(before.size.height)} \u2192 ${round2(after.size.width)} \xD7 ${round2(after.size.height)}) (guide \xA713.5)`
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
if (facts.textFields.length > 0) {
|
|
1482
|
+
problems.push(
|
|
1483
|
+
`read-only: a double-click on "${before.id}" opens a text field (${facts.textFields.join(", ")}) (guide \xA713.5)`
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
for (const key of ["Delete", "Backspace"]) {
|
|
1487
|
+
if (!facts.kept[key])
|
|
1488
|
+
problems.push(`read-only: ${key} on the canvas deleted "${before.id}" (guide \xA713.5)`);
|
|
1489
|
+
}
|
|
1490
|
+
return problems;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// src/testing/cases.ts
|
|
1494
|
+
var DEFAULT_SIZES = [
|
|
1495
|
+
{ width: 1280, height: 900 },
|
|
1496
|
+
{ width: 720, height: 700 },
|
|
1497
|
+
{ width: 420, height: 700 }
|
|
1498
|
+
];
|
|
1499
|
+
var LIGHT = { theme: "light" };
|
|
1500
|
+
var SHOWN_ISLANDS = ["legend", "problems", "status", "inspector"];
|
|
1501
|
+
var LOCKED_DRAG_PX = 40;
|
|
1502
|
+
var CREATE_WAIT_MS = 1e3;
|
|
1503
|
+
var READ_ONLY_WAIT_MS = 1e3;
|
|
1504
|
+
var COLOR_WAIT_MS = 600;
|
|
1505
|
+
var POLL_MS = 50;
|
|
1506
|
+
var REQUESTED_APPEARANCES = [
|
|
1507
|
+
{ theme: "dark" },
|
|
1508
|
+
{ theme: "dark", kontrast: "hoch" },
|
|
1509
|
+
{ theme: "light", kontrast: "hoch" }
|
|
1510
|
+
];
|
|
1511
|
+
var HOVER_MARGIN_MS = 200;
|
|
1512
|
+
var PARK = { x: 1, y: 1 };
|
|
1513
|
+
var TEXT_FIELDS = [
|
|
1514
|
+
["input", "a text field"],
|
|
1515
|
+
["textarea", "a textarea"],
|
|
1516
|
+
["contenteditable", "an editable label"]
|
|
1517
|
+
];
|
|
1518
|
+
var UNDO_REDO_PRESSES = [
|
|
1519
|
+
"Control+KeyZ",
|
|
1520
|
+
"Meta+KeyZ",
|
|
1521
|
+
"Control+Shift+KeyZ",
|
|
1522
|
+
"Meta+Shift+KeyZ",
|
|
1523
|
+
"Control+KeyY"
|
|
1524
|
+
];
|
|
1525
|
+
var ONE_STEP_UP = 135;
|
|
1526
|
+
var TWO_STEPS_UP = 182;
|
|
1527
|
+
var ZOOM_LIMIT_MAX = 400;
|
|
1528
|
+
var ZOOM_LIMIT_MIN = 20;
|
|
1529
|
+
function sizeLabel(size) {
|
|
1530
|
+
return `${size.width} \xD7 ${size.height}`;
|
|
1531
|
+
}
|
|
1532
|
+
function appearanceLabel(appearance) {
|
|
1533
|
+
return appearance.kontrast ? `${appearance.theme} + ${appearance.kontrast}` : appearance.theme;
|
|
1534
|
+
}
|
|
1535
|
+
function fail(caseName, host, problems) {
|
|
1536
|
+
const unique = [...new Set(problems)];
|
|
1537
|
+
throw new Error(
|
|
1538
|
+
`@miragon/modeler-shell conformance: ${caseName} (${host} host)
|
|
1539
|
+
${unique.map((problem) => `- ${problem}`).join("\n")}`
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
function finish(caseName, host, problems, skipped) {
|
|
1543
|
+
if (problems.length > 0) fail(caseName, host, problems);
|
|
1544
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1545
|
+
const unique = [...skipped].filter((skip) => !seen.has(skip.reason) && seen.add(skip.reason));
|
|
1546
|
+
return unique.length > 0 ? { skipped: unique } : void 0;
|
|
1547
|
+
}
|
|
1548
|
+
function assertHooksConsistent(caseName, harness, hooks) {
|
|
1549
|
+
const problems = harnessHookProblems(harness, hooks);
|
|
1550
|
+
if (problems.length > 0) fail(caseName, harness.host, problems);
|
|
1551
|
+
}
|
|
1552
|
+
function unfed(harness, hook) {
|
|
1553
|
+
return !harness.show?.[hook] && !harness.absent?.includes(hook);
|
|
1554
|
+
}
|
|
1555
|
+
function missingHooks(harness, what) {
|
|
1556
|
+
return SHOWN_ISLANDS.filter((island) => unfed(harness, island)).map((island) => ({
|
|
1557
|
+
hook: island,
|
|
1558
|
+
reason: `show.${island} is undefined: ${what} of the ${island} island not verified`
|
|
1559
|
+
}));
|
|
1560
|
+
}
|
|
1561
|
+
async function pollUntil(page, read, done, timeoutMs) {
|
|
1562
|
+
let value = await read();
|
|
1563
|
+
for (let waited = 0; !done(value) && waited < timeoutMs; waited += POLL_MS) {
|
|
1564
|
+
await page.waitForTimeout(POLL_MS);
|
|
1565
|
+
value = await read();
|
|
1566
|
+
}
|
|
1567
|
+
return value;
|
|
1568
|
+
}
|
|
1569
|
+
function visibleIslands(facts) {
|
|
1570
|
+
const islands = /* @__PURE__ */ new Set();
|
|
1571
|
+
for (const island of SHOWN_ISLANDS) if (facts[island]?.visible) islands.add(island);
|
|
1572
|
+
return islands;
|
|
1573
|
+
}
|
|
1574
|
+
async function hover(page, root, target) {
|
|
1575
|
+
const anchor = await findTarget(page, root, target);
|
|
1576
|
+
if (!anchor) return null;
|
|
1577
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
1578
|
+
await page.mouse.move(anchor.client.x, anchor.client.y);
|
|
1579
|
+
await page.waitForTimeout(TOOLTIP_DELAY_MS + HOVER_MARGIN_MS);
|
|
1580
|
+
await settle(page);
|
|
1581
|
+
return anchor;
|
|
1582
|
+
}
|
|
1583
|
+
async function openMenuByClick(page, root) {
|
|
1584
|
+
const trigger = await findTarget(page, root, "menu");
|
|
1585
|
+
if (!trigger) return false;
|
|
1586
|
+
await page.mouse.click(trigger.client.x, trigger.client.y);
|
|
1587
|
+
await settle(page);
|
|
1588
|
+
return true;
|
|
1589
|
+
}
|
|
1590
|
+
async function closeByEscape(page) {
|
|
1591
|
+
await page.keyboard.press("Escape");
|
|
1592
|
+
await settle(page);
|
|
1593
|
+
}
|
|
1594
|
+
async function popupGeometry(page, root, measurement) {
|
|
1595
|
+
const problems = [];
|
|
1596
|
+
if (!await openMenuByClick(page, root)) return ["dropdown: no visible menu trigger to open it"];
|
|
1597
|
+
const dropdown = await findOverlay(page, root, "menu");
|
|
1598
|
+
if (!dropdown) problems.push('dropdown: no visible role="menu" after clicking the trigger');
|
|
1599
|
+
else problems.push(...dropdownProblems(dropdown.box, measurement.container));
|
|
1600
|
+
await closeByEscape(page);
|
|
1601
|
+
const rows = [
|
|
1602
|
+
["menu", "top"],
|
|
1603
|
+
["palette-entry", measurement.paletteRow === "second-row" ? "second-row" : "top"],
|
|
1604
|
+
["zoom-in", "bottom"]
|
|
1605
|
+
];
|
|
1606
|
+
for (const [target, row] of rows) {
|
|
1607
|
+
const anchor = await hover(page, root, target);
|
|
1608
|
+
if (!anchor) {
|
|
1609
|
+
problems.push(`tooltip: no visible ${target} to hover`);
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
const tooltip = await findOverlay(page, root, "tooltip");
|
|
1613
|
+
if (!tooltip) {
|
|
1614
|
+
problems.push(`tooltip: none visible ${TOOLTIP_DELAY_MS + HOVER_MARGIN_MS}ms after hovering ${target}`);
|
|
1615
|
+
continue;
|
|
1616
|
+
}
|
|
1617
|
+
if (tooltip.count > 1) problems.push(`tooltip: ${tooltip.count} tooltips visible at once`);
|
|
1618
|
+
if (anchor.label && !tooltip.text.startsWith(anchor.label)) {
|
|
1619
|
+
problems.push(`tooltip: hovering ${target} ("${anchor.label}") shows "${tooltip.text}"`);
|
|
1620
|
+
}
|
|
1621
|
+
problems.push(
|
|
1622
|
+
...tooltipProblems(tooltip.box, anchor.box, row, measurement.container).map(
|
|
1623
|
+
(problem) => `${problem} (on ${target})`
|
|
1624
|
+
)
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
1628
|
+
return problems;
|
|
1629
|
+
}
|
|
1630
|
+
var geometryCase = {
|
|
1631
|
+
name: "geometry",
|
|
1632
|
+
async run(page, harness) {
|
|
1633
|
+
assertHooksConsistent("geometry", harness, SHOWN_ISLANDS);
|
|
1634
|
+
const root = harness.root ?? DEFAULT_ROOT;
|
|
1635
|
+
const problems = [];
|
|
1636
|
+
for (const size of harness.sizes ?? DEFAULT_SIZES) {
|
|
1637
|
+
const at = sizeLabel(size);
|
|
1638
|
+
await harness.open(page, { size, requested: LIGHT });
|
|
1639
|
+
await settle(page);
|
|
1640
|
+
const check = async (state) => {
|
|
1641
|
+
const measurement = await measureChrome(page, root);
|
|
1642
|
+
problems.push(
|
|
1643
|
+
...[
|
|
1644
|
+
...geometryProblems(measurement, harness.host),
|
|
1645
|
+
...absentIslandProblems(new Set(Object.keys(measurement.islands)), harness.absent)
|
|
1646
|
+
].map((problem) => `[${at}, ${state}] ${problem}`)
|
|
1647
|
+
);
|
|
1648
|
+
return measurement;
|
|
1649
|
+
};
|
|
1650
|
+
const opened = await check("as opened");
|
|
1651
|
+
problems.push(...(await popupGeometry(page, root, opened)).map((problem) => `[${at}] ${problem}`));
|
|
1652
|
+
for (const island of SHOWN_ISLANDS) {
|
|
1653
|
+
const show = harness.show?.[island];
|
|
1654
|
+
if (!show) continue;
|
|
1655
|
+
await show(page);
|
|
1656
|
+
await settle(page);
|
|
1657
|
+
await check(`after show.${island}`);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return finish("geometry", harness.host, problems, missingHooks(harness, "the geometry"));
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
async function menuState(page, root) {
|
|
1664
|
+
const facts = await inPage(page, menuProbe, { root });
|
|
1665
|
+
if (!facts) throw new Error(`@miragon/modeler-shell conformance: no element matches "${root}"`);
|
|
1666
|
+
return facts;
|
|
1667
|
+
}
|
|
1668
|
+
async function menuKeyboardProblems(page, root) {
|
|
1669
|
+
const problems = [];
|
|
1670
|
+
const openWith = async (key, focus) => {
|
|
1671
|
+
if (!await focusTarget(page, root, "menu")) {
|
|
1672
|
+
problems.push("menu: the trigger cannot take focus");
|
|
1673
|
+
return void 0;
|
|
1674
|
+
}
|
|
1675
|
+
await page.keyboard.press(key);
|
|
1676
|
+
await settle(page);
|
|
1677
|
+
const opened2 = await menuState(page, root);
|
|
1678
|
+
problems.push(...menuOpenProblems(opened2, focus).map((problem) => `${key} on the trigger: ${problem}`));
|
|
1679
|
+
return opened2;
|
|
1680
|
+
};
|
|
1681
|
+
const close = async (key) => {
|
|
1682
|
+
await closeByEscape(page);
|
|
1683
|
+
problems.push(
|
|
1684
|
+
...menuClosedProblems(await menuState(page, root), true).map(
|
|
1685
|
+
(problem) => `${key}, then Escape: ${problem}`
|
|
1686
|
+
)
|
|
1687
|
+
);
|
|
1688
|
+
};
|
|
1689
|
+
const opened = await openWith("Enter", "first");
|
|
1690
|
+
if (opened && opened.items.length > 1) {
|
|
1691
|
+
const moves = [
|
|
1692
|
+
["ArrowUp", (count) => count - 1],
|
|
1693
|
+
["ArrowDown", () => 0],
|
|
1694
|
+
["End", (count) => count - 1],
|
|
1695
|
+
["Home", () => 0]
|
|
1696
|
+
];
|
|
1697
|
+
await inPage(page, clearKeysProbe, null);
|
|
1698
|
+
for (const [key, target] of moves) {
|
|
1699
|
+
await page.keyboard.press(key);
|
|
1700
|
+
const moved = await menuState(page, root);
|
|
1701
|
+
const expected = target(moved.items.length);
|
|
1702
|
+
if (moved.active !== expected) {
|
|
1703
|
+
problems.push(
|
|
1704
|
+
`menu: ${key} moves the focus to entry ${moved.active + 1}, expected entry ${expected + 1} of ${moved.items.length}`
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
const { documentKeys } = await inPage(page, afterPressProbe, { root });
|
|
1709
|
+
problems.push(
|
|
1710
|
+
...menuKeysAtDocumentProblems(
|
|
1711
|
+
moves.map(([key]) => key),
|
|
1712
|
+
documentKeys
|
|
1713
|
+
)
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
if (opened) await close("Enter");
|
|
1717
|
+
if (await openWith("ArrowDown", "first")) await close("ArrowDown");
|
|
1718
|
+
if (await openWith("ArrowUp", "last")) await close("ArrowUp");
|
|
1719
|
+
return problems;
|
|
1720
|
+
}
|
|
1721
|
+
async function paletteKeyboardProblems(page, root) {
|
|
1722
|
+
const before = await inPage(page, paletteFocusProbe, { root });
|
|
1723
|
+
if (!before || before.entries.length < 2) return [];
|
|
1724
|
+
if (!await focusTarget(page, root, "palette-tab-stop")) {
|
|
1725
|
+
return ['palette: no entry with tabindex="0" takes the focus'];
|
|
1726
|
+
}
|
|
1727
|
+
const problems = [];
|
|
1728
|
+
const start = before.entries.indexOf(before.tabStops[0] ?? "");
|
|
1729
|
+
const step = start < before.entries.length - 1 ? 1 : -1;
|
|
1730
|
+
const moves = [
|
|
1731
|
+
[step === 1 ? "ArrowRight" : "ArrowLeft", start + step],
|
|
1732
|
+
["End", before.entries.length - 1],
|
|
1733
|
+
["Home", 0]
|
|
1734
|
+
];
|
|
1735
|
+
for (const [key, index] of moves) {
|
|
1736
|
+
await page.keyboard.press(key);
|
|
1737
|
+
const after = await inPage(page, paletteFocusProbe, { root });
|
|
1738
|
+
const expected = before.entries[index];
|
|
1739
|
+
if (!after || after.active !== expected) {
|
|
1740
|
+
problems.push(`palette: ${key} focuses "${after?.active ?? "nothing"}", expected "${expected}"`);
|
|
1741
|
+
} else if (after.tabStops.length !== 1 || after.tabStops[0] !== expected) {
|
|
1742
|
+
problems.push(
|
|
1743
|
+
`palette: after ${key} tabindex="0" is on ${after.tabStops.join(", ") || "no entry"}, expected "${expected}"`
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return problems;
|
|
1748
|
+
}
|
|
1749
|
+
async function paletteShortcutWalk(page, root) {
|
|
1750
|
+
const before = await inPage(page, paletteFocusProbe, { root });
|
|
1751
|
+
if (!before || before.entries.length === 0) return [];
|
|
1752
|
+
if (!await focusTarget(page, root, "palette-tab-stop")) return [];
|
|
1753
|
+
if (before.entries.length > 1) await page.keyboard.press("End");
|
|
1754
|
+
await page.keyboard.press("Home");
|
|
1755
|
+
const seen = [];
|
|
1756
|
+
for (let index = 0; index < before.entries.length; index++) {
|
|
1757
|
+
const facts = await inPage(page, paletteShortcutProbe, { root });
|
|
1758
|
+
if (facts) seen.push(facts);
|
|
1759
|
+
await page.keyboard.press("ArrowRight");
|
|
1760
|
+
}
|
|
1761
|
+
return paletteShortcutProblems(seen);
|
|
1762
|
+
}
|
|
1763
|
+
async function tabSequenceFromMenu(page, root) {
|
|
1764
|
+
if (!await focusTarget(page, root, "menu")) return [];
|
|
1765
|
+
const stops = [];
|
|
1766
|
+
for (let step = 0; step < 3; step++) {
|
|
1767
|
+
await page.keyboard.press("Tab");
|
|
1768
|
+
const stop = await inPage(page, activeStopProbe, { root });
|
|
1769
|
+
stops.push({ description: stop.description, kind: stop.kind });
|
|
1770
|
+
}
|
|
1771
|
+
return tabSequenceProblems(stops);
|
|
1772
|
+
}
|
|
1773
|
+
async function zoomLimitProblems(page, root) {
|
|
1774
|
+
const problems = [];
|
|
1775
|
+
const limits = [
|
|
1776
|
+
["in", "zoom-in", ZOOM_LIMIT_MAX, 12, "out"],
|
|
1777
|
+
["out", "zoom-out", ZOOM_LIMIT_MIN, 16, "in"]
|
|
1778
|
+
];
|
|
1779
|
+
for (const [button, target, limit, maxClicks, other] of limits) {
|
|
1780
|
+
const control = await findTarget(page, root, target);
|
|
1781
|
+
if (!control) continue;
|
|
1782
|
+
let buttons = await inPage(page, zoomButtonsProbe, { root });
|
|
1783
|
+
for (let click = 0; click < maxClicks && buttons[button]?.attributes["aria-disabled"] !== "true"; click++) {
|
|
1784
|
+
await page.mouse.click(control.client.x, control.client.y);
|
|
1785
|
+
await settle(page);
|
|
1786
|
+
buttons = await inPage(page, zoomButtonsProbe, { root });
|
|
1787
|
+
}
|
|
1788
|
+
const { zoom } = await inPage(page, afterPressProbe, { root });
|
|
1789
|
+
const reached = buttons[button];
|
|
1790
|
+
if (reached?.attributes["aria-disabled"] !== "true") {
|
|
1791
|
+
problems.push(
|
|
1792
|
+
`zoom: [data-zoom="${button}"] is not aria-disabled after ${maxClicks} clicks (readout ${zoom ?? "?"}%)`
|
|
1793
|
+
);
|
|
1794
|
+
} else {
|
|
1795
|
+
if (zoom !== limit) {
|
|
1796
|
+
problems.push(`zoom: [data-zoom="${button}"] is disabled at ${zoom ?? "?"}%, expected at ${limit}%`);
|
|
1797
|
+
}
|
|
1798
|
+
if (reached.disabled) problems.push(`zoom: [data-zoom="${button}"] is natively disabled at the limit`);
|
|
1799
|
+
}
|
|
1800
|
+
if (buttons[other]?.attributes["aria-disabled"] === "true") {
|
|
1801
|
+
problems.push(`zoom: [data-zoom="${other}"] is aria-disabled at ${zoom ?? "?"}%`);
|
|
1802
|
+
}
|
|
1803
|
+
if (buttons.actual?.attributes["aria-disabled"] === "true") {
|
|
1804
|
+
problems.push("zoom: the readout is disabled at a limit");
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
1808
|
+
return problems;
|
|
1809
|
+
}
|
|
1810
|
+
var ariaCase = {
|
|
1811
|
+
name: "aria",
|
|
1812
|
+
async run(page, harness) {
|
|
1813
|
+
assertHooksConsistent("aria", harness, SHOWN_ISLANDS);
|
|
1814
|
+
const root = harness.root ?? DEFAULT_ROOT;
|
|
1815
|
+
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
|
|
1816
|
+
await harness.open(page, { size, requested: LIGHT });
|
|
1817
|
+
await settle(page);
|
|
1818
|
+
const facts = await inPage(page, ariaProbe, { root });
|
|
1819
|
+
if (!facts) fail("aria", harness.host, [`no element matches the shell root selector "${root}"`]);
|
|
1820
|
+
const problems = ariaProblems(facts, harness.host);
|
|
1821
|
+
problems.push(...await menuKeyboardProblems(page, root));
|
|
1822
|
+
problems.push(...await paletteKeyboardProblems(page, root));
|
|
1823
|
+
problems.push(...await paletteShortcutWalk(page, root));
|
|
1824
|
+
problems.push(...await tabSequenceFromMenu(page, root));
|
|
1825
|
+
problems.push(...await zoomLimitProblems(page, root));
|
|
1826
|
+
const shown = /* @__PURE__ */ new Set();
|
|
1827
|
+
for (const island of SHOWN_ISLANDS) {
|
|
1828
|
+
const show = harness.show?.[island];
|
|
1829
|
+
if (!show) continue;
|
|
1830
|
+
await show(page);
|
|
1831
|
+
shown.add(island);
|
|
1832
|
+
}
|
|
1833
|
+
let final = facts;
|
|
1834
|
+
if (shown.size > 0) {
|
|
1835
|
+
await settle(page);
|
|
1836
|
+
const withIslands = await inPage(page, ariaProbe, { root });
|
|
1837
|
+
if (withIslands) {
|
|
1838
|
+
final = withIslands;
|
|
1839
|
+
problems.push(...optionalIslandProblems(withIslands, shown));
|
|
1840
|
+
const known = new Set(problems);
|
|
1841
|
+
problems.push(
|
|
1842
|
+
...ariaProblems(withIslands, harness.host).filter((problem) => !known.has(problem)).map((problem) => `with every island shown: ${problem}`)
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
problems.push(...absentIslandProblems(visibleIslands(final), harness.absent));
|
|
1847
|
+
return finish("aria", harness.host, problems, missingHooks(harness, "the roles and names"));
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
var keysCase = {
|
|
1851
|
+
name: "keys",
|
|
1852
|
+
async run(page, harness) {
|
|
1853
|
+
const root = harness.root ?? DEFAULT_ROOT;
|
|
1854
|
+
const host = harness.host;
|
|
1855
|
+
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
|
|
1856
|
+
await harness.open(page, { size, requested: LIGHT });
|
|
1857
|
+
await settle(page);
|
|
1858
|
+
const problems = [];
|
|
1859
|
+
if (!await focusTarget(page, root, "canvas")) {
|
|
1860
|
+
fail("keys", host, ["canvas: the diagram-js <svg> (.djs-container > svg) cannot take focus"]);
|
|
1861
|
+
}
|
|
1862
|
+
await inPage(page, clearKeysProbe, null);
|
|
1863
|
+
let current = (await inPage(page, afterPressProbe, { root })).zoom;
|
|
1864
|
+
if (current === null) fail("keys", host, ['zoom: no readout [data-zoom="actual"] showing a percentage']);
|
|
1865
|
+
const press = async (key, reach, zoom, context = "on the canvas") => {
|
|
1866
|
+
const before = current;
|
|
1867
|
+
await page.keyboard.press(key);
|
|
1868
|
+
let after = await inPage(page, afterPressProbe, { root });
|
|
1869
|
+
const where = `${key} ${context}`;
|
|
1870
|
+
if (reach) {
|
|
1871
|
+
const problem = keyReachProblem(where, reach, host, after);
|
|
1872
|
+
if (problem) problems.push(problem);
|
|
1873
|
+
}
|
|
1874
|
+
if (zoom === void 0) return;
|
|
1875
|
+
const expected = zoom === "unchanged" ? before : zoom;
|
|
1876
|
+
for (let poll = 0; after.zoom === before && expected !== before && poll < 10; poll++) {
|
|
1877
|
+
await page.waitForTimeout(50);
|
|
1878
|
+
after = await inPage(page, afterPressProbe, { root });
|
|
1879
|
+
}
|
|
1880
|
+
if (after.zoom !== expected) {
|
|
1881
|
+
problems.push(
|
|
1882
|
+
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)`
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
current = after.zoom ?? current;
|
|
1886
|
+
};
|
|
1887
|
+
const inside = async (kind, context, steps) => {
|
|
1888
|
+
if (await inPage(page, injectProbe, { root, kind })) await steps();
|
|
1889
|
+
else problems.push(`cannot focus ${context} stand-in inside the root`);
|
|
1890
|
+
await inPage(page, removeInjectedProbe, null);
|
|
1891
|
+
};
|
|
1892
|
+
await press("Control+Digit0", "handled", 100);
|
|
1893
|
+
const pairs = [
|
|
1894
|
+
["Control+Equal", "Control+Minus"],
|
|
1895
|
+
["Control+Shift+Equal", "Control+Shift+Minus"],
|
|
1896
|
+
["Meta+Equal", "Meta+Minus"],
|
|
1897
|
+
["Meta+Shift+Equal", "Meta+Shift+Minus"],
|
|
1898
|
+
["Control+NumpadAdd", "Control+NumpadSubtract"],
|
|
1899
|
+
["Meta+NumpadAdd", "Meta+NumpadSubtract"]
|
|
1900
|
+
];
|
|
1901
|
+
for (const [zoomIn, zoomOut] of pairs) {
|
|
1902
|
+
await press(zoomIn, "handled", ONE_STEP_UP);
|
|
1903
|
+
await press(zoomOut, "handled", 100);
|
|
1904
|
+
}
|
|
1905
|
+
for (const zoomToActual of ["Meta+Digit0", "Control+Numpad0"]) {
|
|
1906
|
+
await press("Control+Equal", "handled", ONE_STEP_UP);
|
|
1907
|
+
await press(zoomToActual, "handled", 100);
|
|
1908
|
+
}
|
|
1909
|
+
for (const key of ["Equal", "Minus", "Shift+Equal", "Shift+Minus", "NumpadAdd", "NumpadSubtract"]) {
|
|
1910
|
+
await press(key, "free", "unchanged", "on the canvas without Cmd/Ctrl");
|
|
1911
|
+
}
|
|
1912
|
+
for (const key of ["Digit0", "Control+Alt+Equal", "Control+Alt+Minus", "Control+Shift+Digit1"]) {
|
|
1913
|
+
await press(key, void 0, "unchanged");
|
|
1914
|
+
}
|
|
1915
|
+
await press("F1", "free", void 0);
|
|
1916
|
+
await press("Escape", "free", void 0, "on the canvas without a menu or dialog");
|
|
1917
|
+
if (await inPage(page, canvasHandlesProbe, { root, key: "F9" })) {
|
|
1918
|
+
await press("F9", "canvas", void 0, "handled by the canvas");
|
|
1919
|
+
}
|
|
1920
|
+
await inPage(page, removeInjectedProbe, null);
|
|
1921
|
+
if (host === "ide") {
|
|
1922
|
+
for (const key of UNDO_REDO_PRESSES) {
|
|
1923
|
+
await press(key, "free", void 0, "on the canvas (undo and redo belong to the VS Code document)");
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
for (const key of ["Control+KeyA", "Meta+KeyA"]) {
|
|
1927
|
+
await press(key, "canvas", void 0, "on the canvas (select all acts on the canvas only)");
|
|
1928
|
+
}
|
|
1929
|
+
await press("Control+Equal", "handled", ONE_STEP_UP);
|
|
1930
|
+
for (const [kind, field] of TEXT_FIELDS) {
|
|
1931
|
+
await inside(kind, field, async () => {
|
|
1932
|
+
await press("Control+Equal", "blocked", "unchanged", `in ${field}`);
|
|
1933
|
+
await press("Control+Minus", "blocked", "unchanged", `in ${field}`);
|
|
1934
|
+
await press("Shift+Digit1", "free", "unchanged", `in ${field}`);
|
|
1935
|
+
await press("Control+KeyZ", "blocked", void 0, `in ${field}`);
|
|
1936
|
+
await press("Meta+Shift+KeyZ", "blocked", void 0, `in ${field}`);
|
|
1937
|
+
await press("Control+KeyY", "blocked", void 0, `in ${field}`);
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
await inside("modal", "a modal dialog", async () => {
|
|
1941
|
+
await press("Control+Equal", "blocked", "unchanged", "with a modal dialog open");
|
|
1942
|
+
await press("Shift+Digit1", "free", "unchanged", "with a modal dialog open");
|
|
1943
|
+
await press("Escape", "blocked", void 0, "with a modal dialog open");
|
|
1944
|
+
});
|
|
1945
|
+
await inside("popover", "a popover", async () => {
|
|
1946
|
+
await press("Control+Minus", "handled", 100, "in a popover");
|
|
1947
|
+
await press("Control+Equal", "handled", ONE_STEP_UP, "in a popover");
|
|
1948
|
+
await press("Escape", "blocked", void 0, "with a popover open");
|
|
1949
|
+
});
|
|
1950
|
+
if (await openMenuByClick(page, root)) {
|
|
1951
|
+
await inPage(page, clearKeysProbe, null);
|
|
1952
|
+
await press("Escape", "handled", void 0, "with the menu open");
|
|
1953
|
+
if (await findOverlay(page, root, "menu")) problems.push("Escape with the menu open did not close it");
|
|
1954
|
+
}
|
|
1955
|
+
await focusTarget(page, root, "canvas");
|
|
1956
|
+
await press("Control+Equal", "handled", TWO_STEPS_UP);
|
|
1957
|
+
await page.keyboard.press("Shift+Digit1");
|
|
1958
|
+
let fitted = await inPage(page, afterPressProbe, { root });
|
|
1959
|
+
const reachProblem = keyReachProblem("Shift+Digit1 on the canvas", "handled", host, fitted);
|
|
1960
|
+
if (reachProblem) problems.push(reachProblem);
|
|
1961
|
+
for (let poll = 0; (fitted.zoom === null || fitted.zoom > 100) && poll < 20; poll++) {
|
|
1962
|
+
await page.waitForTimeout(50);
|
|
1963
|
+
fitted = await inPage(page, afterPressProbe, { root });
|
|
1964
|
+
}
|
|
1965
|
+
if (fitted.zoom === null || fitted.zoom > 100) {
|
|
1966
|
+
problems.push(
|
|
1967
|
+
`Shift+Digit1 on the canvas left the zoom at ${fitted.zoom ?? "?"}%, expected a fit (at most 100%)`
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
return finish("keys", host, problems, []);
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
async function appearanceFacts(page, root) {
|
|
1974
|
+
const probe = () => inPage(page, appearanceProbe, { root, roles: MODE_ROLES });
|
|
1975
|
+
await hover(page, root, "zoom-in");
|
|
1976
|
+
const withTooltip = await probe();
|
|
1977
|
+
const opened = await openMenuByClick(page, root);
|
|
1978
|
+
const withMenu = await probe();
|
|
1979
|
+
if (opened) await closeByEscape(page);
|
|
1980
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
1981
|
+
if (!withTooltip || !withMenu) return withTooltip ?? withMenu;
|
|
1982
|
+
const names = new Set(withTooltip.parts.map((part) => part.name));
|
|
1983
|
+
return {
|
|
1984
|
+
...withMenu,
|
|
1985
|
+
parts: [...withTooltip.parts, ...withMenu.parts.filter((part) => !names.has(part.name))]
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
async function paletteColors(page, root) {
|
|
1989
|
+
const read = async () => {
|
|
1990
|
+
const facts = await inPage(page, paletteColorProbe, { root });
|
|
1991
|
+
return facts ? paletteColorProblems(facts) : [];
|
|
1992
|
+
};
|
|
1993
|
+
return pollUntil(page, read, (problems) => problems.length === 0, COLOR_WAIT_MS);
|
|
1994
|
+
}
|
|
1995
|
+
async function paletteColorWalk(page, root) {
|
|
1996
|
+
const problems = [];
|
|
1997
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
1998
|
+
problems.push(...await paletteColors(page, root));
|
|
1999
|
+
const tools = (await inPage(page, paletteEntriesProbe, { root })).filter(
|
|
2000
|
+
(entry) => isToolEntry(entry) && !entry.disabled
|
|
2001
|
+
);
|
|
2002
|
+
for (const tool of tools) {
|
|
2003
|
+
await page.mouse.move(tool.client.x, tool.client.y);
|
|
2004
|
+
problems.push(...await paletteColors(page, root));
|
|
2005
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
2006
|
+
}
|
|
2007
|
+
const armed = tools.find((tool) => tool.action === "lasso-tool") ?? tools.find((tool) => tool.action.endsWith("-tool"));
|
|
2008
|
+
if (armed) {
|
|
2009
|
+
await page.mouse.click(armed.client.x, armed.client.y);
|
|
2010
|
+
await settle(page);
|
|
2011
|
+
const pressed = (await inPage(page, paletteEntriesProbe, { root })).find(
|
|
2012
|
+
(entry) => entry.action === armed.action
|
|
2013
|
+
);
|
|
2014
|
+
if (!pressed?.pressed) {
|
|
2015
|
+
problems.push(`palette: clicking tool "${armed.action}" does not set aria-pressed="true" (guide \xA75.5)`);
|
|
2016
|
+
}
|
|
2017
|
+
problems.push(...await paletteColors(page, root));
|
|
2018
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
2019
|
+
problems.push(...await paletteColors(page, root));
|
|
2020
|
+
await closeByEscape(page);
|
|
2021
|
+
}
|
|
2022
|
+
return [...new Set(problems)];
|
|
2023
|
+
}
|
|
2024
|
+
var appearanceCase = {
|
|
2025
|
+
name: "appearance",
|
|
2026
|
+
async run(page, harness) {
|
|
2027
|
+
const root = harness.root ?? DEFAULT_ROOT;
|
|
2028
|
+
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
|
|
2029
|
+
const problems = [];
|
|
2030
|
+
for (const requested of REQUESTED_APPEARANCES) {
|
|
2031
|
+
const at = `[requested ${appearanceLabel(requested)}]`;
|
|
2032
|
+
await harness.open(page, { size, requested });
|
|
2033
|
+
if (harness.host === "embedded") {
|
|
2034
|
+
await inPage(page, hostPageModeProbe, {
|
|
2035
|
+
theme: requested.theme,
|
|
2036
|
+
kontrast: requested.kontrast ?? null
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
await settle(page);
|
|
2040
|
+
const facts = await appearanceFacts(page, root);
|
|
2041
|
+
if (!facts) {
|
|
2042
|
+
problems.push(`${at} no element matches the shell root selector "${root}"`);
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
const verdict = appearanceProblems(facts, harness.host, requested, harness.themes);
|
|
2046
|
+
problems.push(...verdict.problems.map((problem) => `${at} ${problem}`));
|
|
2047
|
+
const surround = await inPage(page, surroundProbe, { root });
|
|
2048
|
+
if (surround) problems.push(...surroundProblems(surround).map((problem) => `${at} ${problem}`));
|
|
2049
|
+
problems.push(...(await paletteColorWalk(page, root)).map((problem) => `${at} ${problem}`));
|
|
2050
|
+
}
|
|
2051
|
+
return finish("appearance", harness.host, problems, []);
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
function shapes(page, root) {
|
|
2055
|
+
return inPage(page, shapesProbe, { root });
|
|
2056
|
+
}
|
|
2057
|
+
async function focusPaletteEntry(page, root, action) {
|
|
2058
|
+
const before = await inPage(page, paletteFocusProbe, { root });
|
|
2059
|
+
if (!before || !before.entries.includes(action)) return false;
|
|
2060
|
+
if (!await focusTarget(page, root, "palette-tab-stop")) return false;
|
|
2061
|
+
await page.keyboard.press("Home");
|
|
2062
|
+
for (let step = 0; step <= before.entries.length; step++) {
|
|
2063
|
+
if ((await inPage(page, paletteFocusProbe, { root }))?.active === action) return true;
|
|
2064
|
+
await page.keyboard.press("ArrowRight");
|
|
2065
|
+
}
|
|
2066
|
+
return false;
|
|
2067
|
+
}
|
|
2068
|
+
function firstCreatingEntry(entries) {
|
|
2069
|
+
return entries.find((entry) => !isToolEntry(entry));
|
|
2070
|
+
}
|
|
2071
|
+
async function enterOnEntry(page, root, entry, before) {
|
|
2072
|
+
if (!await focusPaletteEntry(page, root, entry)) return void 0;
|
|
2073
|
+
await page.keyboard.press("Enter");
|
|
2074
|
+
await settle(page);
|
|
2075
|
+
const known = new Set(before.map((shape) => shape.id));
|
|
2076
|
+
return pollUntil(
|
|
2077
|
+
page,
|
|
2078
|
+
() => shapes(page, root),
|
|
2079
|
+
(after) => after.some((shape) => !known.has(shape.id)),
|
|
2080
|
+
CREATE_WAIT_MS
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
var createCase = {
|
|
2084
|
+
name: "create",
|
|
2085
|
+
async run(page, harness) {
|
|
2086
|
+
const root = harness.root ?? DEFAULT_ROOT;
|
|
2087
|
+
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
|
|
2088
|
+
await harness.open(page, { size, requested: LIGHT });
|
|
2089
|
+
await settle(page);
|
|
2090
|
+
const entry = firstCreatingEntry(await inPage(page, paletteEntriesProbe, { root }));
|
|
2091
|
+
if (!entry) return void 0;
|
|
2092
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
2093
|
+
const free = freeRectangle(await measureChrome(page, root), harness.host);
|
|
2094
|
+
const before = await shapes(page, root);
|
|
2095
|
+
const after = await enterOnEntry(page, root, entry.action, before);
|
|
2096
|
+
if (!after) {
|
|
2097
|
+
fail("create", harness.host, [`create: the palette's roving focus does not reach "${entry.action}"`]);
|
|
2098
|
+
}
|
|
2099
|
+
const canvas = await inPage(page, canvasStateProbe, { root });
|
|
2100
|
+
const problems = createProblems({ entry: entry.action, before, after, canvas, free });
|
|
2101
|
+
await closeByEscape(page);
|
|
2102
|
+
return finish("create", harness.host, problems, []);
|
|
2103
|
+
}
|
|
2104
|
+
};
|
|
2105
|
+
async function menuEntries(page, root) {
|
|
2106
|
+
if (!await openMenuByClick(page, root)) return [];
|
|
2107
|
+
const entries = await inPage(page, menuEntriesProbe, { root }) ?? [];
|
|
2108
|
+
await closeByEscape(page);
|
|
2109
|
+
return entries;
|
|
2110
|
+
}
|
|
2111
|
+
async function reachableShape(page, root, id) {
|
|
2112
|
+
const all = await shapes(page, root);
|
|
2113
|
+
return id === void 0 ? all.find((shape) => shape.hit) : all.find((shape) => shape.id === id);
|
|
2114
|
+
}
|
|
2115
|
+
async function lockedShapeChecks(page, root, shape) {
|
|
2116
|
+
await page.mouse.click(shape.client.x, shape.client.y);
|
|
2117
|
+
await settle(page);
|
|
2118
|
+
const selected = await inPage(page, canvasStateProbe, { root });
|
|
2119
|
+
const before = await reachableShape(page, root, shape.id) ?? shape;
|
|
2120
|
+
await page.mouse.move(before.client.x, before.client.y);
|
|
2121
|
+
await page.mouse.down();
|
|
2122
|
+
for (let step = 1; step <= 4; step++) {
|
|
2123
|
+
await page.mouse.move(before.client.x + LOCKED_DRAG_PX * step / 4, before.client.y);
|
|
2124
|
+
}
|
|
2125
|
+
await page.mouse.up();
|
|
2126
|
+
await settle(page);
|
|
2127
|
+
const dragged = await reachableShape(page, root, shape.id);
|
|
2128
|
+
const target = dragged ?? before;
|
|
2129
|
+
await page.mouse.dblclick(target.client.x, target.client.y);
|
|
2130
|
+
await settle(page);
|
|
2131
|
+
const { textFields } = await inPage(page, canvasStateProbe, { root });
|
|
2132
|
+
await closeByEscape(page);
|
|
2133
|
+
const kept = { Delete: true, Backspace: true };
|
|
2134
|
+
for (const key of ["Delete", "Backspace"]) {
|
|
2135
|
+
const current = await reachableShape(page, root, shape.id);
|
|
2136
|
+
if (!current) break;
|
|
2137
|
+
if (!current.selected) await page.mouse.click(current.client.x, current.client.y);
|
|
2138
|
+
await focusTarget(page, root, "canvas");
|
|
2139
|
+
await page.keyboard.press(key);
|
|
2140
|
+
await settle(page);
|
|
2141
|
+
kept[key] = await reachableShape(page, root, shape.id) !== void 0;
|
|
2142
|
+
}
|
|
2143
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
2144
|
+
return lockedShapeProblems({
|
|
2145
|
+
contextPadEntries: selected.contextPadEntries,
|
|
2146
|
+
dragged: { before, after: dragged },
|
|
2147
|
+
textFields,
|
|
2148
|
+
kept
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
var readOnlyCase = {
|
|
2152
|
+
name: "readOnly",
|
|
2153
|
+
async run(page, harness) {
|
|
2154
|
+
assertHooksConsistent("readOnly", harness, ["invalid"]);
|
|
2155
|
+
const invalid = harness.show?.invalid;
|
|
2156
|
+
if (!invalid) {
|
|
2157
|
+
if (harness.absent?.includes("invalid")) return void 0;
|
|
2158
|
+
return {
|
|
2159
|
+
skipped: [
|
|
2160
|
+
{
|
|
2161
|
+
hook: "invalid",
|
|
2162
|
+
reason: "show.invalid is undefined: the read-only canvas of an invalid file was not verified"
|
|
2163
|
+
}
|
|
2164
|
+
]
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
const root = harness.root ?? DEFAULT_ROOT;
|
|
2168
|
+
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
|
|
2169
|
+
const problems = [];
|
|
2170
|
+
const skipped = [];
|
|
2171
|
+
await harness.open(page, { size, requested: LIGHT });
|
|
2172
|
+
await settle(page);
|
|
2173
|
+
const menuBefore = await menuEntries(page, root);
|
|
2174
|
+
await invalid(page);
|
|
2175
|
+
await page.waitForTimeout(INVALID_STATUS_DELAY_MS);
|
|
2176
|
+
const state = await pollUntil(
|
|
2177
|
+
page,
|
|
2178
|
+
() => inPage(page, readOnlyStateProbe, { root }),
|
|
2179
|
+
(facts) => facts === null || readOnlyStateProblems(facts).length === 0,
|
|
2180
|
+
READ_ONLY_WAIT_MS
|
|
2181
|
+
);
|
|
2182
|
+
if (!state) fail("readOnly", harness.host, [`no element matches the shell root selector "${root}"`]);
|
|
2183
|
+
await settle(page);
|
|
2184
|
+
problems.push(...readOnlyStateProblems(state));
|
|
2185
|
+
const entries = await inPage(page, paletteEntriesProbe, { root });
|
|
2186
|
+
problems.push(...readOnlyPaletteProblems(entries));
|
|
2187
|
+
problems.push(...await paletteColors(page, root));
|
|
2188
|
+
const creating = firstCreatingEntry(entries);
|
|
2189
|
+
if (creating) {
|
|
2190
|
+
const before = await shapes(page, root);
|
|
2191
|
+
const after = await enterOnEntry(page, root, creating.action, before);
|
|
2192
|
+
const known = new Set(before.map((shape2) => shape2.id));
|
|
2193
|
+
const added = (after ?? []).filter((shape2) => !known.has(shape2.id));
|
|
2194
|
+
if (added.length > 0) {
|
|
2195
|
+
problems.push(
|
|
2196
|
+
`read-only: Enter on palette entry "${creating.action}" added ${added.map((shape2) => `"${shape2.id}"`).join(", ")} (guide \xA713.5)`
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
await closeByEscape(page);
|
|
2200
|
+
}
|
|
2201
|
+
problems.push(...readOnlyMenuProblems(menuBefore, await menuEntries(page, root)));
|
|
2202
|
+
const shape = await reachableShape(page, root);
|
|
2203
|
+
if (shape) {
|
|
2204
|
+
problems.push(...await lockedShapeChecks(page, root, shape));
|
|
2205
|
+
} else {
|
|
2206
|
+
skipped.push({
|
|
2207
|
+
hook: "open",
|
|
2208
|
+
reason: "the opened document shows no shape a pointer reaches: context pad, move, delete and label editor of the read-only canvas not verified"
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
return finish("readOnly", harness.host, problems, skipped);
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2214
|
+
var cleanupCase = {
|
|
2215
|
+
name: "cleanup",
|
|
2216
|
+
async run(page, harness) {
|
|
2217
|
+
if (!harness.destroy) {
|
|
2218
|
+
return {
|
|
2219
|
+
skipped: [
|
|
2220
|
+
{
|
|
2221
|
+
hook: "destroy",
|
|
2222
|
+
reason: "harness.destroy is undefined: the cleanup after destroy was not verified"
|
|
2223
|
+
}
|
|
2224
|
+
]
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
const root = harness.root ?? DEFAULT_ROOT;
|
|
2228
|
+
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
|
|
2229
|
+
await harness.open(page, { size, requested: LIGHT });
|
|
2230
|
+
await settle(page);
|
|
2231
|
+
const armed = await inPage(page, armCleanupProbe, { root });
|
|
2232
|
+
if (!armed) fail("cleanup", harness.host, [`no element matches the shell root selector "${root}"`]);
|
|
2233
|
+
const problems = [];
|
|
2234
|
+
const zoomIn = await findTarget(page, root, "zoom-in");
|
|
2235
|
+
if (zoomIn) {
|
|
2236
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
2237
|
+
await page.mouse.move(zoomIn.client.x, zoomIn.client.y);
|
|
2238
|
+
}
|
|
2239
|
+
try {
|
|
2240
|
+
await harness.destroy(page);
|
|
2241
|
+
} catch (error) {
|
|
2242
|
+
problems.push(`destroy threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
2243
|
+
}
|
|
2244
|
+
await page.waitForTimeout(Math.max(INVALID_STATUS_DELAY_MS, TOOLTIP_DELAY_MS) + HOVER_MARGIN_MS);
|
|
2245
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
2246
|
+
await page.setViewportSize({ width: size.width - 40, height: size.height - 40 });
|
|
2247
|
+
await settle(page);
|
|
2248
|
+
await inPage(page, injectProbe, { root, kind: "focus-target" });
|
|
2249
|
+
await inPage(page, clearKeysProbe, null);
|
|
2250
|
+
for (const key of ["Control+Equal", "Control+Minus", "Control+Digit0", "Shift+Digit1"]) {
|
|
2251
|
+
await page.keyboard.press(key);
|
|
2252
|
+
const { keys } = await inPage(page, afterPressProbe, { root });
|
|
2253
|
+
const problem = afterDestroyKeyProblem(key, keys);
|
|
2254
|
+
if (problem) problems.push(problem);
|
|
2255
|
+
}
|
|
2256
|
+
await inPage(page, removeInjectedProbe, null);
|
|
2257
|
+
problems.push(...leftoverProblems(await inPage(page, leftoverProbe, null), harness.host));
|
|
2258
|
+
return finish("cleanup", harness.host, problems, []);
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
function createCases() {
|
|
2262
|
+
return [geometryCase, ariaCase, keysCase, createCase, readOnlyCase, appearanceCase, cleanupCase];
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// src/testing/harness.ts
|
|
2266
|
+
function shellConformanceCases() {
|
|
2267
|
+
return createCases();
|
|
2268
|
+
}
|
|
2269
|
+
function vsCodeStubSearch(requested, extra = {}) {
|
|
2270
|
+
const query = new URLSearchParams({ theme: requested.theme });
|
|
2271
|
+
if (requested.kontrast) query.set("kontrast", requested.kontrast);
|
|
2272
|
+
if (extra.colorTheme) query.set("colorTheme", extra.colorTheme);
|
|
2273
|
+
if (extra.reduceMotion) query.set("reduceMotion", "");
|
|
2274
|
+
return query.toString();
|
|
2275
|
+
}
|
|
2276
|
+
function registerShellConformance(harness, test, options = {}) {
|
|
2277
|
+
const onSkip = options.onSkip ?? ((caseName, skips) => {
|
|
2278
|
+
console.warn(
|
|
2279
|
+
`@miragon/modeler-shell conformance: SKIPPED "${caseName}" (${harness.host} host): ${skips.map((skip) => skip.reason).join("; ")}`
|
|
2280
|
+
);
|
|
2281
|
+
});
|
|
2282
|
+
for (const conformanceCase of shellConformanceCases()) {
|
|
2283
|
+
test(`shell conformance: ${conformanceCase.name}`, async ({ page }) => {
|
|
2284
|
+
const outcome = await conformanceCase.run(page, harness);
|
|
2285
|
+
if (outcome?.skipped && outcome.skipped.length > 0) onSkip(conformanceCase.name, outcome.skipped);
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
export {
|
|
2290
|
+
GEOMETRY_TOLERANCE_PX,
|
|
2291
|
+
expectedChromeBoxes,
|
|
2292
|
+
measureChrome,
|
|
2293
|
+
registerShellConformance,
|
|
2294
|
+
shellConformanceCases,
|
|
2295
|
+
vsCodeStubSearch
|
|
2296
|
+
};
|
|
2297
|
+
//# sourceMappingURL=testing.js.map
|