@miragon/modeler-shell 0.1.0 → 0.2.1
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 +1051 -84
- package/README.md +162 -31
- package/dist/appearance-BBaM_4bD.d.ts +323 -0
- package/dist/appearance-XX3JC3pI.d.cts +323 -0
- package/dist/{chunk-6LQOFACK.js → chunk-A4YGKH4Y.js} +1252 -31
- package/dist/chunk-A4YGKH4Y.js.map +1 -0
- package/dist/{chunk-RJRFR6N3.js → chunk-KYDG5DMX.js} +2 -2
- package/dist/{chunk-5JSZ4VGA.js → chunk-OADW3GPA.js} +240 -6
- package/dist/chunk-OADW3GPA.js.map +1 -0
- package/dist/core.cjs +260 -6
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +81 -6
- package/dist/core.d.ts +81 -6
- package/dist/core.js +43 -3
- package/dist/diagram-js.cjs +34 -2
- package/dist/diagram-js.cjs.map +1 -1
- package/dist/diagram-js.d.cts +10 -3
- package/dist/diagram-js.d.ts +10 -3
- package/dist/diagram-js.js +14 -2
- package/dist/diagram-js.js.map +1 -1
- package/dist/index.cjs +1658 -196
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -9
- package/dist/index.d.ts +18 -9
- package/dist/index.js +44 -4
- package/dist/shell.css +692 -3
- package/dist/testing-vscode-stub.js +2 -2
- package/dist/testing.cjs +2849 -550
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +75 -9
- package/dist/testing.d.ts +75 -9
- package/dist/testing.js +2744 -558
- package/dist/testing.js.map +1 -1
- package/dist/toasts-Bp0REqdq.d.cts +142 -0
- package/dist/toasts-Bp0REqdq.d.ts +142 -0
- package/dist/{types-OuFlqZp0.d.cts → types-CnRrINRs.d.cts} +170 -9
- package/dist/{types-DnZn9CPw.d.ts → types-PuDZczzy.d.ts} +170 -9
- package/dist/vscode-extension.js +2 -2
- package/dist/vscode-webview.cjs +1546 -178
- package/dist/vscode-webview.cjs.map +1 -1
- package/dist/vscode-webview.d.cts +6 -4
- package/dist/vscode-webview.d.ts +6 -4
- package/dist/vscode-webview.js +11 -2
- package/dist/vscode-webview.js.map +1 -1
- package/package.json +2 -2
- package/src/core/feedback/appearance.ts +141 -0
- package/src/core/feedback/problems.ts +23 -0
- package/src/core/feedback/toasts.ts +118 -0
- package/src/core/fit.ts +18 -3
- package/src/core/index.ts +43 -0
- package/src/core/keymap.ts +20 -0
- package/src/core/messages.ts +71 -0
- package/src/core/types.ts +24 -2
- package/src/core/web-chrome/bottom-row.ts +37 -0
- package/src/core/web-chrome/drop.ts +78 -0
- package/src/core/web-chrome/top-row.ts +66 -0
- package/src/diagram-js/service.ts +4 -1
- package/src/diagram-js/viewport.ts +19 -0
- package/src/dom/buttons.css +124 -0
- package/src/dom/feedback/appearance.ts +184 -0
- package/src/dom/feedback/feedback.css +435 -0
- package/src/dom/feedback/problems-popover.ts +287 -0
- package/src/dom/feedback/toasts.ts +431 -0
- package/src/dom/index.ts +13 -0
- package/src/dom/keys.ts +8 -0
- package/src/dom/menu.ts +28 -17
- package/src/dom/mount.ts +290 -20
- package/src/dom/problems.ts +5 -4
- package/src/dom/shell-dom.css +7 -4
- package/src/dom/tooltip.ts +10 -0
- package/src/dom/types.ts +188 -6
- package/src/dom/web/drop-zone.ts +181 -0
- package/src/dom/web/empty-state.ts +144 -0
- package/src/dom/web/legal.ts +75 -0
- package/src/dom/web/menu-groups.ts +69 -0
- package/src/dom/web/primary.ts +124 -0
- package/src/dom/web/web.css +386 -0
- package/src/shell.css +3 -0
- package/src/testing/case-tools.ts +164 -0
- package/src/testing/cases.ts +209 -144
- package/src/testing/checks.ts +51 -15
- package/src/testing/facts.ts +56 -2
- package/src/testing/geometry.ts +206 -26
- package/src/testing/harness.ts +56 -7
- package/src/testing/index.ts +4 -0
- package/src/testing/page-tools.ts +24 -1
- package/src/testing/page.ts +10 -0
- package/src/testing/web-cases.ts +913 -0
- package/src/testing/web-checks.ts +1049 -0
- package/src/testing/web-facts.ts +713 -0
- package/src/vscode/webview/mount.ts +13 -1
- package/dist/chunk-5JSZ4VGA.js.map +0 -1
- package/dist/chunk-6LQOFACK.js.map +0 -1
- package/dist/keymap-CObQE6q6.d.cts +0 -151
- package/dist/keymap-DORzHnK-.d.ts +0 -151
- package/dist/types-B6NfkiUG.d.cts +0 -66
- package/dist/types-B6NfkiUG.d.ts +0 -66
- /package/dist/{chunk-RJRFR6N3.js.map → chunk-KYDG5DMX.js.map} +0 -0
package/dist/testing.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
|
+
APPEARANCE_STORAGE_KEY,
|
|
2
3
|
CHROME_ISLANDS,
|
|
3
4
|
INVALID_STATUS_DELAY_MS,
|
|
5
|
+
LEGAL_LINE_HEIGHT,
|
|
6
|
+
MAX_TOASTS,
|
|
4
7
|
PALETTE_CLEARANCE,
|
|
5
8
|
SHELL,
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
TOAST_DURATION_MS,
|
|
10
|
+
TOOLTIP_DELAY_MS,
|
|
11
|
+
decideTopRow,
|
|
12
|
+
defaultMessages,
|
|
13
|
+
legalLineHeight,
|
|
14
|
+
toastStackBottom,
|
|
15
|
+
toastStackWidth,
|
|
16
|
+
zoomRowRaised
|
|
17
|
+
} from "./chunk-OADW3GPA.js";
|
|
8
18
|
|
|
9
19
|
// src/testing/page-tools.ts
|
|
10
20
|
var installPageTools = (_) => {
|
|
@@ -47,6 +57,7 @@ var installPageTools = (_) => {
|
|
|
47
57
|
errors: [],
|
|
48
58
|
injected: [],
|
|
49
59
|
cleanups: [],
|
|
60
|
+
clicks: 0,
|
|
50
61
|
frame,
|
|
51
62
|
async settle() {
|
|
52
63
|
await frame();
|
|
@@ -135,7 +146,9 @@ var CHROME_TARGET_SELECTORS = {
|
|
|
135
146
|
"zoom-actual": '[data-chrome="zoom"] [data-zoom="actual"]',
|
|
136
147
|
"zoom-in": '[data-chrome="zoom"] [data-zoom="in"]',
|
|
137
148
|
"zoom-fit": '[data-chrome="zoom"] [data-zoom="fit"]',
|
|
138
|
-
canvas: ".djs-container > svg"
|
|
149
|
+
canvas: ".djs-container > svg",
|
|
150
|
+
primary: '[data-chrome="primary"]',
|
|
151
|
+
pill: '[data-chrome="problems"]'
|
|
139
152
|
};
|
|
140
153
|
var targetProbe = ({ root, selector }) => {
|
|
141
154
|
const tools = window.__modelerShellConformance;
|
|
@@ -267,7 +280,10 @@ var hostPageModeProbe = ({ theme, kontrast }) => {
|
|
|
267
280
|
var GEOMETRY_TOLERANCE_PX = 1;
|
|
268
281
|
var DEFAULT_ROOT = ".cd-shell";
|
|
269
282
|
var ZOOM_PANEL_WIDTH = 173;
|
|
270
|
-
var LEGAL_LINE =
|
|
283
|
+
var LEGAL_LINE = LEGAL_LINE_HEIGHT;
|
|
284
|
+
var EMPTY_STATE_MAX_WIDTH = 400;
|
|
285
|
+
var POPOVER_MAX_WIDTH = 320;
|
|
286
|
+
var POPOVER_MAX_HEIGHT = 320;
|
|
271
287
|
var LEGEND_MIN_WIDTH = 184;
|
|
272
288
|
var STATUS_MAX_WIDTH = 560;
|
|
273
289
|
var DROPDOWN_MAX_WIDTH = 320;
|
|
@@ -288,14 +304,19 @@ var measureProbe = ({ root, islands }) => {
|
|
|
288
304
|
}
|
|
289
305
|
boxes[name2] = tools.box(element, rootElement);
|
|
290
306
|
}
|
|
307
|
+
const cardWidths = [...rootElement.querySelectorAll('[data-chrome="toasts"] > [data-status]')].filter((card) => tools.visible(card)).map((card) => card.getBoundingClientRect().width);
|
|
291
308
|
return {
|
|
309
|
+
...cardWidths.length > 0 ? { toastCardWidth: Math.max(...cardWidths) } : {},
|
|
292
310
|
container: tools.container(rootElement),
|
|
293
311
|
paletteRow: rootElement.getAttribute("data-palette") === "second-row" ? "second-row" : "top",
|
|
294
312
|
islands: boxes,
|
|
295
313
|
open: {
|
|
296
314
|
legend: rootElement.getAttribute("data-legend") === "open",
|
|
297
315
|
inspector: rootElement.getAttribute("data-inspector") === "open",
|
|
298
|
-
problems: rootElement.getAttribute("data-problems") === "shown"
|
|
316
|
+
problems: rootElement.getAttribute("data-problems") === "shown",
|
|
317
|
+
emptyState: rootElement.getAttribute("data-empty-state") === "shown",
|
|
318
|
+
primaryCollapsed: rootElement.getAttribute("data-primary") === "collapsed",
|
|
319
|
+
zoomRaised: rootElement.getAttribute("data-zoom-row") === "raised"
|
|
299
320
|
},
|
|
300
321
|
duplicates
|
|
301
322
|
};
|
|
@@ -311,7 +332,8 @@ async function measureChrome(page, root = DEFAULT_ROOT) {
|
|
|
311
332
|
container: result.container,
|
|
312
333
|
paletteRow: result.paletteRow,
|
|
313
334
|
islands: result.islands,
|
|
314
|
-
open: result.open
|
|
335
|
+
open: result.open,
|
|
336
|
+
...result.toastCardWidth !== void 0 ? { toastCardWidth: result.toastCardWidth } : {}
|
|
315
337
|
};
|
|
316
338
|
}
|
|
317
339
|
function isNarrow(container) {
|
|
@@ -323,34 +345,66 @@ function isVeryNarrow(container) {
|
|
|
323
345
|
function sheetOpen(measurement) {
|
|
324
346
|
return measurement.open.inspector && isNarrow(measurement.container);
|
|
325
347
|
}
|
|
348
|
+
function bottomObstacle(box, container) {
|
|
349
|
+
return { left: box.left, right: box.left + box.width, top: container.height - box.top };
|
|
350
|
+
}
|
|
351
|
+
var WEB_ONLY = {
|
|
352
|
+
primary: "the top right stays empty in IDE and embedded hosts (guide \xA75.7)",
|
|
353
|
+
legal: "the legal line belongs to the web app; an IDE or an embedding host carries its own (guide \xA75.2)",
|
|
354
|
+
"empty-state": "IDE and embedded hosts have no start screen; they show the empty-file hint (guide \xA713.5)",
|
|
355
|
+
"drop-zone": "IDE and embedded hosts have no drop zone; their files belong to the host (guide \xA75.2)"
|
|
356
|
+
};
|
|
357
|
+
var WORK_CHROME = [
|
|
358
|
+
"menu",
|
|
359
|
+
"primary",
|
|
360
|
+
"palette",
|
|
361
|
+
"zoom",
|
|
362
|
+
"problems",
|
|
363
|
+
"legend",
|
|
364
|
+
"status",
|
|
365
|
+
"inspector"
|
|
366
|
+
];
|
|
326
367
|
function chromeExpectations(measurement, host) {
|
|
327
368
|
const { container, islands, open, paletteRow } = measurement;
|
|
328
369
|
const { edge, gap, row, free, inspectorWidth } = SHELL;
|
|
329
370
|
const web = host === "web";
|
|
371
|
+
const startScreen2 = web && open.emptyState;
|
|
372
|
+
const raised = web && open.zoomRaised;
|
|
330
373
|
const secondRow = paletteRow === "second-row";
|
|
331
374
|
const sheet = sheetOpen(measurement);
|
|
332
375
|
const sideInspector = open.inspector && !isNarrow(container);
|
|
376
|
+
const legalHeight = legalLineHeight(web ? islands.legal?.height : void 0);
|
|
333
377
|
const boxes = {};
|
|
334
378
|
const limits = {};
|
|
335
379
|
const hidden = {};
|
|
336
|
-
const required = ["menu", "palette"];
|
|
380
|
+
const required = startScreen2 ? ["empty-state"] : ["menu", "palette"];
|
|
381
|
+
if (web) required.push("legal");
|
|
337
382
|
boxes.menu = { left: edge, top: edge, width: row, height: row };
|
|
338
383
|
boxes.palette = { top: secondRow ? free : edge, height: row, centerX: container.width / 2 };
|
|
339
384
|
const menu = islands.menu;
|
|
340
|
-
|
|
385
|
+
const primary = web ? islands.primary : void 0;
|
|
386
|
+
if (secondRow) {
|
|
387
|
+
limits.palette = { maxWidth: container.width - 2 * edge };
|
|
388
|
+
} else {
|
|
389
|
+
limits.palette = {};
|
|
390
|
+
if (menu) limits.palette.minLeft = menu.left + menu.width + PALETTE_CLEARANCE;
|
|
391
|
+
if (primary) limits.palette.minRight = container.width - primary.left + PALETTE_CLEARANCE;
|
|
392
|
+
}
|
|
341
393
|
if (sheet) {
|
|
342
394
|
const reason = "hidden while the inspector is a bottom sheet (guide \xA75.11)";
|
|
343
395
|
hidden.zoom = reason;
|
|
344
396
|
hidden.problems = reason;
|
|
345
397
|
hidden.legend = reason;
|
|
346
398
|
} else {
|
|
347
|
-
required.push("zoom");
|
|
348
|
-
|
|
349
|
-
|
|
399
|
+
if (!startScreen2) required.push("zoom");
|
|
400
|
+
const legalStack = edge + legalHeight + gap;
|
|
401
|
+
boxes.zoom = { right: edge, bottom: raised ? legalStack : edge, width: ZOOM_PANEL_WIDTH, height: row };
|
|
402
|
+
const stackBase = web ? legalStack : edge;
|
|
350
403
|
if (islands.problems) boxes.problems = { left: edge, bottom: stackBase, height: row };
|
|
351
404
|
if (islands.legend) {
|
|
352
405
|
const besidePill = open.problems ? stackBase + row + gap : stackBase;
|
|
353
|
-
const
|
|
406
|
+
const aboveBottomRow = (raised ? legalStack : edge) + row + gap;
|
|
407
|
+
const bottom = isVeryNarrow(container) ? Math.max(besidePill, aboveBottomRow) : besidePill;
|
|
354
408
|
boxes.legend = { left: edge, bottom };
|
|
355
409
|
limits.legend = { minWidth: LEGEND_MIN_WIDTH };
|
|
356
410
|
}
|
|
@@ -373,9 +427,52 @@ function chromeExpectations(measurement, host) {
|
|
|
373
427
|
};
|
|
374
428
|
}
|
|
375
429
|
if (web) {
|
|
376
|
-
if (
|
|
430
|
+
if (primary) {
|
|
431
|
+
boxes.primary = { top: edge, right: edge, height: row };
|
|
432
|
+
if (open.primaryCollapsed) boxes.primary.width = row;
|
|
433
|
+
}
|
|
434
|
+
boxes.legal = { left: edge, bottom: edge, height: legalHeight };
|
|
435
|
+
limits.legal = { maxWidth: container.width - 2 * edge };
|
|
436
|
+
if (islands["empty-state"]) {
|
|
437
|
+
boxes["empty-state"] = {
|
|
438
|
+
centerX: container.width / 2,
|
|
439
|
+
centerY: container.height / 2,
|
|
440
|
+
width: Math.min(EMPTY_STATE_MAX_WIDTH, container.width - 2 * edge)
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (islands["drop-zone"]) boxes["drop-zone"] = { left: edge, top: edge, right: edge, bottom: edge };
|
|
444
|
+
} else {
|
|
445
|
+
Object.assign(hidden, WEB_ONLY);
|
|
446
|
+
}
|
|
447
|
+
if (host === "ide") {
|
|
448
|
+
hidden.toasts = "the IDE host shows no toasts; the extension shows the host's notification (guide \xA713.5)";
|
|
449
|
+
hidden["problems-popover"] = "the IDE host opens its Problems view, never a list of its own (guide \xA75.10)";
|
|
377
450
|
} else {
|
|
378
|
-
|
|
451
|
+
if (islands.toasts) {
|
|
452
|
+
const obstacles = ["zoom", "legal", "problems"].flatMap((name2) => {
|
|
453
|
+
const box = islands[name2];
|
|
454
|
+
return box ? [bottomObstacle(box, container)] : [];
|
|
455
|
+
});
|
|
456
|
+
boxes.toasts = {
|
|
457
|
+
centerX: container.width / 2,
|
|
458
|
+
width: toastStackWidth(container.width),
|
|
459
|
+
bottom: toastStackBottom({
|
|
460
|
+
containerWidth: container.width,
|
|
461
|
+
obstacles,
|
|
462
|
+
cardWidth: measurement.toastCardWidth
|
|
463
|
+
})
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
const pill = islands.problems;
|
|
467
|
+
if (pill) {
|
|
468
|
+
boxes["problems-popover"] = { left: edge, bottom: container.height - pill.top + gap };
|
|
469
|
+
limits["problems-popover"] = {
|
|
470
|
+
maxWidth: Math.min(POPOVER_MAX_WIDTH, container.width - 2 * edge),
|
|
471
|
+
maxHeight: POPOVER_MAX_HEIGHT
|
|
472
|
+
};
|
|
473
|
+
} else {
|
|
474
|
+
hidden["problems-popover"] = "the problems list opens only above a visible pill (guide \xA75.10)";
|
|
475
|
+
}
|
|
379
476
|
}
|
|
380
477
|
return { boxes, limits, required, hidden };
|
|
381
478
|
}
|
|
@@ -391,7 +488,8 @@ function sides(box, container) {
|
|
|
391
488
|
bottom: container.height - box.top - box.height,
|
|
392
489
|
width: box.width,
|
|
393
490
|
height: box.height,
|
|
394
|
-
centerX: box.left + box.width / 2
|
|
491
|
+
centerX: box.left + box.width / 2,
|
|
492
|
+
centerY: box.top + box.height / 2
|
|
395
493
|
};
|
|
396
494
|
}
|
|
397
495
|
function boxMismatches(name2, box, expected, container, tolerance = GEOMETRY_TOLERANCE_PX) {
|
|
@@ -452,12 +550,17 @@ function geometryProblems(measurement, host, tolerance = GEOMETRY_TOLERANCE_PX)
|
|
|
452
550
|
}
|
|
453
551
|
const palette = islands.palette;
|
|
454
552
|
const menu = islands.menu;
|
|
553
|
+
const primary = host === "web" ? islands.primary : void 0;
|
|
455
554
|
if (paletteRow === "second-row" && !isNarrow(container) && palette && menu) {
|
|
456
555
|
const clipped = palette.width >= container.width - 2 * SHELL.edge - tolerance;
|
|
457
556
|
const topRowLeft = (container.width - palette.width) / 2;
|
|
458
|
-
|
|
557
|
+
const toMenu = topRowLeft - menu.left - menu.width;
|
|
558
|
+
const toPrimary = primary ? container.width - SHELL.edge - SHELL.row - (topRowLeft + palette.width) : Number.POSITIVE_INFINITY;
|
|
559
|
+
const fits = toMenu >= PALETTE_CLEARANCE + tolerance && toPrimary >= PALETTE_CLEARANCE + tolerance;
|
|
560
|
+
if (!clipped && fits) {
|
|
561
|
+
const kept = primary ? `${round(toMenu)}px to the menu trigger and ${round(toPrimary)}px to the primary action's icon` : `${round(toMenu)}px to the menu trigger`;
|
|
459
562
|
problems.push(
|
|
460
|
-
`palette: in the second row although the top row keeps ${
|
|
563
|
+
`palette: in the second row although the top row keeps ${kept} (at least ${PALETTE_CLEARANCE}px suffice, guide \xA75.15)`
|
|
461
564
|
);
|
|
462
565
|
}
|
|
463
566
|
}
|
|
@@ -468,9 +571,43 @@ function geometryProblems(measurement, host, tolerance = GEOMETRY_TOLERANCE_PX)
|
|
|
468
571
|
}
|
|
469
572
|
if (!flag && shown) problems.push(`${island}: visible, but the root lacks ${attribute2}`);
|
|
470
573
|
};
|
|
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"',
|
|
574
|
+
state("problems", open.problems, 'data-problems="shown"', sheet || open.emptyState);
|
|
575
|
+
state("legend", open.legend, 'data-legend="open"', sheet || open.emptyState);
|
|
576
|
+
state("inspector", open.inspector, 'data-inspector="open"', open.emptyState);
|
|
577
|
+
state("empty-state", open.emptyState, 'data-empty-state="shown"', false);
|
|
578
|
+
problems.push(...webRowProblems(measurement, host, tolerance));
|
|
579
|
+
return problems;
|
|
580
|
+
}
|
|
581
|
+
function webRowProblems(measurement, host, tolerance = GEOMETRY_TOLERANCE_PX) {
|
|
582
|
+
const { container, islands, open } = measurement;
|
|
583
|
+
const problems = [];
|
|
584
|
+
if (host !== "web") {
|
|
585
|
+
if (open.primaryCollapsed)
|
|
586
|
+
problems.push('primary: the root carries data-primary="collapsed" outside the web app');
|
|
587
|
+
if (open.zoomRaised) problems.push('zoom: the root carries data-zoom-row="raised" outside the web app');
|
|
588
|
+
return problems;
|
|
589
|
+
}
|
|
590
|
+
if (open.primaryCollapsed && !islands.primary && !open.emptyState) {
|
|
591
|
+
problems.push('primary: the root carries data-primary="collapsed", but no primary action is visible');
|
|
592
|
+
}
|
|
593
|
+
const legal = islands.legal;
|
|
594
|
+
const zoom = islands.zoom;
|
|
595
|
+
if (legal && zoom) {
|
|
596
|
+
const legalRight = legal.left + legal.width;
|
|
597
|
+
const gap = container.width - SHELL.edge - zoom.width - legalRight;
|
|
598
|
+
const raised = zoomRowRaised({ containerWidth: container.width, legalRight, zoomWidth: zoom.width });
|
|
599
|
+
const borderline = Math.abs(gap - SHELL.gap) <= tolerance;
|
|
600
|
+
if (raised && !open.zoomRaised && !borderline) {
|
|
601
|
+
problems.push(
|
|
602
|
+
`zoom: ${round(gap)}px between the legal line and the zoom panel, but the root lacks data-zoom-row="raised" (below ${SHELL.gap}px the zoom panel stands above the legal line)`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
if (!raised && open.zoomRaised && !borderline) {
|
|
606
|
+
problems.push(
|
|
607
|
+
`zoom: the root carries data-zoom-row="raised" although the zoom panel keeps ${round(gap)}px to the legal line (at least ${SHELL.gap}px)`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
474
611
|
return problems;
|
|
475
612
|
}
|
|
476
613
|
function expectedFitInsets(measurement, host) {
|
|
@@ -478,7 +615,7 @@ function expectedFitInsets(measurement, host) {
|
|
|
478
615
|
const insets = {
|
|
479
616
|
top: paletteRow === "second-row" ? SHELL.fitInsetTopSecondRow : SHELL.fitInsetTop,
|
|
480
617
|
right: SHELL.fitInsetSide,
|
|
481
|
-
bottom: host === "web" && open.problems ? SHELL.fitInsetBottomWeb : SHELL.fitInsetBottom,
|
|
618
|
+
bottom: host === "web" && (open.problems || open.zoomRaised) ? SHELL.fitInsetBottomWeb + legalLineHeight(islands.legal?.height) - LEGAL_LINE : SHELL.fitInsetBottom,
|
|
482
619
|
left: SHELL.fitInsetSide
|
|
483
620
|
};
|
|
484
621
|
if (sheetOpen(measurement)) {
|
|
@@ -522,10 +659,10 @@ function dropdownProblems(box, container, tolerance = GEOMETRY_TOLERANCE_PX) {
|
|
|
522
659
|
)
|
|
523
660
|
];
|
|
524
661
|
}
|
|
525
|
-
function tooltipProblems(tooltip, anchor, row, container, tolerance = GEOMETRY_TOLERANCE_PX) {
|
|
662
|
+
function tooltipProblems(tooltip, anchor, row, container, tolerance = GEOMETRY_TOLERANCE_PX, bottomRow = SHELL.edge) {
|
|
526
663
|
const { edge, gap, row: rowHeight, free } = SHELL;
|
|
527
664
|
const clamp = (centred) => Math.max(edge, Math.min(centred, container.width - edge - tooltip.width));
|
|
528
|
-
const expectedAt = (left) => row === "bottom" ? { left, bottom:
|
|
665
|
+
const expectedAt = (left) => row === "bottom" ? { left, bottom: bottomRow + rowHeight + gap } : { left, top: (row === "top" ? edge : free) + rowHeight + gap };
|
|
529
666
|
let mismatches = boxMismatches(
|
|
530
667
|
"tooltip",
|
|
531
668
|
tooltip,
|
|
@@ -563,10 +700,13 @@ var DOM_ORDER = [
|
|
|
563
700
|
"canvas",
|
|
564
701
|
"status",
|
|
565
702
|
"hint",
|
|
703
|
+
"empty-state",
|
|
566
704
|
"inspector",
|
|
567
705
|
"problems",
|
|
568
706
|
"legend",
|
|
569
|
-
"
|
|
707
|
+
"legal",
|
|
708
|
+
"zoom",
|
|
709
|
+
"toasts"
|
|
570
710
|
];
|
|
571
711
|
var ariaProbe = ({ root }) => {
|
|
572
712
|
const tools = window.__modelerShellConformance;
|
|
@@ -587,6 +727,8 @@ var ariaProbe = ({ root }) => {
|
|
|
587
727
|
}
|
|
588
728
|
const legend = find('[data-chrome="legend"]');
|
|
589
729
|
const labelledBy = legend?.getAttribute("aria-labelledby");
|
|
730
|
+
const legal = find('[data-chrome="legal"]');
|
|
731
|
+
const pillControls = find('[data-chrome="problems"]')?.getAttribute("aria-controls");
|
|
590
732
|
const parts = [
|
|
591
733
|
["menu", trigger],
|
|
592
734
|
["primary", find('[data-chrome="primary"]')],
|
|
@@ -596,13 +738,17 @@ var ariaProbe = ({ root }) => {
|
|
|
596
738
|
["hint", find(".cd-shell-hint")],
|
|
597
739
|
["inspector", find('[data-chrome="inspector"]')],
|
|
598
740
|
["problems", find('[data-chrome="problems"]')],
|
|
741
|
+
["empty-state", find('[data-chrome="empty-state"]')],
|
|
599
742
|
["legend", legend],
|
|
600
|
-
["
|
|
743
|
+
["legal", find('[data-chrome="legal"]')],
|
|
744
|
+
["zoom", zoom],
|
|
745
|
+
["toasts", find('[data-chrome="toasts"]')]
|
|
601
746
|
];
|
|
602
747
|
const present = parts.filter((part) => part[1] !== null);
|
|
603
748
|
present.sort(([, a], [, b]) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
|
|
604
749
|
const titledSelector = [
|
|
605
750
|
'[data-chrome="menu"][title]',
|
|
751
|
+
'[data-chrome="primary"][title]',
|
|
606
752
|
'[data-chrome="palette"] [data-action][title]',
|
|
607
753
|
'[data-chrome="zoom"] [title]',
|
|
608
754
|
'[data-chrome="problems"][title]'
|
|
@@ -630,7 +776,18 @@ var ariaProbe = ({ root }) => {
|
|
|
630
776
|
legend: factsOf(legend),
|
|
631
777
|
legendTitle: labelledBy ? labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim() ?? "").join(" ").trim() : null,
|
|
632
778
|
legendClose: factsOf(legend?.querySelector("button")),
|
|
633
|
-
inspector: factsOf(find('[data-chrome="inspector"]'))
|
|
779
|
+
inspector: factsOf(find('[data-chrome="inspector"]')),
|
|
780
|
+
primary: factsOf(find('[data-chrome="primary"]')),
|
|
781
|
+
primaryCollapsed: rootElement.getAttribute("data-primary") === "collapsed",
|
|
782
|
+
legal: factsOf(legal),
|
|
783
|
+
legalLinks: [...legal?.querySelectorAll("a") ?? []].map((link) => ({
|
|
784
|
+
text: (link.textContent ?? "").replace(/\s+/g, " ").trim(),
|
|
785
|
+
href: link.getAttribute("href") ?? "",
|
|
786
|
+
lang: link.getAttribute("lang"),
|
|
787
|
+
target: link.getAttribute("target"),
|
|
788
|
+
rel: link.getAttribute("rel")
|
|
789
|
+
})),
|
|
790
|
+
problemsControlled: factsOf(pillControls ? document.getElementById(pillControls) : null)
|
|
634
791
|
};
|
|
635
792
|
};
|
|
636
793
|
var menuProbe = ({ root }) => {
|
|
@@ -899,617 +1056,2583 @@ var menuEntriesProbe = ({ root }) => {
|
|
|
899
1056
|
role,
|
|
900
1057
|
menuId: element.closest("[data-menu-id]")?.getAttribute("data-menu-id") ?? null,
|
|
901
1058
|
disabled: element.getAttribute("aria-disabled") === "true",
|
|
902
|
-
group: element.closest("[data-menu-group]")?.getAttribute("data-menu-group") ?? null
|
|
1059
|
+
group: element.closest("[data-menu-group]")?.getAttribute("data-menu-group") ?? null,
|
|
1060
|
+
value: element.getAttribute("data-menu-value"),
|
|
1061
|
+
checked: element.getAttribute("aria-checked"),
|
|
1062
|
+
icon: element.querySelector(".cd-shell-menu-item-icon svg") !== null
|
|
903
1063
|
});
|
|
904
1064
|
}
|
|
905
1065
|
return entries;
|
|
906
1066
|
};
|
|
907
1067
|
|
|
908
|
-
// src/testing/checks.ts
|
|
1068
|
+
// src/testing/web-checks.ts
|
|
1069
|
+
var round2 = (value) => Math.round(value * 10) / 10;
|
|
909
1070
|
var normalize = (text) => text.replace(/\s+/g, " ").trim();
|
|
910
|
-
var
|
|
911
|
-
var
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
out: `${modifier}+-`,
|
|
917
|
-
actual: `${modifier}+0`,
|
|
918
|
-
fit: "Shift+1"
|
|
919
|
-
};
|
|
1071
|
+
var MIRAGON_HOME = "https://www.miragon.io/";
|
|
1072
|
+
var MIRAGON_IMPRESSUM = "https://www.miragon.io/impressum/";
|
|
1073
|
+
var MIRAGON_DATENSCHUTZ = "https://www.miragon.io/datenschutz/";
|
|
1074
|
+
var GERMAN_LEGAL_LINKS = ["Impressum", "Datenschutz"];
|
|
1075
|
+
function speaksEnglish(lang) {
|
|
1076
|
+
return /^en(-|$)/i.test(lang.trim());
|
|
920
1077
|
}
|
|
921
|
-
function
|
|
1078
|
+
function primaryAriaProblems(facts, host) {
|
|
1079
|
+
const primary = facts.primary;
|
|
1080
|
+
if (!primary || host !== "web") return [];
|
|
922
1081
|
const problems = [];
|
|
923
|
-
if (
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1082
|
+
if (primary.tag !== "button") problems.push(`primary: a <${primary.tag}>, expected a <button>`);
|
|
1083
|
+
const label = normalize(primary.attributes["aria-label"] ?? "");
|
|
1084
|
+
if (!label) {
|
|
1085
|
+
problems.push(
|
|
1086
|
+
"primary: no aria-label; it names the action also while the label is collapsed (guide \xA75.15)"
|
|
1087
|
+
);
|
|
1088
|
+
} else if (!facts.primaryCollapsed && primary.text && !label.includes(primary.text)) {
|
|
1089
|
+
problems.push(
|
|
1090
|
+
`primary: the aria-label "${label}" does not contain the visible label "${primary.text}" (WCAG 2.5.3)`
|
|
1091
|
+
);
|
|
928
1092
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1093
|
+
if (primary.disabled) {
|
|
1094
|
+
problems.push(
|
|
1095
|
+
"primary: natively disabled; a disabled primary action keeps the focus with aria-disabled (guide \xA78)"
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
return problems;
|
|
1099
|
+
}
|
|
1100
|
+
function legalAriaProblems(facts, host) {
|
|
1101
|
+
if (host !== "web") return [];
|
|
1102
|
+
const legal = facts.legal;
|
|
1103
|
+
if (!legal) return ['legal: no [data-chrome="legal"] in the web app (guide \xA75.10)'];
|
|
1104
|
+
const problems = [];
|
|
1105
|
+
if (legal.tag !== "nav") problems.push(`legal: the legal line is a <${legal.tag}>, expected a <nav>`);
|
|
1106
|
+
if (!normalize(legal.attributes["aria-label"] ?? "")) problems.push("legal: the <nav> has no aria-label");
|
|
1107
|
+
for (const text of GERMAN_LEGAL_LINKS) {
|
|
1108
|
+
const link = facts.legalLinks.find((candidate) => candidate.text === text);
|
|
1109
|
+
if (!link) {
|
|
1110
|
+
problems.push(`legal: no "${text}" link (guide \xA75.10)`);
|
|
1111
|
+
} else if (link.lang !== "de") {
|
|
1112
|
+
problems.push(
|
|
1113
|
+
`legal: "${text}" has ${link.lang === null ? "no lang" : `lang="${link.lang}"`}, expected lang="de" (guide \xA72: Impressum and Datenschutz stay German)`
|
|
1114
|
+
);
|
|
938
1115
|
}
|
|
939
|
-
|
|
1116
|
+
}
|
|
1117
|
+
for (const link of facts.legalLinks) {
|
|
1118
|
+
if (link.target !== "_blank") continue;
|
|
1119
|
+
const rel = (link.rel ?? "").split(/\s+/);
|
|
1120
|
+
if (!rel.includes("noopener") || !rel.includes("noreferrer")) {
|
|
1121
|
+
problems.push(`legal: the link "${link.text}" opens a new tab without rel="noopener noreferrer"`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return problems;
|
|
1125
|
+
}
|
|
1126
|
+
function pillPopupAriaProblems(facts, host) {
|
|
1127
|
+
const pill = facts.problems;
|
|
1128
|
+
if (!pill || host === "ide") return [];
|
|
1129
|
+
const haspopup = pill.attributes["aria-haspopup"];
|
|
1130
|
+
if (host === "embedded" && haspopup === void 0) return [];
|
|
1131
|
+
const problems = [];
|
|
1132
|
+
if (haspopup !== "dialog") {
|
|
1133
|
+
problems.push(
|
|
1134
|
+
`problems: the pill has aria-haspopup "${haspopup ?? ""}", expected "dialog" (it opens the problems list)`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
if (pill.attributes["aria-expanded"] !== "false") {
|
|
1138
|
+
problems.push(
|
|
1139
|
+
`problems: the pill has aria-expanded "${pill.attributes["aria-expanded"] ?? ""}" while the list is closed, expected "false"`
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
const controls = pill.attributes["aria-controls"];
|
|
1143
|
+
if (!controls) {
|
|
1144
|
+
problems.push("problems: the pill has no aria-controls");
|
|
1145
|
+
} else if (!facts.problemsControlled) {
|
|
1146
|
+
problems.push(`problems: aria-controls="${controls}" names no element`);
|
|
1147
|
+
} else {
|
|
1148
|
+
const role = facts.problemsControlled.attributes.role;
|
|
1149
|
+
if (role !== "dialog") {
|
|
940
1150
|
problems.push(
|
|
941
|
-
`
|
|
1151
|
+
`problems: aria-controls names a ${facts.problemsControlled.description} with role "${role ?? ""}", expected "dialog"`
|
|
942
1152
|
);
|
|
943
1153
|
}
|
|
944
|
-
if (
|
|
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") {
|
|
1154
|
+
if (facts.problemsControlled.attributes["aria-modal"] === "true") {
|
|
949
1155
|
problems.push(
|
|
950
|
-
|
|
1156
|
+
'problems: the problems list is aria-modal="true"; it is a non-modal popover (guide \xA75.10)'
|
|
951
1157
|
);
|
|
952
1158
|
}
|
|
953
1159
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1160
|
+
return problems;
|
|
1161
|
+
}
|
|
1162
|
+
var MENU_GROUP_ORDER = ["file", "model", "export", "appearance", "help"];
|
|
1163
|
+
var FILE_ENTRIES = [
|
|
1164
|
+
{ id: "new-diagram", icon: true },
|
|
1165
|
+
{ id: "open", icon: true },
|
|
1166
|
+
{ id: "show-example", icon: false },
|
|
1167
|
+
{ id: "import", icon: true }
|
|
1168
|
+
];
|
|
1169
|
+
var APPEARANCE_VALUES = ["system", "light", "dark"];
|
|
1170
|
+
function menuGroupProblems(entries, host, themes, choice = "system") {
|
|
1171
|
+
const problems = [];
|
|
1172
|
+
const file = entries.filter((entry) => entry.group === "file");
|
|
1173
|
+
const appearance = entries.filter((entry) => entry.group === "appearance");
|
|
1174
|
+
if (host !== "web") {
|
|
1175
|
+
const guide = host === "ide" ? "its file actions are native (guide \xA713.5)" : "the host owns file and mode (guide \xA75.2)";
|
|
1176
|
+
if (file.length > 0) problems.push(`menu: the ${host} host shows a File group; ${guide}`);
|
|
1177
|
+
if (appearance.length > 0) problems.push(`menu: the ${host} host shows an Appearance group; ${guide}`);
|
|
1178
|
+
return problems;
|
|
1179
|
+
}
|
|
1180
|
+
const sequence = [];
|
|
1181
|
+
for (const entry of entries) {
|
|
1182
|
+
if (entry.group && sequence.at(-1) !== entry.group) sequence.push(entry.group);
|
|
1183
|
+
}
|
|
1184
|
+
const rank = (group) => MENU_GROUP_ORDER.indexOf(group);
|
|
1185
|
+
const ordered = sequence.every((group, index) => index === 0 || rank(sequence[index - 1]) < rank(group));
|
|
1186
|
+
if (!ordered) {
|
|
1187
|
+
problems.push(
|
|
1188
|
+
`menu: the groups stand in the order ${sequence.join(", ")}; expected File, Model, Export, Appearance, Help (guide \xA75.4)`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
const known = FILE_ENTRIES.map((entry) => entry.id);
|
|
1192
|
+
for (const entry of file) {
|
|
1193
|
+
const spec = FILE_ENTRIES.find((candidate) => candidate.id === entry.menuId);
|
|
1194
|
+
if (!spec) {
|
|
1195
|
+
problems.push(
|
|
1196
|
+
`menu: File entry "${entry.name}" has data-menu-id "${entry.menuId ?? ""}"; the shell builds the File group from ${known.join(", ")}`
|
|
1197
|
+
);
|
|
1198
|
+
continue;
|
|
973
1199
|
}
|
|
974
|
-
|
|
975
|
-
|
|
1200
|
+
if (entry.role !== "menuitem")
|
|
1201
|
+
problems.push(`menu: File entry "${entry.name}" is a ${entry.role}, expected a menuitem`);
|
|
1202
|
+
if (spec.icon && !entry.icon) problems.push(`menu: File entry "${entry.name}" has no icon (guide \xA75.4)`);
|
|
1203
|
+
if (!spec.icon && entry.icon) {
|
|
976
1204
|
problems.push(
|
|
977
|
-
`
|
|
1205
|
+
`menu: File entry "${entry.name}" carries an icon; the guide gives ${entry.menuId} none (guide \xA75.14)`
|
|
978
1206
|
);
|
|
979
1207
|
}
|
|
980
|
-
|
|
981
|
-
|
|
1208
|
+
}
|
|
1209
|
+
const fileIds = file.map((entry) => entry.menuId ?? "");
|
|
1210
|
+
const expectedIds = known.filter((id) => fileIds.includes(id));
|
|
1211
|
+
if (fileIds.join() !== expectedIds.join() && fileIds.every((id) => known.includes(id))) {
|
|
1212
|
+
problems.push(
|
|
1213
|
+
`menu: the File group reads ${fileIds.join(", ")}; expected the order ${expectedIds.join(", ")} (guide \xA75.4)`
|
|
982
1214
|
);
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1215
|
+
}
|
|
1216
|
+
if (themes.includes("dark")) {
|
|
1217
|
+
if (appearance.length === 0) {
|
|
1218
|
+
problems.push("menu: a dark-capable modeler shows no Appearance group (guide \xA75.4, \xA713.3)");
|
|
1219
|
+
} else {
|
|
1220
|
+
const values = appearance.map((entry) => entry.value ?? "");
|
|
1221
|
+
if (values.join() !== APPEARANCE_VALUES.join()) {
|
|
1222
|
+
problems.push(
|
|
1223
|
+
`menu: the Appearance options are ${values.join(", ")}; expected system, light, dark (guide \xA713.3)`
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
for (const entry of appearance) {
|
|
1227
|
+
if (entry.role !== "menuitemradio" || entry.menuId !== "appearance") {
|
|
1228
|
+
problems.push(
|
|
1229
|
+
`menu: Appearance option "${entry.name}" is a ${entry.role} in "${entry.menuId ?? ""}"; expected a menuitemradio of the radio group "appearance"`
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
987
1232
|
}
|
|
1233
|
+
problems.push(...appearanceChoiceProblems(entries, choice));
|
|
988
1234
|
}
|
|
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
1235
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1236
|
+
return problems;
|
|
1237
|
+
}
|
|
1238
|
+
function appearanceChoiceProblems(entries, choice) {
|
|
1239
|
+
const checked = entries.filter((entry) => entry.group === "appearance" && entry.checked === "true").map((entry) => entry.value ?? "");
|
|
1240
|
+
return checked.length === 1 && checked[0] === choice ? [] : [
|
|
1241
|
+
`menu: the Appearance group checks ${checked.length > 0 ? checked.join(", ") : "nothing"}; expected "${choice}" (aria-checked, guide \xA712)`
|
|
1242
|
+
];
|
|
1243
|
+
}
|
|
1244
|
+
function lightOnlyMenuProblems(entries, themes) {
|
|
1245
|
+
if (themes.includes("dark")) return [];
|
|
1246
|
+
return entries.some((entry) => entry.group === "appearance" || entry.menuId === "appearance") ? ["menu: a light-only modeler shows the Appearance group; it comes with dark (guide \xA713.3)"] : [];
|
|
1247
|
+
}
|
|
1248
|
+
function labelledWidth(samples) {
|
|
1249
|
+
const widths = samples.flatMap(
|
|
1250
|
+
(sample) => sample.primary && !sample.primary.collapsed && sample.primary.label ? [sample.primary.box.width] : []
|
|
1251
|
+
);
|
|
1252
|
+
return widths.length > 0 ? Math.max(...widths) : void 0;
|
|
1253
|
+
}
|
|
1254
|
+
function topRowProblems(samples, withLabel, name2) {
|
|
1255
|
+
const problems = [];
|
|
1256
|
+
for (const sample of samples) {
|
|
1257
|
+
const { container, paletteWidth, menuRight, primary } = sample;
|
|
1258
|
+
if (!primary || paletteWidth === null || menuRight === null) continue;
|
|
1259
|
+
const decide = (palette, labelled) => decideTopRow({
|
|
1260
|
+
containerWidth: container.width,
|
|
1261
|
+
paletteWidth: palette,
|
|
1262
|
+
menuRight,
|
|
1263
|
+
primary: { withLabel: labelled, iconOnly: SHELL.row }
|
|
1264
|
+
});
|
|
1265
|
+
const expected = decide(paletteWidth, withLabel);
|
|
1266
|
+
const low = decide(paletteWidth - 1, withLabel - 1);
|
|
1267
|
+
const high = decide(paletteWidth + 1, withLabel + 1);
|
|
1268
|
+
const at = `[${round2(container.width)}px wide]`;
|
|
1269
|
+
if (low.paletteRow === high.paletteRow && low.primaryLabel === high.primaryLabel && expected.paletteRow === low.paletteRow && expected.primaryLabel === low.primaryLabel) {
|
|
1270
|
+
const collapsed = expected.primaryLabel === "collapsed";
|
|
1271
|
+
if (collapsed && !primary.collapsed) {
|
|
1008
1272
|
problems.push(
|
|
1009
|
-
|
|
1273
|
+
expected.paletteRow === "top" ? `${at} primary: shows its label, but the ${round2(paletteWidth)}px palette fits the top row only beside the icon; the label collapses first (guide \xA75.15)` : `${at} primary: shows its label, but it no longer fits next to the menu trigger (data-primary="collapsed", guide \xA75.15)`
|
|
1010
1274
|
);
|
|
1011
1275
|
}
|
|
1012
|
-
if (
|
|
1276
|
+
if (!collapsed && primary.collapsed) {
|
|
1277
|
+
problems.push(`${at} primary: collapsed although its ${round2(withLabel)}px label fits (guide \xA75.15)`);
|
|
1278
|
+
}
|
|
1279
|
+
if (expected.paletteRow !== sample.paletteRow) {
|
|
1013
1280
|
problems.push(
|
|
1014
|
-
|
|
1281
|
+
expected.paletteRow === "top" ? `${at} palette: in the second row although it fits the top row beside the collapsed primary action; the label collapses first (guide \xA75.15)` : `${at} palette: in the top row although it keeps less than 16px to the primary action even icon only (guide \xA75.15)`
|
|
1015
1282
|
);
|
|
1016
1283
|
}
|
|
1017
1284
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
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)) {
|
|
1285
|
+
if (primary.collapsed) {
|
|
1286
|
+
if (Math.abs(primary.box.width - SHELL.row) > GEOMETRY_TOLERANCE_PX + 1e-6) {
|
|
1024
1287
|
problems.push(
|
|
1025
|
-
|
|
1288
|
+
`${at} primary: ${round2(primary.box.width)}px wide while collapsed, expected ${SHELL.row}px`
|
|
1026
1289
|
);
|
|
1027
1290
|
}
|
|
1028
|
-
if (
|
|
1029
|
-
problems.push(
|
|
1291
|
+
if (primary.label)
|
|
1292
|
+
problems.push(`${at} primary: collapsed, but its label "${primary.label}" still shows`);
|
|
1293
|
+
if (primary.ariaLabel !== name2) {
|
|
1294
|
+
problems.push(
|
|
1295
|
+
`${at} primary: the aria-label is "${primary.ariaLabel ?? ""}" while collapsed; it keeps "${name2}" (guide \xA75.15)`
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
} else if (!primary.label) {
|
|
1299
|
+
problems.push(`${at} primary: shows no label although the root lacks data-primary="collapsed"`);
|
|
1300
|
+
}
|
|
1301
|
+
if (primary.describedBy) {
|
|
1302
|
+
problems.push(
|
|
1303
|
+
`${at} primary: aria-describedby="${primary.describedBy}"; its tooltip repeats the name (API.md, Primary action)`
|
|
1304
|
+
);
|
|
1030
1305
|
}
|
|
1031
1306
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
);
|
|
1307
|
+
return problems;
|
|
1308
|
+
}
|
|
1309
|
+
function primaryTooltipProblems(collapsed, tooltip, name2, width) {
|
|
1310
|
+
const at = `[${round2(width)}px wide]`;
|
|
1311
|
+
if (collapsed && tooltip === null) {
|
|
1312
|
+
return [`${at} primary: no chrome tooltip while collapsed; it shows the label "${name2}" (guide \xA75.15)`];
|
|
1038
1313
|
}
|
|
1039
|
-
if (
|
|
1040
|
-
|
|
1041
|
-
|
|
1314
|
+
if (collapsed && tooltip !== null && !normalize(tooltip).startsWith(name2)) {
|
|
1315
|
+
return [`${at} primary: the tooltip while collapsed reads "${normalize(tooltip)}", expected "${name2}"`];
|
|
1316
|
+
}
|
|
1317
|
+
if (!collapsed && tooltip !== null) {
|
|
1318
|
+
return [
|
|
1319
|
+
`${at} primary: a tooltip "${normalize(tooltip)}" while the label shows; only the collapsed action has one`
|
|
1320
|
+
];
|
|
1321
|
+
}
|
|
1322
|
+
return [];
|
|
1323
|
+
}
|
|
1324
|
+
function primaryActivationProblems(clicks) {
|
|
1325
|
+
const problems = [];
|
|
1326
|
+
if (clicks.pointer !== 1)
|
|
1327
|
+
problems.push(`primary: a pointer click reached it ${clicks.pointer} times, expected once`);
|
|
1328
|
+
if (clicks.enter !== 1) {
|
|
1042
1329
|
problems.push(
|
|
1043
|
-
|
|
1330
|
+
`primary: Enter on the focused primary action clicked it ${clicks.enter} times, expected once (keyboard activation)`
|
|
1044
1331
|
);
|
|
1045
1332
|
}
|
|
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
1333
|
return problems;
|
|
1052
1334
|
}
|
|
1053
|
-
function
|
|
1335
|
+
function legalContentProblems(links) {
|
|
1054
1336
|
const problems = [];
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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
|
-
}
|
|
1337
|
+
const [copyright, impressum, datenschutz] = links;
|
|
1338
|
+
if (!copyright || !/^©/.test(copyright.text) || !copyright.text.includes("Miragon GmbH")) {
|
|
1339
|
+
problems.push(
|
|
1340
|
+
`legal: the line starts with "${copyright?.text ?? ""}", expected the copyright "\xA9 <year> Miragon GmbH" (guide \xA75.10)`
|
|
1341
|
+
);
|
|
1342
|
+
} else if (copyright.href !== MIRAGON_HOME) {
|
|
1343
|
+
problems.push(`legal: the copyright links "${copyright.href}", expected ${MIRAGON_HOME}`);
|
|
1067
1344
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
`status: role is "${role ?? ""}" for data-severity="${severity ?? ""}", expected "${expected}"`
|
|
1079
|
-
);
|
|
1080
|
-
}
|
|
1345
|
+
const expected = [
|
|
1346
|
+
[impressum, "Impressum", MIRAGON_IMPRESSUM],
|
|
1347
|
+
[datenschutz, "Datenschutz", MIRAGON_DATENSCHUTZ]
|
|
1348
|
+
];
|
|
1349
|
+
for (const [link, text, href] of expected) {
|
|
1350
|
+
if (link?.text !== text) {
|
|
1351
|
+
problems.push(
|
|
1352
|
+
`legal: the links read ${links.map((entry) => `"${entry.text}"`).join(", ")}; expected the copyright, "Impressum", "Datenschutz", then the modeler's links (guide \xA75.10)`
|
|
1353
|
+
);
|
|
1354
|
+
break;
|
|
1081
1355
|
}
|
|
1356
|
+
if (link.href !== href) problems.push(`legal: "${text}" links "${link.href}", expected ${href}`);
|
|
1082
1357
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1358
|
+
return [...new Set(problems)];
|
|
1359
|
+
}
|
|
1360
|
+
function legalReachProblems(facts) {
|
|
1361
|
+
const legal = facts.legal;
|
|
1362
|
+
if (!legal) return [];
|
|
1363
|
+
const width = round2(facts.container.width);
|
|
1364
|
+
const problems = [];
|
|
1365
|
+
for (const link of facts.links) {
|
|
1366
|
+
const box = link.box;
|
|
1367
|
+
const inside = box.left >= Math.max(0, legal.left) - GEOMETRY_TOLERANCE_PX && box.left + box.width <= Math.min(legal.left + legal.width, facts.container.width) + GEOMETRY_TOLERANCE_PX && box.top >= legal.top - GEOMETRY_TOLERANCE_PX && box.top + box.height <= legal.top + legal.height + GEOMETRY_TOLERANCE_PX;
|
|
1368
|
+
if (!inside) {
|
|
1369
|
+
problems.push(
|
|
1370
|
+
`legal: [${width}px wide] "${link.text}" is cut off or scrolled out of the legal line; it wraps instead (guide \xA75.10, WCAG 1.4.10)`
|
|
1371
|
+
);
|
|
1093
1372
|
}
|
|
1094
1373
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1374
|
+
return problems;
|
|
1375
|
+
}
|
|
1376
|
+
function zoomRowBottomProblems(zoom, raised, container, legalHeight) {
|
|
1377
|
+
if (!zoom) return [];
|
|
1378
|
+
const bottom = raised ? SHELL.edge + legalLineHeight(legalHeight) + SHELL.gap : SHELL.edge;
|
|
1379
|
+
return boxMismatches("zoom", zoom, { bottom }, container).map(
|
|
1380
|
+
(problem) => `[${round2(container.width)}px wide${raised ? ", raised" : ""}] ${problem}`
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
function emptyStateProblems(facts, container, lang) {
|
|
1384
|
+
const problems = [];
|
|
1385
|
+
if (!facts.shown)
|
|
1386
|
+
problems.push('empty-state: the root lacks data-empty-state="shown" after show.emptyState');
|
|
1387
|
+
const card = facts.card;
|
|
1388
|
+
if (!card?.visible) return [...problems, "empty-state: the card is not visible after show.emptyState"];
|
|
1389
|
+
problems.push(
|
|
1390
|
+
...boxMismatches(
|
|
1391
|
+
"empty-state",
|
|
1392
|
+
card.box,
|
|
1393
|
+
{
|
|
1394
|
+
centerX: container.width / 2,
|
|
1395
|
+
centerY: container.height / 2,
|
|
1396
|
+
width: Math.min(400, container.width - 2 * SHELL.edge)
|
|
1397
|
+
},
|
|
1398
|
+
container
|
|
1399
|
+
)
|
|
1400
|
+
);
|
|
1401
|
+
if (!card.labelledBy) problems.push("empty-state: the card is not labelled by its title (aria-labelledby)");
|
|
1402
|
+
if (!facts.komet) {
|
|
1403
|
+
problems.push("empty-state: no Komet <img> on the card (guide \xA72, \xA78)");
|
|
1404
|
+
} else {
|
|
1405
|
+
if (facts.komet.alt !== "") {
|
|
1406
|
+
problems.push(
|
|
1407
|
+
`empty-state: the Komet has ${facts.komet.alt === null ? "no alt" : `alt="${facts.komet.alt}"`}; it is decorative, alt="" (guide \xA78)`
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
if (Math.abs(facts.komet.width - 64) > 1 || Math.abs(facts.komet.height - 64) > 1) {
|
|
1411
|
+
problems.push(
|
|
1412
|
+
`empty-state: the Komet is ${round2(facts.komet.width)} \xD7 ${round2(facts.komet.height)}px, expected 64 \xD7 64 (guide \xA78)`
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (facts.heading?.tag !== "h1") {
|
|
1417
|
+
problems.push(
|
|
1418
|
+
`empty-state: the title is ${facts.heading ? `a <${facts.heading.tag}>` : "missing"}, expected an <h1>`
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
const [primary, ghost, ...rest] = facts.buttons;
|
|
1422
|
+
if (!primary) {
|
|
1423
|
+
problems.push('empty-state: no "New diagram" button on the card (guide \xA78)');
|
|
1424
|
+
} else {
|
|
1425
|
+
if (primary.variant !== "primary") {
|
|
1426
|
+
problems.push(
|
|
1427
|
+
`empty-state: the first button "${primary.name}" is ${primary.variant ?? "no"} variant, expected the blue primary "New diagram" first (guide \xA78)`
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
if (ghost && ghost.variant !== "ghost") {
|
|
1431
|
+
problems.push(
|
|
1432
|
+
`empty-state: the second button "${ghost.name}" is ${ghost.variant ?? "no"} variant, expected the ghost "Show example" (guide \xA78)`
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
if (rest.length > 0)
|
|
1436
|
+
problems.push(
|
|
1437
|
+
`empty-state: ${facts.buttons.length} buttons on the card; exactly two actions (guide \xA78)`
|
|
1438
|
+
);
|
|
1439
|
+
for (const button of [primary, ghost]) {
|
|
1440
|
+
if (button && Math.abs(button.height - SHELL.entry) > 1) {
|
|
1441
|
+
problems.push(
|
|
1442
|
+
`empty-state: "${button.name}" is ${round2(button.height)}px high, expected ${SHELL.entry}px (guide \xA78)`
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (speaksEnglish(lang)) {
|
|
1447
|
+
if (primary.name !== defaultMessages.newDiagram) {
|
|
1448
|
+
problems.push(
|
|
1449
|
+
`empty-state: the primary button reads "${primary.name}", expected "${defaultMessages.newDiagram}" (guide \xA72)`
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
if (ghost && ghost.name !== defaultMessages.showExample) {
|
|
1453
|
+
problems.push(
|
|
1454
|
+
`empty-state: the ghost button reads "${ghost.name}", expected "${defaultMessages.showExample}" (guide \xA72)`
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
for (const part of facts.work) {
|
|
1460
|
+
if (part.visible) {
|
|
1461
|
+
problems.push(
|
|
1462
|
+
`empty-state: ${part.name} is visible behind the start screen; it hides the work chrome (guide \xA78)`
|
|
1463
|
+
);
|
|
1102
1464
|
}
|
|
1103
1465
|
}
|
|
1466
|
+
if (!facts.canvasInert) {
|
|
1467
|
+
problems.push(
|
|
1468
|
+
"empty-state: the canvas slot is not inert behind the start screen; nothing else takes a click or the Tab (guide \xA78)"
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
if (!facts.legalVisible)
|
|
1472
|
+
problems.push("empty-state: the legal line is not visible; a public page always shows it (guide \xA78)");
|
|
1104
1473
|
return problems;
|
|
1105
1474
|
}
|
|
1106
|
-
function
|
|
1107
|
-
const [first, second, third] = stops;
|
|
1108
|
-
const sequence = first?.kind === "primary" ? [second, third] : [first, second];
|
|
1109
|
-
const [palette, canvas] = sequence;
|
|
1475
|
+
function emptyStateTabProblems(owners) {
|
|
1110
1476
|
const problems = [];
|
|
1111
|
-
|
|
1477
|
+
for (const stop of owners) {
|
|
1478
|
+
if (["empty-state", "legal", "toasts", "body", "outside"].includes(stop.owner)) continue;
|
|
1112
1479
|
problems.push(
|
|
1113
|
-
`
|
|
1480
|
+
`empty-state: Tab reaches ${stop.description} (${stop.owner}) behind the start screen; only the card and the legal links take the focus (guide \xA78)`
|
|
1114
1481
|
);
|
|
1115
|
-
}
|
|
1482
|
+
}
|
|
1483
|
+
if (!owners.some((stop) => stop.owner === "empty-state")) {
|
|
1484
|
+
problems.push("empty-state: Tab never reaches the card's buttons");
|
|
1485
|
+
}
|
|
1486
|
+
if (!owners.some((stop) => stop.owner === "legal"))
|
|
1487
|
+
problems.push("empty-state: Tab never reaches the legal links");
|
|
1488
|
+
return [...new Set(problems)];
|
|
1489
|
+
}
|
|
1490
|
+
function emptyStateKeyProblems(key, before, after, keys) {
|
|
1491
|
+
const problems = [];
|
|
1492
|
+
if (after !== before) {
|
|
1116
1493
|
problems.push(
|
|
1117
|
-
`
|
|
1494
|
+
`empty-state: ${key} behind the start screen changed the zoom from ${before ?? "?"}% to ${after ?? "?"}%; the chrome keys zoom no diagram there`
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
if (keys.some((recorded) => recorded.defaultPrevented)) {
|
|
1498
|
+
problems.push(
|
|
1499
|
+
`empty-state: ${key} behind the start screen arrived prevented; with no diagram to zoom the browser keeps its page zoom (WCAG 1.4.4)`
|
|
1118
1500
|
);
|
|
1119
1501
|
}
|
|
1120
1502
|
return problems;
|
|
1121
1503
|
}
|
|
1122
|
-
function
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
);
|
|
1504
|
+
function afterStartProblems(facts, focus) {
|
|
1505
|
+
const problems = [];
|
|
1506
|
+
const after = 'after "New diagram"';
|
|
1507
|
+
if (facts.shown) problems.push(`empty-state: ${after} the root still carries data-empty-state="shown"`);
|
|
1508
|
+
if (facts.card?.visible) problems.push(`empty-state: ${after} the card still shows`);
|
|
1509
|
+
for (const name2 of ["menu", "palette", "zoom"]) {
|
|
1510
|
+
const part = facts.work.find((candidate) => candidate.name === name2);
|
|
1511
|
+
if (part && !part.visible) problems.push(`empty-state: ${after} the ${name2} is still hidden`);
|
|
1512
|
+
}
|
|
1513
|
+
if (facts.canvasInert) problems.push(`empty-state: ${after} the canvas slot is still inert`);
|
|
1514
|
+
if (focus.owner !== "canvas") {
|
|
1515
|
+
problems.push(
|
|
1516
|
+
`empty-state: ${after} the focus is on ${focus.description}, expected the canvas <svg> (API.md, Empty state)`
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
return problems;
|
|
1126
1520
|
}
|
|
1127
|
-
function
|
|
1128
|
-
const
|
|
1129
|
-
return
|
|
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
|
-
);
|
|
1521
|
+
function toastLiveRegion(regions, status) {
|
|
1522
|
+
const role = status === "danger" ? "alert" : "status";
|
|
1523
|
+
return regions.find((region) => region.role === role);
|
|
1132
1524
|
}
|
|
1133
|
-
function
|
|
1525
|
+
function toastAnatomyProblems(toast, status, lang, announcement) {
|
|
1134
1526
|
const problems = [];
|
|
1135
|
-
|
|
1136
|
-
|
|
1527
|
+
const role = status === "danger" ? "alert" : "status";
|
|
1528
|
+
if (!announcement.persistent) {
|
|
1529
|
+
problems.push(
|
|
1530
|
+
`toasts: no role="${role}" live region in the root before the first toast; a live region created together with its text is often not announced (guide \xA78, \xA712)`
|
|
1531
|
+
);
|
|
1532
|
+
} else if (!announcement.announced) {
|
|
1533
|
+
problems.push(
|
|
1534
|
+
`toasts: the ${status} toast's text never reaches the role="${role}" live region (guide \xA78, \xA712)`
|
|
1535
|
+
);
|
|
1137
1536
|
}
|
|
1138
|
-
|
|
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) {
|
|
1537
|
+
if (toast.role === "status" || toast.role === "alert") {
|
|
1142
1538
|
problems.push(
|
|
1143
|
-
`
|
|
1539
|
+
`toasts: the ${status} toast's card carries role="${toast.role}"; the live region announces it, so a live card is read twice`
|
|
1144
1540
|
);
|
|
1145
1541
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1542
|
+
const close = toast.buttons.at(-1);
|
|
1543
|
+
if (!close || close.text !== "") {
|
|
1544
|
+
problems.push(`toasts: the ${status} toast has no Close icon button at its end (guide \xA78)`);
|
|
1545
|
+
} else if (!close.name) {
|
|
1546
|
+
problems.push(`toasts: the ${status} toast's Close button has no accessible name`);
|
|
1547
|
+
} else if (speaksEnglish(lang) && close.name !== defaultMessages.close) {
|
|
1548
|
+
problems.push(
|
|
1549
|
+
`toasts: the Close button is named "${close.name}", expected "${defaultMessages.close}" (guide \xA78)`
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
if (toast.box.height < 40 - GEOMETRY_TOLERANCE_PX) {
|
|
1553
|
+
problems.push(
|
|
1554
|
+
`toasts: the ${status} toast is ${round2(toast.box.height)}px high, at least 40px (guide \xA78)`
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
return problems;
|
|
1558
|
+
}
|
|
1559
|
+
function shownToasts(toasts2, texts) {
|
|
1560
|
+
return toasts2.flatMap((toast) => texts.filter((text) => toast.text.includes(text)));
|
|
1561
|
+
}
|
|
1562
|
+
function toastStackProblems(toasts2, shown, expected) {
|
|
1563
|
+
const problems = [];
|
|
1564
|
+
const present = shownToasts(toasts2, shown);
|
|
1565
|
+
if (toasts2.length > MAX_TOASTS)
|
|
1566
|
+
problems.push(`toasts: ${toasts2.length} toasts at once, at most ${MAX_TOASTS} (guide \xA78)`);
|
|
1567
|
+
if (present.join("\0") !== expected.join("\0")) {
|
|
1568
|
+
problems.push(
|
|
1569
|
+
`toasts: after ${shown.length} toasts the stack shows ${present.map((text) => `"${text}"`).join(", ") || "none"}; expected ${expected.map((text) => `"${text}"`).join(", ")} (at most ${MAX_TOASTS}, the oldest without an action leaves, guide \xA78)`
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
const tops = toasts2.filter((toast) => shown.some((text) => toast.text.includes(text))).map((toast) => toast.box.top);
|
|
1573
|
+
if (tops.some((top, index) => index > 0 && top < tops[index - 1])) {
|
|
1574
|
+
problems.push("toasts: the newest toast is not at the bottom of the stack (guide \xA78)");
|
|
1575
|
+
}
|
|
1576
|
+
return problems;
|
|
1577
|
+
}
|
|
1578
|
+
function repeatedToastProblems(toasts2, text) {
|
|
1579
|
+
const count = toasts2.filter((toast) => toast.text.includes(text)).length;
|
|
1580
|
+
return count > 1 ? [`toasts: a repeated "${text}" stacks ${count} toasts; it restarts the shown one (API.md, Toasts)`] : [];
|
|
1581
|
+
}
|
|
1582
|
+
var TOAST_RULES = {
|
|
1583
|
+
plain: "it stays for its lifetime",
|
|
1584
|
+
hovered: "hover pauses its time",
|
|
1585
|
+
focused: "focus pauses its time",
|
|
1586
|
+
resumed: "it resumes with at least 1.5s left",
|
|
1587
|
+
action: "a toast with an action never closes by itself"
|
|
1588
|
+
};
|
|
1589
|
+
function toastTimingProblems(timings) {
|
|
1590
|
+
return timings.flatMap((timing) => {
|
|
1591
|
+
const since = timing.condition === "resumed" ? "after the pointer and the focus left it" : "after it appeared";
|
|
1592
|
+
if (timing.expectation === "present" && !timing.present) {
|
|
1593
|
+
return [
|
|
1594
|
+
`toasts: "${timing.text}" closed within ${timing.after}ms ${since}; ${TOAST_RULES[timing.condition]} (guide \xA78)`
|
|
1595
|
+
];
|
|
1155
1596
|
}
|
|
1156
|
-
if (
|
|
1157
|
-
|
|
1597
|
+
if (timing.expectation === "gone" && timing.present) {
|
|
1598
|
+
return [
|
|
1599
|
+
`toasts: "${timing.text}" is still there ${timing.after}ms ${since}; it closes after its lifetime (guide \xA78)`
|
|
1600
|
+
];
|
|
1158
1601
|
}
|
|
1602
|
+
return [];
|
|
1159
1603
|
});
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1604
|
+
}
|
|
1605
|
+
function popoverKeyboardProblems(steps, rowCount) {
|
|
1606
|
+
const problems = [];
|
|
1607
|
+
for (const step of steps) {
|
|
1608
|
+
if (step.focused?.index !== step.expected) {
|
|
1163
1609
|
problems.push(
|
|
1164
|
-
`
|
|
1610
|
+
`problems-popover: ${step.key} reaches ${step.focused ? `row ${step.focused.index + 1}` : "no row"}, expected row ${step.expected + 1} of ${rowCount}; the keyboard reads every row, text rows too (guide \xA75.10, WCAG 2.1.1)`
|
|
1611
|
+
);
|
|
1612
|
+
} else if (!step.focused.inView) {
|
|
1613
|
+
problems.push(
|
|
1614
|
+
`problems-popover: row ${step.expected + 1} takes the focus on ${step.key} but stays scrolled out of the list`
|
|
1165
1615
|
);
|
|
1166
1616
|
}
|
|
1167
1617
|
}
|
|
1168
1618
|
return problems;
|
|
1169
1619
|
}
|
|
1170
|
-
function
|
|
1620
|
+
function openPopoverProblems(facts, container, lang) {
|
|
1171
1621
|
const problems = [];
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
if (
|
|
1176
|
-
problems.push(
|
|
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;
|
|
1622
|
+
const popover2 = facts.popover;
|
|
1623
|
+
if (!popover2?.visible)
|
|
1624
|
+
return ["problems-popover: a click on the pill shows no problems list (guide \xA75.10)"];
|
|
1625
|
+
if (popover2.attributes.role !== "dialog") {
|
|
1626
|
+
problems.push(`problems-popover: role "${popover2.attributes.role ?? ""}", expected "dialog"`);
|
|
1194
1627
|
}
|
|
1195
|
-
if (
|
|
1196
|
-
|
|
1628
|
+
if (popover2.attributes["aria-modal"] === "true")
|
|
1629
|
+
problems.push('problems-popover: aria-modal="true"; it is not modal');
|
|
1630
|
+
if (!popover2.title) {
|
|
1631
|
+
problems.push("problems-popover: not labelled by its title (aria-labelledby)");
|
|
1632
|
+
} else if (speaksEnglish(lang) && popover2.title !== defaultMessages.problemsTitle) {
|
|
1633
|
+
problems.push(
|
|
1634
|
+
`problems-popover: titled "${popover2.title}", expected "${defaultMessages.problemsTitle}" (guide \xA75.10)`
|
|
1635
|
+
);
|
|
1197
1636
|
}
|
|
1198
|
-
if (
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1637
|
+
if (facts.pill?.attributes["aria-expanded"] !== "true") {
|
|
1638
|
+
problems.push(
|
|
1639
|
+
`problems: aria-expanded is "${facts.pill?.attributes["aria-expanded"] ?? ""}" while the list is open, expected "true"`
|
|
1640
|
+
);
|
|
1202
1641
|
}
|
|
1203
|
-
if (
|
|
1204
|
-
|
|
1642
|
+
if (facts.rows.length === 0) {
|
|
1643
|
+
problems.push("problems-popover: the list has no rows; show.problems passes items in the web host");
|
|
1205
1644
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
|
|
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) {
|
|
1645
|
+
for (const row of facts.rows) {
|
|
1646
|
+
if (row.height < SHELL.entry - GEOMETRY_TOLERANCE_PX) {
|
|
1647
|
+
problems.push(
|
|
1648
|
+
`problems-popover: the row "${row.text}" is ${round2(row.height)}px high, at least ${SHELL.entry}px (guide \xA75.10)`
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
if (!row.icon)
|
|
1652
|
+
problems.push(`problems-popover: the row "${row.text}" has no severity icon (guide \xA75.10)`);
|
|
1653
|
+
}
|
|
1654
|
+
if (facts.focus !== "popover") {
|
|
1228
1655
|
problems.push(
|
|
1229
|
-
|
|
1656
|
+
`problems-popover: opening it leaves the focus on ${facts.focused}; it moves to the first row (API.md, Problems popover)`
|
|
1230
1657
|
);
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1658
|
+
}
|
|
1659
|
+
const pill = facts.pill;
|
|
1660
|
+
if (pill) {
|
|
1661
|
+
problems.push(
|
|
1662
|
+
...boxMismatches(
|
|
1663
|
+
"problems-popover",
|
|
1664
|
+
popover2.box,
|
|
1665
|
+
{ left: SHELL.edge, bottom: container.height - pill.box.top + SHELL.gap },
|
|
1666
|
+
container
|
|
1667
|
+
)
|
|
1668
|
+
);
|
|
1669
|
+
if (popover2.box.width > Math.min(320, container.width - 2 * SHELL.edge) + GEOMETRY_TOLERANCE_PX + 1e-6) {
|
|
1236
1670
|
problems.push(
|
|
1237
|
-
|
|
1671
|
+
`problems-popover: width is ${round2(popover2.box.width)}px, at most ${round2(Math.min(320, container.width - 2 * SHELL.edge))}px expected`
|
|
1238
1672
|
);
|
|
1239
1673
|
}
|
|
1674
|
+
if (popover2.box.height > 320 + GEOMETRY_TOLERANCE_PX + 1e-6) {
|
|
1675
|
+
problems.push(`problems-popover: height is ${round2(popover2.box.height)}px, at most 320px expected`);
|
|
1676
|
+
}
|
|
1240
1677
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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;
|
|
1678
|
+
return problems;
|
|
1679
|
+
}
|
|
1680
|
+
function closedPopoverProblems(facts, how) {
|
|
1681
|
+
const problems = [];
|
|
1682
|
+
if (facts.popover?.visible) problems.push(`problems-popover: ${how} does not close the list`);
|
|
1683
|
+
if (facts.pill && facts.pill.attributes["aria-expanded"] !== "false") {
|
|
1250
1684
|
problems.push(
|
|
1251
|
-
|
|
1685
|
+
`problems: aria-expanded is "${facts.pill.attributes["aria-expanded"] ?? ""}" after ${how}, expected "false"`
|
|
1252
1686
|
);
|
|
1253
1687
|
}
|
|
1254
|
-
if (
|
|
1688
|
+
if (how === "Escape" && facts.focus !== "pill") {
|
|
1689
|
+
problems.push(`problems-popover: Escape leaves the focus on ${facts.focused}, expected the pill`);
|
|
1690
|
+
}
|
|
1691
|
+
if (how === "an outside click" && facts.focus === "pill") {
|
|
1255
1692
|
problems.push(
|
|
1256
|
-
"
|
|
1693
|
+
"problems-popover: an outside click moved the focus to the pill; it closes without moving the focus"
|
|
1257
1694
|
);
|
|
1258
1695
|
}
|
|
1259
|
-
if (
|
|
1696
|
+
if (how === "a row with an element" && facts.focus !== "canvas") {
|
|
1260
1697
|
problems.push(
|
|
1261
|
-
|
|
1698
|
+
`problems-popover: a row with an element leaves the focus on ${facts.focused}, expected the canvas`
|
|
1262
1699
|
);
|
|
1263
1700
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1701
|
+
return problems;
|
|
1702
|
+
}
|
|
1703
|
+
function dropStateProblems(facts, expected, context, container) {
|
|
1704
|
+
const problems = [];
|
|
1705
|
+
const zone = facts.zone;
|
|
1706
|
+
const over = facts.events.filter((event) => event.type === "dragover");
|
|
1707
|
+
if (expected === "none") {
|
|
1708
|
+
if (facts.state !== null)
|
|
1709
|
+
problems.push(`drop-zone: ${context} sets data-drop="${facts.state}"; it is no file drag`);
|
|
1710
|
+
if (zone?.visible) problems.push(`drop-zone: ${context} shows the drop zone`);
|
|
1711
|
+
if (over.some((event) => event.defaultPrevented))
|
|
1712
|
+
problems.push(`drop-zone: ${context} is prevented; the drop zone leaves non-file drags alone`);
|
|
1713
|
+
return problems;
|
|
1714
|
+
}
|
|
1715
|
+
if (facts.state !== expected) {
|
|
1266
1716
|
problems.push(
|
|
1267
|
-
|
|
1717
|
+
`drop-zone: ${context} sets data-drop="${facts.state ?? ""}", expected "${expected}" (guide \xA79)`
|
|
1268
1718
|
);
|
|
1269
1719
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
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
|
-
}
|
|
1720
|
+
if (facts.events.some((event) => !event.defaultPrevented && event.type !== "dragleave")) {
|
|
1721
|
+
problems.push(`drop-zone: ${context} is not prevented, so the browser would open the file`);
|
|
1289
1722
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1723
|
+
const effect = expected === "over" ? "copy" : "none";
|
|
1724
|
+
for (const event of over) {
|
|
1725
|
+
if (event.dropEffect !== effect)
|
|
1726
|
+
problems.push(`drop-zone: ${context} has dropEffect "${event.dropEffect}", expected "${effect}"`);
|
|
1294
1727
|
}
|
|
1295
|
-
return {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
"
|
|
1302
|
-
"
|
|
1303
|
-
"
|
|
1304
|
-
]
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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`);
|
|
1728
|
+
if (!zone?.visible) return [...problems, `drop-zone: ${context} shows no drop zone (guide \xA79)`];
|
|
1729
|
+
problems.push(
|
|
1730
|
+
...boxMismatches("drop-zone", zone.box, { left: 12, top: 12, right: 12, bottom: 12 }, container).map(
|
|
1731
|
+
(problem) => `${problem} (${context})`
|
|
1732
|
+
)
|
|
1733
|
+
);
|
|
1734
|
+
if (zone.ariaHidden !== "true") problems.push('drop-zone: the overlay lacks aria-hidden="true"');
|
|
1735
|
+
const fill = expected === "over" ? ["--cd-ui-hover", facts.roles.hover] : ["--cd-ui-danger-flaeche", facts.roles.dangerFlaeche];
|
|
1736
|
+
const border = expected === "over" ? ["--cd-ui-interaktion", facts.roles.interaktion] : ["--cd-ui-danger", facts.roles.danger];
|
|
1737
|
+
if (zone.background !== fill[1]) {
|
|
1738
|
+
problems.push(
|
|
1739
|
+
`drop-zone: ${context} fills ${zone.background}, expected ${fill[0]} (${fill[1]}) (guide \xA79)`
|
|
1740
|
+
);
|
|
1314
1741
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
if (written.includes(name2)) problems.push(`the root keeps ${name2} after destroy`);
|
|
1742
|
+
if (zone.borderStyle !== "dashed" || zone.borderWidth !== "2px" || zone.borderColor !== border[1]) {
|
|
1743
|
+
problems.push(
|
|
1744
|
+
`drop-zone: ${context} has a ${zone.borderWidth} ${zone.borderStyle} ${zone.borderColor} border, expected 2px dashed ${border[0]} (${border[1]}) (guide \xA79)`
|
|
1745
|
+
);
|
|
1320
1746
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1747
|
+
if (!facts.chip?.text) {
|
|
1748
|
+
problems.push(`drop-zone: ${context} shows no chip text (guide \xA79)`);
|
|
1749
|
+
} else if (facts.chip.background !== facts.roles.flaecheHoch) {
|
|
1750
|
+
problems.push(
|
|
1751
|
+
`drop-zone: the chip fills ${facts.chip.background}, expected --cd-ui-flaeche-hoch (${facts.roles.flaecheHoch})`
|
|
1752
|
+
);
|
|
1323
1753
|
}
|
|
1324
1754
|
return problems;
|
|
1325
1755
|
}
|
|
1326
|
-
function
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
if (
|
|
1330
|
-
|
|
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`);
|
|
1756
|
+
function expectedWebMode(requested, themes, choice) {
|
|
1757
|
+
const kontrast = requested.kontrast ? { kontrast: requested.kontrast } : {};
|
|
1758
|
+
if (!themes.includes("dark")) return { carrier: "light", effective: { theme: "light", ...kontrast } };
|
|
1759
|
+
if (choice === "system") return { carrier: "system", effective: { theme: requested.theme, ...kontrast } };
|
|
1760
|
+
return { carrier: choice, effective: { theme: choice, ...kontrast } };
|
|
1343
1761
|
}
|
|
1344
|
-
function
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
]
|
|
1762
|
+
function webModeProblems(facts, themes, choice, colors) {
|
|
1763
|
+
const problems = [];
|
|
1764
|
+
const dark = themes.includes("dark");
|
|
1765
|
+
if (!dark) {
|
|
1766
|
+
const metas = facts.themeColors;
|
|
1767
|
+
if (metas.length !== 1 || metas[0]?.media !== null) {
|
|
1768
|
+
problems.push(
|
|
1769
|
+
`mode: a light-only web app carries one theme-color without media (guide \xA713.3); found ${metas.map((meta) => meta.media ? `${meta.content} (${meta.media})` : meta.content).join(", ") || "none"}`
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
if (facts.colorScheme !== null && facts.colorScheme.split(/\s+/).includes("dark")) {
|
|
1773
|
+
problems.push(`mode: a light-only web app declares color-scheme "${facts.colorScheme}"`);
|
|
1774
|
+
}
|
|
1775
|
+
return problems;
|
|
1350
1776
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1777
|
+
const label = choice === "system" ? "System" : choice === "dark" ? "Dark" : "Light";
|
|
1778
|
+
const expectedScheme = choice === "system" ? "light dark" : choice;
|
|
1779
|
+
if (facts.colorScheme !== expectedScheme) {
|
|
1780
|
+
problems.push(
|
|
1781
|
+
`mode: with ${label} <meta name="color-scheme"> reads "${facts.colorScheme ?? "(none)"}", expected "${expectedScheme}" (guide \xA713.3)`
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
const stored = choice === "system" ? null : choice;
|
|
1785
|
+
if (!facts.storageBlocked && facts.stored !== stored) {
|
|
1786
|
+
problems.push(
|
|
1787
|
+
stored === null ? `mode: with System localStorage keeps miragon.darstellung="${facts.stored ?? ""}"; System removes the entry (guide \xA713.3)` : `mode: with ${label} localStorage holds miragon.darstellung=${facts.stored === null ? "(none)" : `"${facts.stored}"`}, expected "${stored}" (guide \xA713.3)`
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
const byMedia = (scheme) => facts.themeColors.find((meta) => meta.media?.replace(/\s+/g, "") === `(prefers-color-scheme:${scheme})`);
|
|
1791
|
+
const light = byMedia("light");
|
|
1792
|
+
const darkMeta = byMedia("dark");
|
|
1793
|
+
if (!light || !darkMeta) {
|
|
1794
|
+
problems.push(
|
|
1795
|
+
"mode: a dark-capable web app carries two theme-color metas, one per prefers-color-scheme (guide \xA713.3)"
|
|
1796
|
+
);
|
|
1797
|
+
} else if (colors) {
|
|
1798
|
+
const want = choice === "system" ? colors : { light: colors[choice], dark: colors[choice] };
|
|
1799
|
+
if (light.content !== want.light || darkMeta.content !== want.dark) {
|
|
1800
|
+
problems.push(
|
|
1801
|
+
`mode: with ${label} the theme-color metas read ${light.content} (light) and ${darkMeta.content} (dark), expected ${want.light} and ${want.dark} (guide \xA713.3)`
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
return problems;
|
|
1355
1806
|
}
|
|
1356
|
-
function
|
|
1357
|
-
|
|
1807
|
+
function modeSwitchProblems(changes, label) {
|
|
1808
|
+
const relevant = changes.filter((change) => ["theme", "color-scheme", "theme-color"].includes(change.part));
|
|
1809
|
+
if (relevant.length === 0) return [`mode: choosing ${label} changed nothing on <html> or its meta tags`];
|
|
1810
|
+
const problems = [];
|
|
1811
|
+
const frames = [...new Set(relevant.map((change) => change.frame))];
|
|
1812
|
+
if (frames.length > 1) {
|
|
1813
|
+
const parts = relevant.map((change) => `${change.part} in frame ${change.frame}`).join(", ");
|
|
1814
|
+
problems.push(`mode: choosing ${label} changed ${parts}; one frame for all of them (guide \xA713.3)`);
|
|
1815
|
+
}
|
|
1816
|
+
const first = Math.min(...frames);
|
|
1817
|
+
if (!changes.some(
|
|
1818
|
+
(change) => change.part === "no-transition" && change.value === "on" && change.frame === first
|
|
1819
|
+
)) {
|
|
1820
|
+
problems.push(
|
|
1821
|
+
`mode: choosing ${label} switched without cd-shell-no-transition on <html> in that frame (guide \xA77: no colour fades)`
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
return problems;
|
|
1358
1825
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1826
|
+
|
|
1827
|
+
// src/testing/checks.ts
|
|
1828
|
+
var normalize2 = (text) => text.replace(/\s+/g, " ").trim();
|
|
1829
|
+
var name = (element) => normalize2(element.attributes["aria-label"] ?? element.text);
|
|
1830
|
+
var attribute = (element, key) => element.attributes[key];
|
|
1831
|
+
function expectedKeyShortcuts(platform) {
|
|
1832
|
+
const modifier = platform === "mac" ? "Meta" : "Control";
|
|
1833
|
+
return {
|
|
1834
|
+
in: `${modifier}+= ${modifier}+Plus`,
|
|
1835
|
+
out: `${modifier}+-`,
|
|
1836
|
+
actual: `${modifier}+0`,
|
|
1837
|
+
fit: "Shift+1"
|
|
1838
|
+
};
|
|
1363
1839
|
}
|
|
1364
|
-
function
|
|
1840
|
+
function ariaProblems(facts, host) {
|
|
1365
1841
|
const problems = [];
|
|
1366
|
-
|
|
1367
|
-
if (!
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1842
|
+
if (host === "embedded") {
|
|
1843
|
+
if (!facts.rootLang)
|
|
1844
|
+
problems.push("root: an embedded shell root carries the tool's lang itself (guide \xA72, \xA712)");
|
|
1845
|
+
} else if (!facts.documentLang) {
|
|
1846
|
+
problems.push("<html> has no lang (guide \xA72)");
|
|
1847
|
+
}
|
|
1848
|
+
const trigger = facts.trigger;
|
|
1849
|
+
if (!trigger) {
|
|
1850
|
+
problems.push('menu: no [data-chrome="menu"] trigger');
|
|
1851
|
+
} else {
|
|
1852
|
+
if (trigger.tag !== "button")
|
|
1853
|
+
problems.push(`menu: the trigger is a <${trigger.tag}>, expected a <button>`);
|
|
1854
|
+
if (!attribute(trigger, "aria-label")) problems.push("menu: the icon-only trigger has no aria-label");
|
|
1855
|
+
if (attribute(trigger, "aria-haspopup") !== "menu") {
|
|
1856
|
+
problems.push(`menu: aria-haspopup is "${attribute(trigger, "aria-haspopup") ?? ""}", expected "menu"`);
|
|
1857
|
+
}
|
|
1858
|
+
if (attribute(trigger, "aria-expanded") !== "false") {
|
|
1371
1859
|
problems.push(
|
|
1372
|
-
`
|
|
1860
|
+
`menu: aria-expanded is "${attribute(trigger, "aria-expanded") ?? ""}" while closed, expected "false"`
|
|
1373
1861
|
);
|
|
1374
1862
|
}
|
|
1375
|
-
if (
|
|
1376
|
-
|
|
1863
|
+
if (!attribute(trigger, "aria-controls")) {
|
|
1864
|
+
problems.push("menu: the trigger has no aria-controls");
|
|
1865
|
+
} else if (!facts.controlled) {
|
|
1866
|
+
problems.push(`menu: aria-controls="${attribute(trigger, "aria-controls")}" names no element`);
|
|
1867
|
+
} else if (attribute(facts.controlled, "role") !== "menu") {
|
|
1377
1868
|
problems.push(
|
|
1378
|
-
`
|
|
1869
|
+
`menu: aria-controls names a ${facts.controlled.description} with role "${attribute(facts.controlled, "role") ?? ""}", expected "menu"`
|
|
1379
1870
|
);
|
|
1380
1871
|
}
|
|
1381
1872
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1873
|
+
if (!facts.palette) {
|
|
1874
|
+
problems.push('palette: no [data-chrome="palette"]');
|
|
1875
|
+
} else {
|
|
1876
|
+
const toolbar = facts.toolbar;
|
|
1877
|
+
if (!toolbar || attribute(toolbar, "role") !== "toolbar") {
|
|
1878
|
+
problems.push('palette: .djs-palette-entries has no role="toolbar"');
|
|
1879
|
+
} else if (!attribute(toolbar, "aria-label")) {
|
|
1880
|
+
problems.push("palette: the toolbar has no aria-label");
|
|
1881
|
+
}
|
|
1882
|
+
for (const entry of facts.entries) {
|
|
1883
|
+
const action = attribute(entry, "data-action") ?? entry.description;
|
|
1884
|
+
if (attribute(entry, "role") !== "button")
|
|
1885
|
+
problems.push(`palette: entry "${action}" has no role="button"`);
|
|
1886
|
+
if (!attribute(entry, "aria-label"))
|
|
1887
|
+
problems.push(`palette: entry "${action}" has no aria-label (title)`);
|
|
1888
|
+
const tabindex = attribute(entry, "tabindex");
|
|
1889
|
+
if (tabindex !== "0" && tabindex !== "-1") {
|
|
1890
|
+
problems.push(`palette: entry "${action}" has tabindex "${tabindex ?? ""}", expected roving 0/-1`);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
const stops = facts.entries.filter((entry) => attribute(entry, "tabindex") === "0");
|
|
1894
|
+
if (facts.entries.length > 0 && stops.length !== 1) {
|
|
1895
|
+
problems.push(
|
|
1896
|
+
`palette: ${stops.length} entries with tabindex="0", expected exactly one (roving tabindex)`
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
const tools = facts.entries.filter(
|
|
1900
|
+
(entry) => entry.group === "tools" && (attribute(entry, "data-action") ?? "").endsWith("-tool")
|
|
1901
|
+
);
|
|
1902
|
+
for (const tool of tools) {
|
|
1903
|
+
const pressed2 = attribute(tool, "aria-pressed");
|
|
1904
|
+
if (pressed2 !== "true" && pressed2 !== "false") {
|
|
1905
|
+
problems.push(`palette: tool "${attribute(tool, "data-action")}" has no aria-pressed`);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
const pressed = tools.filter((tool) => attribute(tool, "aria-pressed") === "true");
|
|
1909
|
+
if (pressed.length > 1) problems.push(`palette: ${pressed.length} tools are aria-pressed at once`);
|
|
1393
1910
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1911
|
+
const zoom = facts.zoom;
|
|
1912
|
+
if (!zoom) {
|
|
1913
|
+
problems.push('zoom: no [data-chrome="zoom"]');
|
|
1914
|
+
} else {
|
|
1915
|
+
if (attribute(zoom, "role") !== "group") problems.push('zoom: the panel has no role="group"');
|
|
1916
|
+
if (!attribute(zoom, "aria-label")) problems.push("zoom: the panel has no aria-label");
|
|
1917
|
+
const shortcuts = expectedKeyShortcuts(facts.platform);
|
|
1918
|
+
for (const button of ["out", "actual", "in", "fit"]) {
|
|
1919
|
+
const control = facts.zoomButtons[button];
|
|
1920
|
+
if (!control) {
|
|
1921
|
+
problems.push(`zoom: no [data-zoom="${button}"] button`);
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
if (control.tag !== "button") problems.push(`zoom: [data-zoom="${button}"] is a <${control.tag}>`);
|
|
1925
|
+
if (!name(control)) problems.push(`zoom: [data-zoom="${button}"] has no accessible name`);
|
|
1926
|
+
if (attribute(control, "aria-keyshortcuts") !== shortcuts[button]) {
|
|
1927
|
+
problems.push(
|
|
1928
|
+
`zoom: [data-zoom="${button}"] aria-keyshortcuts is "${attribute(control, "aria-keyshortcuts") ?? ""}", expected "${shortcuts[button]}" (${facts.platform})`
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
if (control.disabled) {
|
|
1932
|
+
problems.push(
|
|
1933
|
+
`zoom: [data-zoom="${button}"] is natively disabled; aria-disabled keeps the focus (guide \xA75.8)`
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
const readout = facts.zoomButtons.actual;
|
|
1938
|
+
if (readout) {
|
|
1939
|
+
if (!/\d/.test(readout.text))
|
|
1940
|
+
problems.push(`zoom: the readout shows "${readout.text}", expected a percentage`);
|
|
1941
|
+
const label = normalize2(attribute(readout, "aria-label") ?? "");
|
|
1942
|
+
if (!label.includes(readout.text)) {
|
|
1943
|
+
problems.push(
|
|
1944
|
+
`zoom: the readout's aria-label "${label}" does not name the current zoom "${readout.text}"`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
if (attribute(readout, "aria-disabled") === "true")
|
|
1948
|
+
problems.push("zoom: the readout is never disabled");
|
|
1949
|
+
}
|
|
1398
1950
|
}
|
|
1399
|
-
const
|
|
1400
|
-
const
|
|
1401
|
-
if (
|
|
1402
|
-
if (!facts.canvas.focusInCanvas) {
|
|
1951
|
+
const present = new Set(facts.order);
|
|
1952
|
+
const expectedOrder = DOM_ORDER.filter((part) => present.has(part));
|
|
1953
|
+
if (facts.order.join() !== expectedOrder.join()) {
|
|
1403
1954
|
problems.push(
|
|
1404
|
-
`
|
|
1955
|
+
`DOM order is ${facts.order.join(", ")}; expected ${expectedOrder.join(", ")} (guide \xA75.14)`
|
|
1405
1956
|
);
|
|
1406
1957
|
}
|
|
1407
|
-
|
|
1408
|
-
const
|
|
1409
|
-
|
|
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) {
|
|
1958
|
+
if (!present.has("canvas")) problems.push("canvas: no diagram-js <svg> (.djs-container > svg) in the root");
|
|
1959
|
+
const canvas = facts.canvas;
|
|
1960
|
+
if (canvas && !normalize2(attribute(canvas, "aria-label") ?? "") && !attribute(canvas, "aria-labelledby")) {
|
|
1412
1961
|
problems.push(
|
|
1413
|
-
|
|
1962
|
+
'canvas: the focusable <svg> has no accessible name (WCAG 4.1.2); shellModule names it role="application" with messages.canvas'
|
|
1414
1963
|
);
|
|
1415
1964
|
}
|
|
1965
|
+
for (const element of facts.positiveTabindex)
|
|
1966
|
+
problems.push(`positive tabindex on ${element} (guide \xA75.14)`);
|
|
1967
|
+
for (const element of facts.titled) {
|
|
1968
|
+
problems.push(`${element} has a native title; the chrome tooltip replaces it (guide \xA75.6)`);
|
|
1969
|
+
}
|
|
1970
|
+
problems.push(...primaryAriaProblems(facts, host), ...legalAriaProblems(facts, host));
|
|
1971
|
+
return problems;
|
|
1972
|
+
}
|
|
1973
|
+
function optionalIslandProblems(facts, shown) {
|
|
1974
|
+
const problems = [];
|
|
1975
|
+
if (shown.has("problems")) {
|
|
1976
|
+
const pill = facts.problems;
|
|
1977
|
+
if (!pill?.visible) {
|
|
1978
|
+
problems.push("problems: the pill is not visible after show.problems");
|
|
1979
|
+
} else {
|
|
1980
|
+
if (pill.tag !== "button") problems.push(`problems: the pill is a <${pill.tag}>, expected a <button>`);
|
|
1981
|
+
const label = normalize2(attribute(pill, "aria-label") ?? "");
|
|
1982
|
+
if (!label) problems.push("problems: the pill has no aria-label (count and errors)");
|
|
1983
|
+
else if (!label.includes(pill.text)) {
|
|
1984
|
+
problems.push(`problems: the aria-label "${label}" does not contain the visible text "${pill.text}"`);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
if (shown.has("status")) {
|
|
1989
|
+
const status = facts.status;
|
|
1990
|
+
if (!status?.visible) {
|
|
1991
|
+
problems.push("status: the status box is not visible after show.status");
|
|
1992
|
+
} else {
|
|
1993
|
+
const role = attribute(status, "role");
|
|
1994
|
+
const severity = attribute(status, "data-severity");
|
|
1995
|
+
const expected = severity === "danger" ? "alert" : "status";
|
|
1996
|
+
if (role !== expected) {
|
|
1997
|
+
problems.push(
|
|
1998
|
+
`status: role is "${role ?? ""}" for data-severity="${severity ?? ""}", expected "${expected}"`
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
if (shown.has("legend")) {
|
|
2004
|
+
const legend = facts.legend;
|
|
2005
|
+
if (!legend?.visible) {
|
|
2006
|
+
problems.push("legend: the legend is not visible after show.legend");
|
|
2007
|
+
} else {
|
|
2008
|
+
if (!facts.legendTitle)
|
|
2009
|
+
problems.push("legend: the card is not labelled by its title (aria-labelledby)");
|
|
2010
|
+
if (!facts.legendClose || !attribute(facts.legendClose, "aria-label")) {
|
|
2011
|
+
problems.push("legend: the close button has no aria-label");
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
if (shown.has("inspector")) {
|
|
2016
|
+
const inspector = facts.inspector;
|
|
2017
|
+
if (!inspector?.visible) {
|
|
2018
|
+
problems.push("inspector: the inspector is not visible after show.inspector");
|
|
2019
|
+
} else {
|
|
2020
|
+
if (inspector.tag !== "aside") problems.push(`inspector: a <${inspector.tag}>, expected an <aside>`);
|
|
2021
|
+
if (!attribute(inspector, "aria-label")) problems.push("inspector: the <aside> has no aria-label");
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
return problems;
|
|
2025
|
+
}
|
|
2026
|
+
function tabSequenceProblems(stops, primary = false) {
|
|
2027
|
+
const [first, second, third] = stops;
|
|
2028
|
+
const sequence = first?.kind === "primary" ? [second, third] : [first, second];
|
|
2029
|
+
const [palette, canvas] = sequence;
|
|
2030
|
+
const problems = [];
|
|
2031
|
+
if (primary && first?.kind !== "primary") {
|
|
2032
|
+
problems.push(
|
|
2033
|
+
`tab order: Tab from the menu trigger lands on ${first?.description ?? "nothing"}, expected the primary action (top left, top right, top centre, guide \xA75.14)`
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
if (palette?.kind !== "palette") {
|
|
2037
|
+
problems.push(
|
|
2038
|
+
`tab order: Tab from the menu trigger lands on ${palette?.description ?? "nothing"}, expected the palette's roving entry (guide \xA75.14)`
|
|
2039
|
+
);
|
|
2040
|
+
} else if (canvas?.kind !== "canvas") {
|
|
2041
|
+
problems.push(
|
|
2042
|
+
`tab order: Tab from the palette lands on ${canvas?.description ?? "nothing"}, expected the canvas <svg> (an unexpected tab stop, guide \xA75.14)`
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
return problems;
|
|
2046
|
+
}
|
|
2047
|
+
function paletteShortcutProblems(entries) {
|
|
2048
|
+
return entries.filter((entry) => entry.chip && !entry.keyShortcuts).map(
|
|
2049
|
+
(entry) => `palette: entry "${entry.action}" shows the shortcut "${entry.chip}" in its tooltip but has no aria-keyshortcuts (guide \xA75.6)`
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
function menuKeysAtDocumentProblems(pressed, documentKeys) {
|
|
2053
|
+
const reached = new Set(documentKeys.map((key) => key.key));
|
|
2054
|
+
return pressed.filter((key) => reached.has(key)).map(
|
|
2055
|
+
(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)`
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
function menuOpenProblems(facts, focus) {
|
|
2059
|
+
const problems = [];
|
|
2060
|
+
if (facts.expanded !== "true") {
|
|
2061
|
+
problems.push(`menu: aria-expanded is "${facts.expanded ?? ""}" while open, expected "true"`);
|
|
2062
|
+
}
|
|
2063
|
+
const menu = facts.menu;
|
|
2064
|
+
if (!menu) return [...problems, 'menu: no visible role="menu" after opening'];
|
|
2065
|
+
if (!attribute(menu, "aria-label")) problems.push("menu: the dropdown has no aria-label");
|
|
2066
|
+
if (facts.controls && attribute(menu, "id") !== facts.controls) {
|
|
2067
|
+
problems.push(
|
|
2068
|
+
`menu: the visible dropdown is #${attribute(menu, "id") ?? ""}, the trigger controls #${facts.controls}`
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
if (facts.items.length === 0) problems.push("menu: the dropdown has no menuitem");
|
|
2072
|
+
facts.items.forEach((item, index) => {
|
|
2073
|
+
const label = item.name || `item ${index + 1}`;
|
|
2074
|
+
if (!item.name) problems.push(`menu: ${label} has no accessible name`);
|
|
2075
|
+
if (item.role !== "menuitem" && item.checked !== "true" && item.checked !== "false") {
|
|
2076
|
+
problems.push(`menu: "${label}" (${item.role}) has aria-checked "${item.checked ?? ""}"`);
|
|
2077
|
+
}
|
|
2078
|
+
if (item.role === "menuitemradio" && !item.group) {
|
|
2079
|
+
problems.push(`menu: radio "${label}" is not in a labelled role="group"`);
|
|
2080
|
+
}
|
|
2081
|
+
if (item.haspopup === "dialog" && !item.name.endsWith("\u2026")) {
|
|
2082
|
+
problems.push(`menu: "${label}" opens a dialog, so its label ends with "\u2026" (U+2026)`);
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
if (focus && facts.items.length > 0) {
|
|
2086
|
+
const expected = focus === "first" ? 0 : facts.items.length - 1;
|
|
2087
|
+
if (facts.active !== expected) {
|
|
2088
|
+
problems.push(
|
|
2089
|
+
`menu: focus is on ${facts.active < 0 ? "no entry" : `entry ${facts.active + 1}`}, expected the ${focus} entry`
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
return problems;
|
|
2094
|
+
}
|
|
2095
|
+
function menuClosedProblems(facts, focusOnTrigger) {
|
|
2096
|
+
const problems = [];
|
|
2097
|
+
if (facts.expanded !== "false")
|
|
2098
|
+
problems.push(`menu: aria-expanded is "${facts.expanded ?? ""}" after closing`);
|
|
2099
|
+
if (facts.menu) problems.push("menu: the dropdown is still visible after Escape");
|
|
2100
|
+
if (focusOnTrigger && !facts.activeIsTrigger)
|
|
2101
|
+
problems.push("menu: Escape did not return the focus to the trigger");
|
|
2102
|
+
return problems;
|
|
2103
|
+
}
|
|
2104
|
+
function keyReachProblem(press, reach, host, seen) {
|
|
2105
|
+
const shouldPrevent = reach === "handled" || reach === "canvas";
|
|
2106
|
+
if (host === "ide" && reach !== "free") {
|
|
2107
|
+
if (seen.keys.length > 0) {
|
|
2108
|
+
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)`;
|
|
2109
|
+
}
|
|
2110
|
+
const atDocument = seen.documentKeys ?? [];
|
|
2111
|
+
const prevented2 = atDocument.length === 1 ? atDocument[0]?.defaultPrevented : void 0;
|
|
2112
|
+
if (prevented2 === true && !shouldPrevent) {
|
|
2113
|
+
return `${press} was prevented; the shell only stops it, so the field, dialog or browser still acts (guide \xA713.5)`;
|
|
2114
|
+
}
|
|
2115
|
+
if (prevented2 === false && shouldPrevent) {
|
|
2116
|
+
return `${press} was stopped but not prevented; a handled key also calls preventDefault`;
|
|
2117
|
+
}
|
|
2118
|
+
return void 0;
|
|
2119
|
+
}
|
|
2120
|
+
if (seen.keys.length === 0) {
|
|
2121
|
+
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`;
|
|
2122
|
+
}
|
|
2123
|
+
if (seen.keys.length > 1) return `${press} reached window ${seen.keys.length} times`;
|
|
2124
|
+
const prevented = seen.keys[0]?.defaultPrevented ?? false;
|
|
2125
|
+
if (prevented && !shouldPrevent) {
|
|
2126
|
+
return `${press} arrived prevented; ${reach === "free" ? "nothing may handle it" : "the browser, the field or the dialog must act"}`;
|
|
2127
|
+
}
|
|
2128
|
+
if (!prevented && shouldPrevent) {
|
|
2129
|
+
return `${press} arrived unprevented; a handled key calls preventDefault (the browser would zoom the page)`;
|
|
2130
|
+
}
|
|
2131
|
+
return void 0;
|
|
2132
|
+
}
|
|
2133
|
+
function expectedAppearance(requested, themes) {
|
|
2134
|
+
const fallback = themes[0];
|
|
2135
|
+
if (fallback === void 0) throw new Error("@miragon/modeler-shell conformance: harness.themes is empty");
|
|
2136
|
+
const theme = themes.includes(requested.theme) ? requested.theme : fallback;
|
|
2137
|
+
return requested.kontrast ? { theme, kontrast: requested.kontrast } : { theme };
|
|
2138
|
+
}
|
|
2139
|
+
var describeAppearance = (appearance) => appearance.kontrast ? `${appearance.theme} + ${appearance.kontrast}` : appearance.theme;
|
|
2140
|
+
function expectedEmbeddedAppearance(facts, themes) {
|
|
2141
|
+
const fallback = themes[0];
|
|
2142
|
+
if (fallback === void 0) throw new Error("@miragon/modeler-shell conformance: harness.themes is empty");
|
|
2143
|
+
const host = facts.themed.filter((element) => element.relation === "ancestor").at(-1);
|
|
2144
|
+
return host?.kontrast === "hoch" ? { theme: fallback, kontrast: "hoch" } : { theme: fallback };
|
|
2145
|
+
}
|
|
2146
|
+
function appearanceProblems(facts, host, requested, themes, web) {
|
|
2147
|
+
const ownDocument = host !== "embedded";
|
|
2148
|
+
const expected = web?.effective ?? (ownDocument ? expectedAppearance(requested, themes) : expectedEmbeddedAppearance(facts, themes));
|
|
2149
|
+
const carrierTheme = web?.carrier ?? expected.theme;
|
|
2150
|
+
const problems = [];
|
|
2151
|
+
const described = carrierTheme === expected.theme ? describeAppearance(expected) : `${carrierTheme} showing ${describeAppearance(expected)}`;
|
|
2152
|
+
const label = ownDocument ? `requested ${describeAppearance(requested)}, expected ${described}` : `host page ${describeAppearance(requested)}, expected ${described}`;
|
|
2153
|
+
const carrier = ownDocument ? facts.themed.find((element) => element.isDocument && element.theme !== null) : facts.themed.find((element) => element.relation === "self" && element.theme !== null);
|
|
2154
|
+
if (!carrier) {
|
|
2155
|
+
problems.push(
|
|
2156
|
+
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)`
|
|
2157
|
+
);
|
|
2158
|
+
} else {
|
|
2159
|
+
if (carrier.theme !== carrierTheme) {
|
|
2160
|
+
problems.push(`${carrier.description} has data-cd-theme="${carrier.theme}" (${label})`);
|
|
2161
|
+
}
|
|
2162
|
+
if ((carrier.kontrast ?? void 0) !== expected.kontrast) {
|
|
2163
|
+
problems.push(
|
|
2164
|
+
`${carrier.description} has ${carrier.kontrast ? `data-cd-kontrast="${carrier.kontrast}"` : "no data-cd-kontrast"} (${label})`
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
for (const element of facts.themed) {
|
|
2169
|
+
if (element === carrier) continue;
|
|
2170
|
+
const stray = ownDocument ? !element.isDocument || element.theme === null : element.relation === "descendant" || element.relation === "self";
|
|
2171
|
+
if (!stray) continue;
|
|
2172
|
+
const values = [
|
|
2173
|
+
element.theme !== null ? `data-cd-theme="${element.theme}"` : "",
|
|
2174
|
+
element.kontrast !== null ? `data-cd-kontrast="${element.kontrast}"` : ""
|
|
2175
|
+
].filter(Boolean).join(" ");
|
|
2176
|
+
const sameMode = carrier !== void 0 && element.theme === carrier.theme && element.kontrast === carrier.kontrast;
|
|
2177
|
+
problems.push(
|
|
2178
|
+
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)`
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
if (ownDocument && !facts.documentClasses.includes("cd-shell-document")) {
|
|
2182
|
+
problems.push(
|
|
2183
|
+
"<html> lacks cd-shell-document in an own document (the reset of the webview's host styles)"
|
|
2184
|
+
);
|
|
2185
|
+
}
|
|
2186
|
+
if (!ownDocument && facts.documentClasses.includes("cd-shell-document")) {
|
|
2187
|
+
problems.push(
|
|
2188
|
+
"<html> of a foreign page carries cd-shell-document: the own-document reset restyles the host page"
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
const reference = facts.parts[0];
|
|
2192
|
+
if (reference && !reference.roles["--cd-ui-text"]) {
|
|
2193
|
+
problems.push(
|
|
2194
|
+
"--cd-ui-text is empty at the shell root: the vendored cd-tokens.generated.css is not loaded"
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
for (const part of facts.parts) {
|
|
2198
|
+
const schemes = part.colorScheme.split(/\s+/);
|
|
2199
|
+
const dark = schemes.includes("dark");
|
|
2200
|
+
const light = schemes.includes("light");
|
|
2201
|
+
if (expected.theme === "light" && (dark || carrier && !light)) {
|
|
2202
|
+
problems.push(`${part.name}: color-scheme is "${part.colorScheme}" (${label})`);
|
|
2203
|
+
}
|
|
2204
|
+
if (expected.theme === "dark" && (!dark || light)) {
|
|
2205
|
+
problems.push(`${part.name}: color-scheme is "${part.colorScheme}" (${label})`);
|
|
2206
|
+
}
|
|
2207
|
+
if (reference && part !== reference) {
|
|
2208
|
+
for (const [role, value] of Object.entries(part.roles)) {
|
|
2209
|
+
if (value !== reference.roles[role]) {
|
|
2210
|
+
problems.push(
|
|
2211
|
+
`${part.name}: ${role} resolves to "${value}", the root to "${reference.roles[role] ?? ""}" (half-dark)`
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
for (const required of ["dropdown", "tooltip"]) {
|
|
2218
|
+
if (!facts.parts.some((part) => part.name === required)) {
|
|
2219
|
+
problems.push(`${required}: not visible when the kit opened it, so its mode was not checked`);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return { problems };
|
|
2223
|
+
}
|
|
2224
|
+
var SHELL_ROOT_ATTRIBUTES = [
|
|
2225
|
+
"data-cd-shell-host",
|
|
2226
|
+
"data-palette",
|
|
2227
|
+
"data-legend",
|
|
2228
|
+
"data-inspector",
|
|
2229
|
+
"data-problems",
|
|
2230
|
+
"data-readonly",
|
|
2231
|
+
"data-primary",
|
|
2232
|
+
"data-zoom-row",
|
|
2233
|
+
"data-empty-state",
|
|
2234
|
+
"data-drop"
|
|
2235
|
+
];
|
|
2236
|
+
var EMBEDDED_ROOT_ATTRIBUTES = ["data-cd-theme", "data-cd-kontrast"];
|
|
2237
|
+
var BENIGN_ERRORS = /ResizeObserver loop/;
|
|
2238
|
+
function leftoverProblems(facts, host) {
|
|
2239
|
+
const problems = [];
|
|
2240
|
+
for (const element of facts.classedElements) problems.push(`${element} is left after destroy`);
|
|
2241
|
+
const ownDocument = host !== "embedded";
|
|
2242
|
+
for (const name2 of facts.documentClasses) {
|
|
2243
|
+
if (ownDocument && name2 === "cd-shell-document") continue;
|
|
2244
|
+
problems.push(`<html> keeps the class ${name2} after destroy`);
|
|
2245
|
+
}
|
|
2246
|
+
for (const element of facts.chromeElements) problems.push(`${element} is left after destroy`);
|
|
2247
|
+
for (const name2 of facts.rootClasses) problems.push(`the root keeps the class ${name2} after destroy`);
|
|
2248
|
+
const written = ownDocument ? SHELL_ROOT_ATTRIBUTES : [...SHELL_ROOT_ATTRIBUTES, ...EMBEDDED_ROOT_ATTRIBUTES];
|
|
2249
|
+
for (const name2 of facts.rootAttributes) {
|
|
2250
|
+
if (written.includes(name2)) problems.push(`the root keeps ${name2} after destroy`);
|
|
2251
|
+
}
|
|
2252
|
+
for (const error of facts.errors) {
|
|
2253
|
+
if (!BENIGN_ERRORS.test(error)) problems.push(`the page reported an error after destroy: ${error}`);
|
|
2254
|
+
}
|
|
2255
|
+
return problems;
|
|
2256
|
+
}
|
|
2257
|
+
function afterDestroyKeyProblem(press, recorded) {
|
|
2258
|
+
if (recorded.length === 0)
|
|
2259
|
+
return `${press} after destroy never reached window: a shell listener still stops it`;
|
|
2260
|
+
if (recorded.some((key) => key.defaultPrevented)) {
|
|
2261
|
+
return `${press} after destroy arrived prevented: a shell listener is still installed`;
|
|
2262
|
+
}
|
|
2263
|
+
return void 0;
|
|
2264
|
+
}
|
|
2265
|
+
var ABSENT_ISLANDS = ["legend", "inspector", "problems", "status"];
|
|
2266
|
+
function absentIslands(absent) {
|
|
2267
|
+
return ABSENT_ISLANDS.filter((island) => absent?.includes(island));
|
|
2268
|
+
}
|
|
2269
|
+
function absentIslandProblems(visible, absent) {
|
|
2270
|
+
return absentIslands(absent).filter((island) => visible.has(island)).map((island) => `${island}: visible, but harness.absent says the modeler has no ${island}`);
|
|
2271
|
+
}
|
|
2272
|
+
function harnessHookProblems(harness, hooks) {
|
|
2273
|
+
return hooks.filter(
|
|
2274
|
+
(hook) => hook === "drop" ? harness.absent?.includes("drop") && harness.drop : hook !== "problemSelection" && harness.absent?.includes(hook) && harness.show?.[hook]
|
|
2275
|
+
).map(
|
|
2276
|
+
(hook) => hook === "drop" ? 'harness: absent lists "drop", but harness.drop is given' : `harness: absent lists "${hook}", but show.${hook} is given`
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
function surroundProblems(facts) {
|
|
2280
|
+
if (!facts.container) return ["canvas surround: no .djs-container in the root to check"];
|
|
2281
|
+
if (!facts.painted) {
|
|
2282
|
+
return [
|
|
2283
|
+
"canvas surround: nothing from .djs-container out to the root paints a background colour; the surround is var(--cd-ui-grund) (guide \xA710.1)"
|
|
2284
|
+
];
|
|
2285
|
+
}
|
|
2286
|
+
if (facts.painted.color === facts.grund) return [];
|
|
2287
|
+
return [
|
|
2288
|
+
`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)`
|
|
2289
|
+
];
|
|
2290
|
+
}
|
|
2291
|
+
function isToolEntry(entry) {
|
|
2292
|
+
return entry.group === "tools" || entry.action.endsWith("-tool");
|
|
2293
|
+
}
|
|
2294
|
+
function paletteEntryState(entry) {
|
|
2295
|
+
if (entry.disabled) return "aria-disabled";
|
|
2296
|
+
if (entry.hovered) return entry.pressed ? "pressed and hovered" : "hovered";
|
|
2297
|
+
return entry.pressed ? "pressed" : "at rest";
|
|
2298
|
+
}
|
|
2299
|
+
function paletteColorProblems(facts) {
|
|
2300
|
+
const problems = [];
|
|
2301
|
+
for (const entry of facts.entries) {
|
|
2302
|
+
if (!isToolEntry(entry)) continue;
|
|
2303
|
+
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];
|
|
2304
|
+
const state = paletteEntryState(entry);
|
|
2305
|
+
if (entry.color !== value) {
|
|
2306
|
+
problems.push(
|
|
2307
|
+
`palette: tool "${entry.action}" ${state} has color ${entry.color}, expected ${role} (${value}) (guide \xA75.5)`
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
if (entry.icon && entry.icon.color !== value) {
|
|
2311
|
+
const icon = entry.icon.kind === "svg" ? "<svg> icon" : "::before icon";
|
|
2312
|
+
problems.push(
|
|
2313
|
+
`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)`
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
return problems;
|
|
2318
|
+
}
|
|
2319
|
+
var round3 = (value) => Math.round(value * 10) / 10;
|
|
2320
|
+
function createProblems(facts) {
|
|
2321
|
+
const known = new Set(facts.before.map((shape2) => shape2.id));
|
|
2322
|
+
const added = facts.after.filter((shape2) => !known.has(shape2.id));
|
|
2323
|
+
const pressed = `Enter on palette entry "${facts.entry}"`;
|
|
2324
|
+
if (added.length === 0) {
|
|
2325
|
+
return [
|
|
2326
|
+
`create: ${pressed} added no element; Enter on a creating entry places it in the centre of the free rectangle (guide \xA75.5)`
|
|
2327
|
+
];
|
|
2328
|
+
}
|
|
2329
|
+
if (added.length > 1) {
|
|
2330
|
+
return [
|
|
2331
|
+
`create: ${pressed} added ${added.length} elements (${added.map((shape2) => shape2.id).join(", ")}), expected exactly one`
|
|
2332
|
+
];
|
|
2333
|
+
}
|
|
2334
|
+
const [shape] = added;
|
|
2335
|
+
const problems = [];
|
|
2336
|
+
if (!shape.selected) problems.push(`create: the new element "${shape.id}" is not selected (guide \xA75.5)`);
|
|
2337
|
+
if (!facts.canvas.focusInCanvas) {
|
|
2338
|
+
problems.push(
|
|
2339
|
+
`create: after ${pressed} the focus is on ${facts.canvas.focused}, expected the canvas (its <svg> or the label editor, guide \xA75.5)`
|
|
2340
|
+
);
|
|
2341
|
+
}
|
|
2342
|
+
const centre = { x: shape.box.left + shape.box.width / 2, y: shape.box.top + shape.box.height / 2 };
|
|
2343
|
+
const { free } = facts;
|
|
2344
|
+
const tolerance = GEOMETRY_TOLERANCE_PX + 1e-6;
|
|
2345
|
+
const inside = centre.x >= free.left - tolerance && centre.x <= free.right + tolerance && centre.y >= free.top - tolerance && centre.y <= free.bottom + tolerance;
|
|
2346
|
+
if (!inside) {
|
|
2347
|
+
problems.push(
|
|
2348
|
+
`create: the new element "${shape.id}" is centred at (${round3(centre.x)}, ${round3(centre.y)})px, outside the free rectangle ${round3(free.left)}\u2013${round3(free.right)} \xD7 ${round3(free.top)}\u2013${round3(free.bottom)}px (guide \xA75.5, \xA75.9)`
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
return problems;
|
|
2352
|
+
}
|
|
2353
|
+
function readOnlyStateProblems(facts) {
|
|
2354
|
+
const problems = [];
|
|
2355
|
+
const after = `${INVALID_STATUS_DELAY_MS}ms after show.invalid`;
|
|
2356
|
+
if (!facts.readOnly) problems.push(`read-only: the root has no data-readonly ${after}`);
|
|
2357
|
+
const status = facts.status;
|
|
2358
|
+
if (!status?.visible) {
|
|
2359
|
+
problems.push(`read-only: the status box is not visible ${after} (guide \xA75.12)`);
|
|
2360
|
+
} else if (status.attributes.role !== "alert") {
|
|
2361
|
+
problems.push(
|
|
2362
|
+
`read-only: the status box of an invalid file has role "${status.attributes.role ?? ""}", expected "alert" (guide \xA713.5)`
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
return problems;
|
|
2366
|
+
}
|
|
2367
|
+
var READ_ONLY_PALETTE_ACTIONS = ["lasso-tool", "hand-tool"];
|
|
2368
|
+
function readOnlyPaletteProblems(entries) {
|
|
2369
|
+
return entries.flatMap((entry) => {
|
|
2370
|
+
const staysActive = READ_ONLY_PALETTE_ACTIONS.includes(entry.action);
|
|
2371
|
+
if (staysActive && entry.disabled) {
|
|
2372
|
+
return [
|
|
2373
|
+
`palette: "${entry.action}" is aria-disabled on the read-only canvas; Lasso and Hand stay active (guide \xA713.5)`
|
|
2374
|
+
];
|
|
2375
|
+
}
|
|
2376
|
+
if (!staysActive && !entry.disabled) {
|
|
2377
|
+
return [`palette: "${entry.action}" is not aria-disabled on the read-only canvas (guide \xA713.5)`];
|
|
2378
|
+
}
|
|
2379
|
+
return [];
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
var LEGEND_MENU_ID = "legend";
|
|
2383
|
+
function readOnlyMenuProblems(before, after) {
|
|
2384
|
+
const problems = [];
|
|
2385
|
+
const key = (entry) => `${entry.menuId ?? ""}\0${entry.name}`;
|
|
2386
|
+
const enabledBefore = new Set(before.filter((entry) => !entry.disabled).map(key));
|
|
2387
|
+
for (const entry of after) {
|
|
2388
|
+
if (!entry.disabled) continue;
|
|
2389
|
+
if (entry.menuId === LEGEND_MENU_ID && entry.role === "menuitemcheckbox") {
|
|
2390
|
+
problems.push(
|
|
2391
|
+
"menu: the Legend toggle is aria-disabled on the read-only canvas; a view-only toggle stays usable (guide \xA713.5)"
|
|
2392
|
+
);
|
|
2393
|
+
} else if (entry.group !== "model" && enabledBefore.has(key(entry))) {
|
|
2394
|
+
problems.push(
|
|
2395
|
+
`menu: "${entry.name}" is aria-disabled on the read-only canvas; export and help stay usable (guide \xA713.5)`
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
return problems;
|
|
2400
|
+
}
|
|
2401
|
+
function lockedShapeProblems(facts) {
|
|
2402
|
+
const problems = [];
|
|
2403
|
+
const { before, after } = facts.dragged;
|
|
2404
|
+
if (facts.contextPadEntries.length > 0) {
|
|
2405
|
+
problems.push(
|
|
2406
|
+
`read-only: selecting "${before.id}" shows the context pad (${facts.contextPadEntries.join(", ")}) (guide \xA713.5)`
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
if (!after) {
|
|
2410
|
+
problems.push(`read-only: a 40px drag removed "${before.id}" from the canvas (guide \xA713.5)`);
|
|
2411
|
+
} 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) {
|
|
2412
|
+
problems.push(
|
|
2413
|
+
`read-only: a 40px drag changed "${before.id}" (transform "${before.transform}" \u2192 "${after.transform}", ${round3(before.size.width)} \xD7 ${round3(before.size.height)} \u2192 ${round3(after.size.width)} \xD7 ${round3(after.size.height)}) (guide \xA713.5)`
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
if (facts.textFields.length > 0) {
|
|
2417
|
+
problems.push(
|
|
2418
|
+
`read-only: a double-click on "${before.id}" opens a text field (${facts.textFields.join(", ")}) (guide \xA713.5)`
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
for (const key of ["Delete", "Backspace"]) {
|
|
2422
|
+
if (!facts.kept[key])
|
|
2423
|
+
problems.push(`read-only: ${key} on the canvas deleted "${before.id}" (guide \xA713.5)`);
|
|
2424
|
+
}
|
|
2425
|
+
return problems;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
// src/testing/case-tools.ts
|
|
2429
|
+
var DEFAULT_SIZES = [
|
|
2430
|
+
{ width: 1280, height: 900 },
|
|
2431
|
+
{ width: 720, height: 700 },
|
|
2432
|
+
{ width: 420, height: 700 }
|
|
2433
|
+
];
|
|
2434
|
+
var LIGHT = { theme: "light" };
|
|
2435
|
+
var SHOWN_ISLANDS = ["legend", "problems", "status", "inspector"];
|
|
2436
|
+
var POLL_MS = 50;
|
|
2437
|
+
var HOVER_MARGIN_MS = 200;
|
|
2438
|
+
var PARK = { x: 1, y: 1 };
|
|
2439
|
+
function sizeLabel(size) {
|
|
2440
|
+
return `${size.width} \xD7 ${size.height}`;
|
|
2441
|
+
}
|
|
2442
|
+
function fail(caseName, host, problems) {
|
|
2443
|
+
const unique = [...new Set(problems)];
|
|
2444
|
+
throw new Error(
|
|
2445
|
+
`@miragon/modeler-shell conformance: ${caseName} (${host} host)
|
|
2446
|
+
${unique.map((problem) => `- ${problem}`).join("\n")}`
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2449
|
+
function finish(caseName, host, problems, skipped) {
|
|
2450
|
+
if (problems.length > 0) fail(caseName, host, problems);
|
|
2451
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2452
|
+
const unique = [...skipped].filter((skip) => !seen.has(skip.reason) && seen.add(skip.reason));
|
|
2453
|
+
return unique.length > 0 ? { skipped: unique } : void 0;
|
|
2454
|
+
}
|
|
2455
|
+
function assertHooksConsistent(caseName, harness, hooks) {
|
|
2456
|
+
const problems = harnessHookProblems(harness, hooks);
|
|
2457
|
+
if (problems.length > 0) fail(caseName, harness.host, problems);
|
|
2458
|
+
}
|
|
2459
|
+
function unfed(harness, hook) {
|
|
2460
|
+
return !harness.show?.[hook] && !harness.absent?.includes(hook);
|
|
2461
|
+
}
|
|
2462
|
+
async function openHarness(page, harness, context) {
|
|
2463
|
+
if (harness.host === "web" && page.emulateMedia) {
|
|
2464
|
+
await page.emulateMedia({
|
|
2465
|
+
colorScheme: context.requested.theme,
|
|
2466
|
+
contrast: context.requested.kontrast === "hoch" ? "more" : "no-preference"
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
await harness.open(page, context);
|
|
2470
|
+
}
|
|
2471
|
+
function webHookSkips(harness, hooks) {
|
|
2472
|
+
return Object.entries(hooks).filter(([hook]) => hook === "toast" ? harness.host !== "ide" : harness.host === "web").filter(([hook]) => unfed(harness, hook)).map(([hook, what]) => ({ hook, reason: `show.${hook} is undefined: ${what} not verified` }));
|
|
2473
|
+
}
|
|
2474
|
+
async function pollUntil(page, read, done, timeoutMs) {
|
|
2475
|
+
let value = await read();
|
|
2476
|
+
for (let waited = 0; !done(value) && waited < timeoutMs; waited += POLL_MS) {
|
|
2477
|
+
await page.waitForTimeout(POLL_MS);
|
|
2478
|
+
value = await read();
|
|
2479
|
+
}
|
|
2480
|
+
return value;
|
|
2481
|
+
}
|
|
2482
|
+
async function hover(page, root, target) {
|
|
2483
|
+
const anchor = await findTarget(page, root, target);
|
|
2484
|
+
if (!anchor) return null;
|
|
2485
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
2486
|
+
await page.mouse.move(anchor.client.x, anchor.client.y);
|
|
2487
|
+
await page.waitForTimeout(TOOLTIP_DELAY_MS + HOVER_MARGIN_MS);
|
|
2488
|
+
await settle(page);
|
|
2489
|
+
return anchor;
|
|
2490
|
+
}
|
|
2491
|
+
async function openMenuByClick(page, root) {
|
|
2492
|
+
const trigger = await findTarget(page, root, "menu");
|
|
2493
|
+
if (!trigger) return false;
|
|
2494
|
+
await page.mouse.click(trigger.client.x, trigger.client.y);
|
|
2495
|
+
await settle(page);
|
|
2496
|
+
return true;
|
|
2497
|
+
}
|
|
2498
|
+
async function closeByEscape(page) {
|
|
2499
|
+
await page.keyboard.press("Escape");
|
|
2500
|
+
await settle(page);
|
|
2501
|
+
}
|
|
2502
|
+
async function menuEntries(page, root) {
|
|
2503
|
+
if (!await openMenuByClick(page, root)) return [];
|
|
2504
|
+
const entries = await inPage(page, menuEntriesProbe, { root }) ?? [];
|
|
2505
|
+
await closeByEscape(page);
|
|
2506
|
+
return entries;
|
|
2507
|
+
}
|
|
2508
|
+
function shapes(page, root) {
|
|
2509
|
+
return inPage(page, shapesProbe, { root });
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
// src/testing/web-facts.ts
|
|
2513
|
+
var webModeProbe = ({ key }) => {
|
|
2514
|
+
const html = document.documentElement;
|
|
2515
|
+
let stored = null;
|
|
2516
|
+
let storageBlocked = false;
|
|
2517
|
+
try {
|
|
2518
|
+
stored = localStorage.getItem(key);
|
|
2519
|
+
} catch {
|
|
2520
|
+
storageBlocked = true;
|
|
2521
|
+
}
|
|
2522
|
+
return {
|
|
2523
|
+
theme: html.getAttribute("data-cd-theme"),
|
|
2524
|
+
kontrast: html.getAttribute("data-cd-kontrast"),
|
|
2525
|
+
colorScheme: document.querySelector('meta[name="color-scheme"]')?.getAttribute("content") ?? null,
|
|
2526
|
+
themeColors: [...document.querySelectorAll('meta[name="theme-color"]')].map((meta) => ({
|
|
2527
|
+
content: meta.getAttribute("content") ?? "",
|
|
2528
|
+
media: meta.getAttribute("media")
|
|
2529
|
+
})),
|
|
2530
|
+
stored,
|
|
2531
|
+
storageBlocked,
|
|
2532
|
+
noTransition: html.classList.contains("cd-shell-no-transition")
|
|
2533
|
+
};
|
|
2534
|
+
};
|
|
2535
|
+
var watchModeProbe = (_) => {
|
|
2536
|
+
const tools = window.__modelerShellConformance;
|
|
2537
|
+
tools.modeWatch?.stop();
|
|
2538
|
+
const html = document.documentElement;
|
|
2539
|
+
const changes = [];
|
|
2540
|
+
let frame = 0;
|
|
2541
|
+
let running = true;
|
|
2542
|
+
const tick = () => {
|
|
2543
|
+
if (!running) return;
|
|
2544
|
+
frame += 1;
|
|
2545
|
+
requestAnimationFrame(tick);
|
|
2546
|
+
};
|
|
2547
|
+
requestAnimationFrame(tick);
|
|
2548
|
+
const observer = new MutationObserver((records) => {
|
|
2549
|
+
for (const record of records) {
|
|
2550
|
+
const target = record.target;
|
|
2551
|
+
if (target === html) {
|
|
2552
|
+
if (record.attributeName === "class") {
|
|
2553
|
+
const had = (record.oldValue ?? "").split(/\s+/).includes("cd-shell-no-transition");
|
|
2554
|
+
const has = html.classList.contains("cd-shell-no-transition");
|
|
2555
|
+
if (had !== has) changes.push({ part: "no-transition", value: has ? "on" : "off", frame });
|
|
2556
|
+
} else if (record.attributeName === "data-cd-theme") {
|
|
2557
|
+
changes.push({ part: "theme", value: html.getAttribute("data-cd-theme"), frame });
|
|
2558
|
+
} else if (record.attributeName === "data-cd-kontrast") {
|
|
2559
|
+
changes.push({ part: "kontrast", value: html.getAttribute("data-cd-kontrast"), frame });
|
|
2560
|
+
}
|
|
2561
|
+
} else if (target.localName === "meta") {
|
|
2562
|
+
const name2 = target.getAttribute("name");
|
|
2563
|
+
const media = target.getAttribute("media");
|
|
2564
|
+
changes.push({
|
|
2565
|
+
part: name2 === "color-scheme" ? "color-scheme" : "theme-color",
|
|
2566
|
+
value: `${target.getAttribute("content") ?? ""}${media ? ` ${media}` : ""}`,
|
|
2567
|
+
frame
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
});
|
|
2572
|
+
observer.observe(html, {
|
|
2573
|
+
attributes: true,
|
|
2574
|
+
attributeOldValue: true,
|
|
2575
|
+
attributeFilter: ["data-cd-theme", "data-cd-kontrast", "class"]
|
|
2576
|
+
});
|
|
2577
|
+
for (const meta of document.querySelectorAll('meta[name="color-scheme"], meta[name="theme-color"]')) {
|
|
2578
|
+
observer.observe(meta, { attributes: true, attributeFilter: ["content"] });
|
|
2579
|
+
}
|
|
2580
|
+
tools.modeWatch = {
|
|
2581
|
+
changes,
|
|
2582
|
+
stop() {
|
|
2583
|
+
running = false;
|
|
2584
|
+
observer.disconnect();
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
};
|
|
2588
|
+
var modeChangesProbe = (_) => {
|
|
2589
|
+
const tools = window.__modelerShellConformance;
|
|
2590
|
+
const watch = tools.modeWatch;
|
|
2591
|
+
if (!watch) return [];
|
|
2592
|
+
watch.stop();
|
|
2593
|
+
tools.modeWatch = void 0;
|
|
2594
|
+
return [...watch.changes];
|
|
2595
|
+
};
|
|
2596
|
+
var legalLineProbe = ({ root }) => {
|
|
2597
|
+
const tools = window.__modelerShellConformance;
|
|
2598
|
+
const rootElement = document.querySelector(root);
|
|
2599
|
+
if (!rootElement) return null;
|
|
2600
|
+
const legal = rootElement.querySelector('[data-chrome="legal"]');
|
|
2601
|
+
return {
|
|
2602
|
+
container: tools.container(rootElement),
|
|
2603
|
+
legal: legal && tools.visible(legal) ? tools.box(legal, rootElement) : null,
|
|
2604
|
+
links: [...legal?.querySelectorAll("a") ?? []].map((link) => ({
|
|
2605
|
+
text: (link.textContent ?? "").replace(/\s+/g, " ").trim(),
|
|
2606
|
+
box: tools.box(link, rootElement)
|
|
2607
|
+
}))
|
|
2608
|
+
};
|
|
2609
|
+
};
|
|
2610
|
+
var topRowProbe = ({ root }) => {
|
|
2611
|
+
const tools = window.__modelerShellConformance;
|
|
2612
|
+
const rootElement = document.querySelector(root);
|
|
2613
|
+
if (!rootElement) return null;
|
|
2614
|
+
const palette = rootElement.querySelector('[data-chrome="palette"]');
|
|
2615
|
+
const menu = rootElement.querySelector('[data-chrome="menu"]');
|
|
2616
|
+
const primary = rootElement.querySelector('[data-chrome="primary"]');
|
|
2617
|
+
const menuBox = menu && tools.visible(menu) ? tools.box(menu, rootElement) : null;
|
|
2618
|
+
let primaryFacts = null;
|
|
2619
|
+
if (primary && tools.visible(primary)) {
|
|
2620
|
+
const rect = primary.getBoundingClientRect();
|
|
2621
|
+
const labels = [...primary.querySelectorAll("span")].filter(
|
|
2622
|
+
(span) => tools.visible(span) && (span.textContent ?? "").trim() !== ""
|
|
2623
|
+
);
|
|
2624
|
+
primaryFacts = {
|
|
2625
|
+
box: tools.box(primary, rootElement),
|
|
2626
|
+
client: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
2627
|
+
collapsed: rootElement.getAttribute("data-primary") === "collapsed",
|
|
2628
|
+
ariaLabel: primary.getAttribute("aria-label"),
|
|
2629
|
+
describedBy: primary.getAttribute("aria-describedby"),
|
|
2630
|
+
label: labels.length > 0 ? labels.map((span) => (span.textContent ?? "").trim()).join(" ") : null
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
return {
|
|
2634
|
+
container: tools.container(rootElement),
|
|
2635
|
+
paletteRow: rootElement.getAttribute("data-palette") === "second-row" ? "second-row" : "top",
|
|
2636
|
+
paletteWidth: palette ? palette.offsetWidth - palette.clientWidth + palette.scrollWidth : null,
|
|
2637
|
+
menuRight: menuBox ? menuBox.left + menuBox.width : null,
|
|
2638
|
+
primary: primaryFacts
|
|
2639
|
+
};
|
|
2640
|
+
};
|
|
2641
|
+
var countClicksProbe = ({ root, selector }) => {
|
|
2642
|
+
const tools = window.__modelerShellConformance;
|
|
2643
|
+
const element = document.querySelector(root)?.querySelector(selector);
|
|
2644
|
+
if (!element) return false;
|
|
2645
|
+
tools.clicks = 0;
|
|
2646
|
+
const listener = () => {
|
|
2647
|
+
tools.clicks += 1;
|
|
2648
|
+
};
|
|
2649
|
+
element.addEventListener("click", listener, true);
|
|
2650
|
+
tools.cleanups.push(() => element.removeEventListener("click", listener, true));
|
|
2651
|
+
return true;
|
|
2652
|
+
};
|
|
2653
|
+
var clicksProbe = (_) => window.__modelerShellConformance.clicks;
|
|
2654
|
+
var emptyStateProbe = ({
|
|
2655
|
+
root,
|
|
2656
|
+
work
|
|
2657
|
+
}) => {
|
|
2658
|
+
const tools = window.__modelerShellConformance;
|
|
2659
|
+
const rootElement = document.querySelector(root);
|
|
2660
|
+
if (!rootElement) return null;
|
|
2661
|
+
const card = rootElement.querySelector('[data-chrome="empty-state"]');
|
|
2662
|
+
const image = card?.querySelector("img") ?? null;
|
|
2663
|
+
const heading = card?.querySelector("h1, h2, h3, h4, h5, h6") ?? null;
|
|
2664
|
+
const labelledBy = card?.getAttribute("aria-labelledby");
|
|
2665
|
+
const parts = work.map((name2) => [
|
|
2666
|
+
name2,
|
|
2667
|
+
rootElement.querySelector(`[data-chrome="${name2}"]`)
|
|
2668
|
+
]);
|
|
2669
|
+
parts.push(["hint", rootElement.querySelector(".cd-shell-hint")]);
|
|
2670
|
+
const legal = rootElement.querySelector('[data-chrome="legal"]');
|
|
2671
|
+
return {
|
|
2672
|
+
shown: rootElement.getAttribute("data-empty-state") === "shown",
|
|
2673
|
+
card: card ? {
|
|
2674
|
+
visible: tools.visible(card),
|
|
2675
|
+
box: tools.box(card, rootElement),
|
|
2676
|
+
tag: card.tagName.toLowerCase(),
|
|
2677
|
+
labelledBy: labelledBy ? labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim() ?? "").join(" ").trim() : null
|
|
2678
|
+
} : null,
|
|
2679
|
+
komet: image ? {
|
|
2680
|
+
tag: image.tagName.toLowerCase(),
|
|
2681
|
+
alt: image.getAttribute("alt"),
|
|
2682
|
+
width: image.getBoundingClientRect().width,
|
|
2683
|
+
height: image.getBoundingClientRect().height
|
|
2684
|
+
} : null,
|
|
2685
|
+
heading: heading ? { tag: heading.tagName.toLowerCase(), text: (heading.textContent ?? "").replace(/\s+/g, " ").trim() } : null,
|
|
2686
|
+
// The card's actions; a text link inside the hint paragraph is no action (guide §8).
|
|
2687
|
+
buttons: [...card?.querySelectorAll("button") ?? []].filter((button) => tools.visible(button) && !button.closest("p")).map((button) => {
|
|
2688
|
+
const rect = button.getBoundingClientRect();
|
|
2689
|
+
return {
|
|
2690
|
+
variant: button.getAttribute("data-variant"),
|
|
2691
|
+
name: (button.getAttribute("aria-label") ?? button.textContent ?? "").replace(/\s+/g, " ").trim(),
|
|
2692
|
+
height: rect.height,
|
|
2693
|
+
client: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
|
|
2694
|
+
};
|
|
2695
|
+
}),
|
|
2696
|
+
hint: [...card?.querySelectorAll("p") ?? []].map((paragraph) => (paragraph.textContent ?? "").trim()).filter(Boolean).at(-1) ?? null,
|
|
2697
|
+
work: parts.filter((part) => part[1] !== null).map(([name2, element]) => ({
|
|
2698
|
+
name: name2,
|
|
2699
|
+
visibility: getComputedStyle(element).visibility,
|
|
2700
|
+
visible: tools.visible(element)
|
|
2701
|
+
})),
|
|
2702
|
+
canvasInert: rootElement.querySelector(".cd-shell-canvas")?.hasAttribute("inert") ?? false,
|
|
2703
|
+
legalVisible: legal ? tools.visible(legal) : false
|
|
2704
|
+
};
|
|
2705
|
+
};
|
|
2706
|
+
var focusOwnerProbe = ({ root }) => {
|
|
2707
|
+
const tools = window.__modelerShellConformance;
|
|
2708
|
+
const active = document.activeElement;
|
|
2709
|
+
const rootElement = document.querySelector(root);
|
|
2710
|
+
if (!active || active === document.body || active === document.documentElement) {
|
|
2711
|
+
return { owner: "body", description: "<body>" };
|
|
2712
|
+
}
|
|
2713
|
+
const description = tools.describe(active);
|
|
2714
|
+
if (!rootElement?.contains(active)) return { owner: "outside", description };
|
|
2715
|
+
if (active.closest(".djs-container")) return { owner: "canvas", description };
|
|
2716
|
+
const island = active.closest("[data-chrome]")?.getAttribute("data-chrome");
|
|
2717
|
+
return { owner: island ?? "other", description };
|
|
2718
|
+
};
|
|
2719
|
+
var blurProbe = (_) => {
|
|
2720
|
+
document.activeElement?.blur?.();
|
|
2721
|
+
};
|
|
2722
|
+
var toastsProbe = ({ root }) => {
|
|
2723
|
+
const tools = window.__modelerShellConformance;
|
|
2724
|
+
const rootElement = document.querySelector(root);
|
|
2725
|
+
if (!rootElement) return null;
|
|
2726
|
+
const stack = rootElement.querySelector('[data-chrome="toasts"]');
|
|
2727
|
+
const open = (element) => {
|
|
2728
|
+
try {
|
|
2729
|
+
return element.matches(":popover-open");
|
|
2730
|
+
} catch {
|
|
2731
|
+
return false;
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
2734
|
+
const toasts2 = stack ? [...stack.querySelectorAll(":scope > [data-status]")].filter(
|
|
2735
|
+
(toast) => tools.visible(toast)
|
|
2736
|
+
) : [];
|
|
2737
|
+
const name2 = (element) => (element.getAttribute("aria-label") ?? element.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
2738
|
+
const centre = (element) => {
|
|
2739
|
+
const rect = element.getBoundingClientRect();
|
|
2740
|
+
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
2741
|
+
};
|
|
2742
|
+
return {
|
|
2743
|
+
stack: stack ? {
|
|
2744
|
+
visible: tools.visible(stack),
|
|
2745
|
+
box: tools.box(stack, rootElement),
|
|
2746
|
+
popover: stack.getAttribute("popover"),
|
|
2747
|
+
popoverOpen: open(stack)
|
|
2748
|
+
} : null,
|
|
2749
|
+
toasts: toasts2.map((toast) => {
|
|
2750
|
+
const point = centre(toast);
|
|
2751
|
+
const hit = document.elementFromPoint(point.x, point.y);
|
|
2752
|
+
return {
|
|
2753
|
+
text: (toast.textContent ?? "").replace(/\s+/g, " ").trim(),
|
|
2754
|
+
role: toast.getAttribute("role"),
|
|
2755
|
+
status: toast.getAttribute("data-status"),
|
|
2756
|
+
box: tools.box(toast, rootElement),
|
|
2757
|
+
client: point,
|
|
2758
|
+
buttons: [...toast.querySelectorAll("button")].filter((button) => tools.visible(button)).map((button) => ({
|
|
2759
|
+
name: name2(button),
|
|
2760
|
+
text: (button.textContent ?? "").replace(/\s+/g, " ").trim(),
|
|
2761
|
+
client: centre(button)
|
|
2762
|
+
})),
|
|
2763
|
+
topmost: hit !== null && toast.contains(hit),
|
|
2764
|
+
focused: toast.contains(document.activeElement)
|
|
2765
|
+
};
|
|
2766
|
+
}),
|
|
2767
|
+
strays: [...document.querySelectorAll(".cd-shell-toast")].filter((element) => !stack?.contains(element) && tools.visible(element)).map((element) => tools.describe(element)),
|
|
2768
|
+
live: [...rootElement.querySelectorAll(".cd-shell-live [role], .cd-shell-live [aria-live]")].map(
|
|
2769
|
+
(region) => ({
|
|
2770
|
+
role: region.getAttribute("role"),
|
|
2771
|
+
live: region.getAttribute("aria-live"),
|
|
2772
|
+
text: (region.textContent ?? "").replace(/\s+/g, " ").trim()
|
|
2773
|
+
})
|
|
2774
|
+
)
|
|
2775
|
+
};
|
|
2776
|
+
};
|
|
2777
|
+
var focusToastButtonProbe = ({
|
|
2778
|
+
root,
|
|
2779
|
+
text,
|
|
2780
|
+
name: name2
|
|
2781
|
+
}) => {
|
|
2782
|
+
const stack = document.querySelector(root)?.querySelector('[data-chrome="toasts"]');
|
|
2783
|
+
const toast = [...stack?.querySelectorAll(":scope > [data-status]") ?? []].find(
|
|
2784
|
+
(candidate) => (candidate.textContent ?? "").includes(text)
|
|
2785
|
+
);
|
|
2786
|
+
const button = [...toast?.querySelectorAll("button") ?? []].find(
|
|
2787
|
+
(candidate) => (candidate.getAttribute("aria-label") ?? candidate.textContent ?? "").trim() === name2
|
|
2788
|
+
);
|
|
2789
|
+
if (!button) return false;
|
|
2790
|
+
button.focus();
|
|
2791
|
+
return document.activeElement === button;
|
|
2792
|
+
};
|
|
2793
|
+
var watchToastStackProbe = ({ root }) => {
|
|
2794
|
+
const tools = window.__modelerShellConformance;
|
|
2795
|
+
const stack = document.querySelector(root)?.querySelector('[data-chrome="toasts"]');
|
|
2796
|
+
if (!stack) return false;
|
|
2797
|
+
const seen = [];
|
|
2798
|
+
const listener = (event) => seen.push(event.newState);
|
|
2799
|
+
stack.addEventListener("beforetoggle", listener);
|
|
2800
|
+
tools.cleanups.push(() => stack.removeEventListener("beforetoggle", listener));
|
|
2801
|
+
tools.toastToggles = seen;
|
|
2802
|
+
return true;
|
|
2803
|
+
};
|
|
2804
|
+
var toastStackTogglesProbe = ({ root }) => {
|
|
2805
|
+
const tools = window.__modelerShellConformance;
|
|
2806
|
+
const stack = document.querySelector(root)?.querySelector('[data-chrome="toasts"]');
|
|
2807
|
+
let open = false;
|
|
2808
|
+
try {
|
|
2809
|
+
open = stack?.matches(":popover-open") ?? false;
|
|
2810
|
+
} catch {
|
|
2811
|
+
open = false;
|
|
2812
|
+
}
|
|
2813
|
+
return { toggles: [...tools.toastToggles ?? []], open };
|
|
2814
|
+
};
|
|
2815
|
+
var showModalProbe = (_) => {
|
|
2816
|
+
const tools = window.__modelerShellConformance;
|
|
2817
|
+
const dialog = document.createElement("dialog");
|
|
2818
|
+
dialog.setAttribute("data-modeler-shell-conformance", "show-modal");
|
|
2819
|
+
dialog.setAttribute("aria-label", "Conformance stand-in");
|
|
2820
|
+
dialog.style.cssText = "width:60vw;height:60vh;max-width:none;max-height:none;";
|
|
2821
|
+
document.body.append(dialog);
|
|
2822
|
+
tools.injected.push(dialog);
|
|
2823
|
+
dialog.showModal();
|
|
2824
|
+
try {
|
|
2825
|
+
return dialog.matches(":modal");
|
|
2826
|
+
} catch {
|
|
2827
|
+
return dialog.open;
|
|
2828
|
+
}
|
|
2829
|
+
};
|
|
2830
|
+
var popoverProbe = ({ root }) => {
|
|
2831
|
+
const tools = window.__modelerShellConformance;
|
|
2832
|
+
const rootElement = document.querySelector(root);
|
|
2833
|
+
if (!rootElement) return null;
|
|
2834
|
+
const pill = rootElement.querySelector('[data-chrome="problems"]');
|
|
2835
|
+
const controls = pill?.getAttribute("aria-controls");
|
|
2836
|
+
const popover2 = (controls ? document.getElementById(controls) : null) ?? rootElement.querySelector('[data-chrome="problems-popover"]');
|
|
2837
|
+
const attributes = (element) => {
|
|
2838
|
+
const all = {};
|
|
2839
|
+
for (const name2 of element.getAttributeNames()) all[name2] = element.getAttribute(name2) ?? "";
|
|
2840
|
+
return all;
|
|
2841
|
+
};
|
|
2842
|
+
const labelledBy = popover2?.getAttribute("aria-labelledby");
|
|
2843
|
+
const active = document.activeElement;
|
|
2844
|
+
const rows = popover2 ? [...popover2.querySelectorAll(".cd-shell-problem")].filter((row) => tools.visible(row)) : [];
|
|
2845
|
+
const pillRect = pill?.getBoundingClientRect();
|
|
2846
|
+
return {
|
|
2847
|
+
pill: pill && pillRect ? {
|
|
2848
|
+
attributes: attributes(pill),
|
|
2849
|
+
box: tools.box(pill, rootElement),
|
|
2850
|
+
client: { x: pillRect.left + pillRect.width / 2, y: pillRect.top + pillRect.height / 2 },
|
|
2851
|
+
visible: tools.visible(pill)
|
|
2852
|
+
} : null,
|
|
2853
|
+
popover: popover2 ? {
|
|
2854
|
+
attributes: attributes(popover2),
|
|
2855
|
+
box: tools.box(popover2, rootElement),
|
|
2856
|
+
visible: tools.visible(popover2),
|
|
2857
|
+
title: labelledBy ? labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim() ?? "").join(" ").trim() : null
|
|
2858
|
+
} : null,
|
|
2859
|
+
rows: rows.map((row) => {
|
|
2860
|
+
const target = row.matches("button") ? row : row.querySelector("button");
|
|
2861
|
+
const rect = (target ?? row).getBoundingClientRect();
|
|
2862
|
+
return {
|
|
2863
|
+
button: target !== null,
|
|
2864
|
+
height: row.getBoundingClientRect().height,
|
|
2865
|
+
icon: row.querySelector("svg") !== null,
|
|
2866
|
+
text: (row.textContent ?? "").replace(/\s+/g, " ").trim(),
|
|
2867
|
+
client: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
|
|
2868
|
+
};
|
|
2869
|
+
}),
|
|
2870
|
+
focus: !active || active === document.body ? "body" : pill && active === pill ? "pill" : popover2?.contains(active) ? "popover" : active.closest(".djs-container") ? "canvas" : "other",
|
|
2871
|
+
focused: !active || active === document.body ? "<body>" : tools.describe(active),
|
|
2872
|
+
focusedRow: (() => {
|
|
2873
|
+
const index = active ? rows.findIndex((row2) => row2 === active || row2.contains(active)) : -1;
|
|
2874
|
+
const row = rows[index];
|
|
2875
|
+
if (!row) return null;
|
|
2876
|
+
let scroller = row.parentElement;
|
|
2877
|
+
while (scroller && scroller !== popover2 && scroller.scrollHeight <= scroller.clientHeight) {
|
|
2878
|
+
scroller = scroller.parentElement;
|
|
2879
|
+
}
|
|
2880
|
+
const view = (scroller ?? popover2 ?? row).getBoundingClientRect();
|
|
2881
|
+
const box = row.getBoundingClientRect();
|
|
2882
|
+
return { index, inView: box.top >= view.top - 1 && box.bottom <= view.bottom + 1 };
|
|
2883
|
+
})()
|
|
2884
|
+
};
|
|
2885
|
+
};
|
|
2886
|
+
var dragProbe = async (spec) => {
|
|
2887
|
+
const tools = window.__modelerShellConformance;
|
|
2888
|
+
const rootElement = document.querySelector(spec.root);
|
|
2889
|
+
if (!rootElement) return null;
|
|
2890
|
+
const transfer = new DataTransfer();
|
|
2891
|
+
for (const file of spec.files) {
|
|
2892
|
+
transfer.items.add(new File([file.text], file.name, { type: file.type }));
|
|
2893
|
+
}
|
|
2894
|
+
for (const text of spec.strings ?? []) transfer.items.add(text, "text/plain");
|
|
2895
|
+
const rect = rootElement.getBoundingClientRect();
|
|
2896
|
+
const clientX = rect.left + rect.width / 2;
|
|
2897
|
+
const clientY = rect.top + rect.height / 2;
|
|
2898
|
+
const target = document.elementFromPoint(clientX, clientY) ?? rootElement;
|
|
2899
|
+
const events = [];
|
|
2900
|
+
for (const type of spec.steps) {
|
|
2901
|
+
const event = new DragEvent(type, {
|
|
2902
|
+
bubbles: true,
|
|
2903
|
+
cancelable: true,
|
|
2904
|
+
composed: true,
|
|
2905
|
+
dataTransfer: transfer,
|
|
2906
|
+
clientX,
|
|
2907
|
+
clientY
|
|
2908
|
+
});
|
|
2909
|
+
const effects = { dropEffect: "none", effectAllowed: "all" };
|
|
2910
|
+
const recorded = new Proxy(transfer, {
|
|
2911
|
+
get(target2, key) {
|
|
2912
|
+
if (key === "dropEffect" || key === "effectAllowed") return effects[key];
|
|
2913
|
+
const value = Reflect.get(target2, key, target2);
|
|
2914
|
+
return typeof value === "function" ? value.bind(target2) : value;
|
|
2915
|
+
},
|
|
2916
|
+
set(target2, key, value) {
|
|
2917
|
+
if (key === "dropEffect" || key === "effectAllowed") {
|
|
2918
|
+
effects[key] = String(value);
|
|
2919
|
+
return true;
|
|
2920
|
+
}
|
|
2921
|
+
return Reflect.set(target2, key, value, target2);
|
|
2922
|
+
}
|
|
2923
|
+
});
|
|
2924
|
+
Object.defineProperty(event, "dataTransfer", { configurable: true, value: recorded });
|
|
2925
|
+
target.dispatchEvent(event);
|
|
2926
|
+
events.push({ type, defaultPrevented: event.defaultPrevented, dropEffect: effects.dropEffect ?? "none" });
|
|
2927
|
+
}
|
|
2928
|
+
await tools.settle();
|
|
2929
|
+
const resolve = (role) => {
|
|
2930
|
+
const probe = document.createElement("span");
|
|
2931
|
+
probe.setAttribute("data-modeler-shell-conformance", "role-probe");
|
|
2932
|
+
probe.style.cssText = `position:absolute;visibility:hidden;background-color:var(${role});`;
|
|
2933
|
+
rootElement.append(probe);
|
|
2934
|
+
const color = getComputedStyle(probe).backgroundColor;
|
|
2935
|
+
probe.remove();
|
|
2936
|
+
return color;
|
|
2937
|
+
};
|
|
2938
|
+
const resolveColor = (role) => {
|
|
2939
|
+
const probe = document.createElement("span");
|
|
2940
|
+
probe.setAttribute("data-modeler-shell-conformance", "role-probe");
|
|
2941
|
+
probe.style.cssText = `position:absolute;visibility:hidden;color:var(${role});`;
|
|
2942
|
+
rootElement.append(probe);
|
|
2943
|
+
const color = getComputedStyle(probe).color;
|
|
2944
|
+
probe.remove();
|
|
2945
|
+
return color;
|
|
2946
|
+
};
|
|
2947
|
+
const zone = rootElement.querySelector('[data-chrome="drop-zone"]');
|
|
2948
|
+
const chip = zone?.querySelector(".cd-shell-drop-zone-chip") ?? null;
|
|
2949
|
+
const zoneStyle = zone ? getComputedStyle(zone) : null;
|
|
2950
|
+
return {
|
|
2951
|
+
state: rootElement.getAttribute("data-drop"),
|
|
2952
|
+
events,
|
|
2953
|
+
zone: zone && zoneStyle ? {
|
|
2954
|
+
visible: tools.visible(zone),
|
|
2955
|
+
box: tools.box(zone, rootElement),
|
|
2956
|
+
ariaHidden: zone.getAttribute("aria-hidden"),
|
|
2957
|
+
background: zoneStyle.backgroundColor,
|
|
2958
|
+
borderStyle: zoneStyle.borderTopStyle,
|
|
2959
|
+
borderWidth: zoneStyle.borderTopWidth,
|
|
2960
|
+
borderColor: zoneStyle.borderTopColor,
|
|
2961
|
+
radius: zoneStyle.borderTopLeftRadius,
|
|
2962
|
+
cursor: zoneStyle.cursor
|
|
2963
|
+
} : null,
|
|
2964
|
+
chip: chip ? {
|
|
2965
|
+
text: (chip.textContent ?? "").replace(/\s+/g, " ").trim(),
|
|
2966
|
+
background: getComputedStyle(chip).backgroundColor
|
|
2967
|
+
} : null,
|
|
2968
|
+
roles: {
|
|
2969
|
+
hover: resolve("--cd-ui-hover"),
|
|
2970
|
+
interaktion: resolveColor("--cd-ui-interaktion"),
|
|
2971
|
+
dangerFlaeche: resolve("--cd-ui-danger-flaeche"),
|
|
2972
|
+
danger: resolveColor("--cd-ui-danger"),
|
|
2973
|
+
flaecheHoch: resolve("--cd-ui-flaeche-hoch")
|
|
2974
|
+
}
|
|
2975
|
+
};
|
|
2976
|
+
};
|
|
2977
|
+
|
|
2978
|
+
// src/testing/web-cases.ts
|
|
2979
|
+
var WIDEST_STEP = 1280;
|
|
2980
|
+
var NARROWEST_STEP = 400;
|
|
2981
|
+
var PHONE_WIDTH = 320;
|
|
2982
|
+
var STEP_PX = 24;
|
|
2983
|
+
var TOAST_MARGIN_MS = 900;
|
|
2984
|
+
var TOAST_RESUME_MS = 1500;
|
|
2985
|
+
var TOAST_ANNOUNCE_WAIT_MS = 1e3;
|
|
2986
|
+
var DROP_WAIT_MS = 2e3;
|
|
2987
|
+
var START_SCREEN_TABS = 10;
|
|
2988
|
+
var TOAST_STATUSES = ["success", "info", "warning", "danger"];
|
|
2989
|
+
var documentLangProbe = (_) => document.documentElement.lang;
|
|
2990
|
+
function sizeOf(harness) {
|
|
2991
|
+
return (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
|
|
2992
|
+
}
|
|
2993
|
+
function skipFor(hook, what) {
|
|
2994
|
+
return {
|
|
2995
|
+
skipped: [
|
|
2996
|
+
{
|
|
2997
|
+
hook,
|
|
2998
|
+
reason: `${hook === "drop" ? "harness.drop" : `show.${hook}`} is undefined: ${what} not verified`
|
|
2999
|
+
}
|
|
3000
|
+
]
|
|
3001
|
+
};
|
|
3002
|
+
}
|
|
3003
|
+
async function aria(page, root) {
|
|
3004
|
+
return inPage(page, ariaProbe, { root });
|
|
3005
|
+
}
|
|
3006
|
+
async function rowSamples(page, root, height) {
|
|
3007
|
+
const samples = [];
|
|
3008
|
+
const measurements = [];
|
|
3009
|
+
for (let width = WIDEST_STEP; width >= NARROWEST_STEP; width -= STEP_PX) {
|
|
3010
|
+
await page.setViewportSize({ width, height });
|
|
3011
|
+
await settle(page);
|
|
3012
|
+
const sample = await inPage(page, topRowProbe, { root });
|
|
3013
|
+
if (sample) samples.push(sample);
|
|
3014
|
+
measurements.push(await measureChrome(page, root));
|
|
3015
|
+
}
|
|
3016
|
+
return { samples, measurements };
|
|
3017
|
+
}
|
|
3018
|
+
async function primaryTooltipAt(page, root, width, height) {
|
|
3019
|
+
await page.setViewportSize({ width, height });
|
|
3020
|
+
await settle(page);
|
|
3021
|
+
const primary = await findTarget(page, root, "primary");
|
|
3022
|
+
if (!primary) return null;
|
|
3023
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
3024
|
+
await page.mouse.move(primary.client.x, primary.client.y);
|
|
3025
|
+
await page.waitForTimeout(TOOLTIP_DELAY_MS + HOVER_MARGIN_MS);
|
|
3026
|
+
await settle(page);
|
|
3027
|
+
const tooltip = await findOverlay(page, root, "tooltip");
|
|
3028
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
3029
|
+
return tooltip?.text ?? null;
|
|
3030
|
+
}
|
|
3031
|
+
async function primaryActivation(page, root) {
|
|
3032
|
+
const primary = await findTarget(page, root, "primary");
|
|
3033
|
+
if (!primary) return [];
|
|
3034
|
+
await inPage(page, countClicksProbe, { root, selector: '[data-chrome="primary"]' });
|
|
3035
|
+
await page.mouse.click(primary.client.x, primary.client.y);
|
|
3036
|
+
await settle(page);
|
|
3037
|
+
const pointer = await inPage(page, clicksProbe, null);
|
|
3038
|
+
if (!await focusTarget(page, root, "primary")) {
|
|
3039
|
+
await inPage(page, removeInjectedProbe, null);
|
|
3040
|
+
return ["primary: the primary action cannot take the focus"];
|
|
3041
|
+
}
|
|
3042
|
+
await page.keyboard.press("Enter");
|
|
3043
|
+
await settle(page);
|
|
3044
|
+
const enter = await inPage(page, clicksProbe, null) - pointer;
|
|
3045
|
+
await inPage(page, removeInjectedProbe, null);
|
|
3046
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
3047
|
+
return primaryActivationProblems({ pointer, enter });
|
|
3048
|
+
}
|
|
3049
|
+
async function fileEntryProblems(page, root) {
|
|
3050
|
+
const entries = (await menuEntries(page, root)).filter((entry) => entry.group === "file");
|
|
3051
|
+
const problems = [];
|
|
3052
|
+
for (const entry of entries) {
|
|
3053
|
+
if (!entry.menuId || !await openMenuByClick(page, root)) continue;
|
|
3054
|
+
const item = await inPage(page, fileItemProbe, { root, menuId: entry.menuId });
|
|
3055
|
+
if (!item) {
|
|
3056
|
+
await closeByEscape(page);
|
|
3057
|
+
continue;
|
|
3058
|
+
}
|
|
3059
|
+
await page.mouse.click(item.x, item.y);
|
|
3060
|
+
await settle(page);
|
|
3061
|
+
const menu = await inPage(page, menuProbe, { root });
|
|
3062
|
+
if (menu?.menu || menu?.expanded === "true") {
|
|
3063
|
+
problems.push(
|
|
3064
|
+
`menu: File entry "${entry.name}" leaves the menu open; an action closes it first (guide \xA75.4)`
|
|
3065
|
+
);
|
|
3066
|
+
await closeByEscape(page);
|
|
3067
|
+
}
|
|
3068
|
+
if (entry.name.endsWith("\u2026")) await closeByEscape(page);
|
|
3069
|
+
}
|
|
3070
|
+
return problems;
|
|
3071
|
+
}
|
|
3072
|
+
var fileItemProbe = ({
|
|
3073
|
+
root,
|
|
3074
|
+
menuId
|
|
3075
|
+
}) => {
|
|
3076
|
+
const item = document.querySelector(root)?.querySelector(`[role="menu"] [data-menu-id="${menuId}"]`);
|
|
3077
|
+
if (!item) return null;
|
|
3078
|
+
const rect = item.getBoundingClientRect();
|
|
3079
|
+
return rect.width > 0 ? { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 } : null;
|
|
3080
|
+
};
|
|
3081
|
+
async function lockedFileProblems(page, harness, root) {
|
|
3082
|
+
const invalid = harness.show?.invalid;
|
|
3083
|
+
if (!invalid) return [];
|
|
3084
|
+
await invalid(page);
|
|
3085
|
+
await page.waitForTimeout(INVALID_STATUS_DELAY_MS + HOVER_MARGIN_MS);
|
|
3086
|
+
return (await menuEntries(page, root)).filter((entry) => entry.group === "file" && entry.disabled).map(
|
|
3087
|
+
(entry) => `menu: File entry "${entry.name}" is aria-disabled on the read-only canvas (guide \xA713.5)`
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
var webChromeCase = {
|
|
3091
|
+
name: "webChrome",
|
|
3092
|
+
async run(page, harness) {
|
|
3093
|
+
const root = harness.root ?? DEFAULT_ROOT;
|
|
3094
|
+
const size = sizeOf(harness);
|
|
3095
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
3096
|
+
await settle(page);
|
|
3097
|
+
const problems = [];
|
|
3098
|
+
if (harness.host !== "web") {
|
|
3099
|
+
const measurement = await measureChrome(page, root);
|
|
3100
|
+
for (const island of ["primary", "legal", "empty-state", "drop-zone"]) {
|
|
3101
|
+
if (measurement.islands[island]) {
|
|
3102
|
+
problems.push(
|
|
3103
|
+
`${island}: visible in the ${harness.host} host; it belongs to the web app (guide \xA75.2)`
|
|
3104
|
+
);
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
return finish("webChrome", harness.host, problems, []);
|
|
3108
|
+
}
|
|
3109
|
+
const facts = await aria(page, root);
|
|
3110
|
+
if (facts) problems.push(...legalContentProblems(facts.legalLinks));
|
|
3111
|
+
const name2 = (facts?.primary?.attributes["aria-label"] ?? "").trim();
|
|
3112
|
+
problems.push(...await primaryActivation(page, root));
|
|
3113
|
+
const { samples, measurements } = await rowSamples(page, root, size.height);
|
|
3114
|
+
for (const measurement of measurements) {
|
|
3115
|
+
problems.push(
|
|
3116
|
+
...webRowProblems(measurement, "web").map(
|
|
3117
|
+
(problem) => `[${Math.round(measurement.container.width)}px wide] ${problem}`
|
|
3118
|
+
),
|
|
3119
|
+
...zoomRowBottomProblems(
|
|
3120
|
+
measurement.islands.zoom,
|
|
3121
|
+
measurement.open.zoomRaised,
|
|
3122
|
+
measurement.container,
|
|
3123
|
+
measurement.islands.legal?.height
|
|
3124
|
+
)
|
|
3125
|
+
);
|
|
3126
|
+
}
|
|
3127
|
+
if (samples.some((sample) => sample.primary)) {
|
|
3128
|
+
const withLabel = labelledWidth(samples);
|
|
3129
|
+
if (withLabel === void 0) {
|
|
3130
|
+
problems.push(
|
|
3131
|
+
`primary: never shows its label between ${WIDEST_STEP}px and ${NARROWEST_STEP}px; it collapses only when the top row gets tight (guide \xA75.15)`
|
|
3132
|
+
);
|
|
3133
|
+
} else {
|
|
3134
|
+
problems.push(...topRowProblems(samples, withLabel, name2));
|
|
3135
|
+
}
|
|
3136
|
+
const collapsed = samples.find((sample) => sample.primary?.collapsed);
|
|
3137
|
+
const labelled = samples.find((sample) => sample.primary && !sample.primary.collapsed);
|
|
3138
|
+
for (const sample of [labelled, collapsed]) {
|
|
3139
|
+
if (!sample?.primary) continue;
|
|
3140
|
+
const width = sample.container.width;
|
|
3141
|
+
const tooltip = await primaryTooltipAt(page, root, width, size.height);
|
|
3142
|
+
problems.push(...primaryTooltipProblems(sample.primary.collapsed, tooltip, name2, width));
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
await page.setViewportSize({ width: PHONE_WIDTH, height: size.height });
|
|
3146
|
+
await settle(page);
|
|
3147
|
+
const legalLine = await inPage(page, legalLineProbe, { root });
|
|
3148
|
+
if (legalLine) problems.push(...legalReachProblems(legalLine));
|
|
3149
|
+
await page.setViewportSize({ width: size.width, height: size.height });
|
|
3150
|
+
await settle(page);
|
|
3151
|
+
problems.push(...await fileEntryProblems(page, root));
|
|
3152
|
+
problems.push(...await lockedFileProblems(page, harness, root));
|
|
3153
|
+
return finish("webChrome", harness.host, [...new Set(problems)], []);
|
|
3154
|
+
}
|
|
3155
|
+
};
|
|
3156
|
+
function startScreen(page, root) {
|
|
3157
|
+
return inPage(page, emptyStateProbe, { root, work: WORK_CHROME });
|
|
3158
|
+
}
|
|
3159
|
+
async function toastFocusOnStartScreen(page, root, show) {
|
|
3160
|
+
const text = "Conformance: a toast on the start screen";
|
|
3161
|
+
await show(page, { status: "danger", text });
|
|
3162
|
+
await settle(page);
|
|
3163
|
+
const closeName = (await toasts(page, root))?.toasts.find((toast) => toast.text.includes(text))?.buttons.at(-1)?.name;
|
|
3164
|
+
if (!closeName || !await inPage(page, focusToastButtonProbe, { root, text, name: closeName })) return [];
|
|
3165
|
+
await page.keyboard.press("Enter");
|
|
3166
|
+
await settle(page);
|
|
3167
|
+
const focus = await inPage(page, focusOwnerProbe, { root });
|
|
3168
|
+
return focus.owner === "empty-state" ? [] : [
|
|
3169
|
+
`empty-state: closing a focused toast on the start screen leaves the focus on ${focus.description}; it goes to the card's "New diagram" (WCAG 2.4.3)`
|
|
3170
|
+
];
|
|
3171
|
+
}
|
|
3172
|
+
var emptyStateCase = {
|
|
3173
|
+
name: "emptyState",
|
|
3174
|
+
async run(page, harness) {
|
|
3175
|
+
assertHooksConsistent("emptyState", harness, ["emptyState"]);
|
|
3176
|
+
const root = harness.root ?? DEFAULT_ROOT;
|
|
3177
|
+
const size = sizeOf(harness);
|
|
3178
|
+
const show = harness.show?.emptyState;
|
|
3179
|
+
if (harness.host !== "web") {
|
|
3180
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
3181
|
+
if (show) await show(page);
|
|
3182
|
+
await settle(page);
|
|
3183
|
+
const facts2 = await startScreen(page, root);
|
|
3184
|
+
const problems2 = [];
|
|
3185
|
+
if (facts2?.card?.visible || facts2?.shown) {
|
|
3186
|
+
problems2.push(
|
|
3187
|
+
`empty-state: a start screen in the ${harness.host} host; it shows the empty-file hint instead (guide \xA713.5)`
|
|
3188
|
+
);
|
|
3189
|
+
}
|
|
3190
|
+
return finish("emptyState", harness.host, problems2, []);
|
|
3191
|
+
}
|
|
3192
|
+
if (!show) {
|
|
3193
|
+
return unfed(harness, "emptyState") ? skipFor("emptyState", "the start screen") : void 0;
|
|
3194
|
+
}
|
|
3195
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
3196
|
+
await settle(page);
|
|
3197
|
+
const lang = await inPage(page, documentLangProbe, null);
|
|
3198
|
+
await show(page);
|
|
3199
|
+
await settle(page);
|
|
3200
|
+
const container = (await measureChrome(page, root)).container;
|
|
3201
|
+
const facts = await startScreen(page, root);
|
|
3202
|
+
if (!facts) return finish("emptyState", harness.host, [`no element matches "${root}"`], []);
|
|
3203
|
+
const problems = emptyStateProblems(facts, container, lang);
|
|
3204
|
+
await inPage(page, blurProbe, null);
|
|
3205
|
+
const stops = [];
|
|
3206
|
+
for (let press = 0; press < START_SCREEN_TABS; press++) {
|
|
3207
|
+
await page.keyboard.press("Tab");
|
|
3208
|
+
stops.push(await inPage(page, focusOwnerProbe, { root }));
|
|
3209
|
+
}
|
|
3210
|
+
problems.push(...emptyStateTabProblems(stops));
|
|
3211
|
+
await inPage(page, blurProbe, null);
|
|
3212
|
+
for (const key of ["Control+Equal", "Control+Minus", "Shift+Digit1"]) {
|
|
3213
|
+
await inPage(page, clearKeysProbe, null);
|
|
3214
|
+
const before = (await inPage(page, afterPressProbe, { root })).zoom;
|
|
3215
|
+
await page.keyboard.press(key);
|
|
3216
|
+
const after = await inPage(page, afterPressProbe, { root });
|
|
3217
|
+
problems.push(...emptyStateKeyProblems(key, before, after.zoom, after.keys));
|
|
3218
|
+
}
|
|
3219
|
+
const showToast = harness.show?.toast;
|
|
3220
|
+
if (showToast) problems.push(...await toastFocusOnStartScreen(page, root, showToast));
|
|
3221
|
+
const start = facts.buttons[0];
|
|
3222
|
+
if (start) {
|
|
3223
|
+
await page.mouse.click(start.client.x, start.client.y);
|
|
3224
|
+
await settle(page);
|
|
3225
|
+
const after = await pollUntil(
|
|
3226
|
+
page,
|
|
3227
|
+
() => startScreen(page, root),
|
|
3228
|
+
(state) => state === null || !state.shown,
|
|
3229
|
+
1e3
|
|
3230
|
+
);
|
|
3231
|
+
if (after) problems.push(...afterStartProblems(after, await inPage(page, focusOwnerProbe, { root })));
|
|
3232
|
+
}
|
|
3233
|
+
return finish("emptyState", harness.host, problems, []);
|
|
3234
|
+
}
|
|
3235
|
+
};
|
|
3236
|
+
function toasts(page, root) {
|
|
3237
|
+
return inPage(page, toastsProbe, { root });
|
|
3238
|
+
}
|
|
3239
|
+
async function dismissAll(page, root) {
|
|
3240
|
+
for (let attempt = 0; attempt < 6; attempt++) {
|
|
3241
|
+
const close = (await toasts(page, root))?.toasts[0]?.buttons.at(-1);
|
|
3242
|
+
if (!close) break;
|
|
3243
|
+
await page.mouse.click(close.client.x, close.client.y);
|
|
3244
|
+
await settle(page);
|
|
3245
|
+
}
|
|
3246
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
3247
|
+
}
|
|
3248
|
+
async function presentAt(page, root, since, ms, texts) {
|
|
3249
|
+
const wait = since + ms - Date.now();
|
|
3250
|
+
if (wait > 0) await page.waitForTimeout(wait);
|
|
3251
|
+
await settle(page);
|
|
3252
|
+
const shown = (await toasts(page, root))?.toasts ?? [];
|
|
3253
|
+
return new Set(texts.filter((text) => shown.some((toast) => toast.text.includes(text))));
|
|
3254
|
+
}
|
|
3255
|
+
async function toastAnatomy(page, root, show, lang) {
|
|
3256
|
+
const problems = [];
|
|
3257
|
+
const before = (await toasts(page, root))?.live ?? [];
|
|
3258
|
+
for (const status of TOAST_STATUSES) {
|
|
3259
|
+
const text = `Conformance: a ${status} toast`;
|
|
3260
|
+
await show(page, { status, text });
|
|
3261
|
+
await settle(page);
|
|
3262
|
+
const toast = (await toasts(page, root))?.toasts.find((candidate) => candidate.text.includes(text));
|
|
3263
|
+
if (!toast) {
|
|
3264
|
+
problems.push(`toasts: show.toast with a ${status} toast shows nothing`);
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3267
|
+
const persistent = toastLiveRegion(before, status) !== void 0;
|
|
3268
|
+
const reached = (facts) => toastLiveRegion(facts?.live ?? [], status)?.text.includes(text) ?? false;
|
|
3269
|
+
const announced = persistent && reached(await pollUntil(page, () => toasts(page, root), reached, TOAST_ANNOUNCE_WAIT_MS));
|
|
3270
|
+
problems.push(...toastAnatomyProblems(toast, status, lang, { persistent, announced }));
|
|
3271
|
+
const close = toast.buttons.at(-1);
|
|
3272
|
+
if (close) {
|
|
3273
|
+
await page.mouse.click(close.client.x, close.client.y);
|
|
3274
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
3275
|
+
await settle(page);
|
|
3276
|
+
const still = (await toasts(page, root))?.toasts.some((candidate) => candidate.text.includes(text));
|
|
3277
|
+
if (still) problems.push(`toasts: Close does not close the ${status} toast`);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
await dismissAll(page, root);
|
|
3281
|
+
return problems;
|
|
3282
|
+
}
|
|
3283
|
+
async function toastStack(page, root, show) {
|
|
3284
|
+
const texts = ["A", "B", "C", "D"].map((letter) => `Conformance: stacked ${letter}`);
|
|
3285
|
+
const [first, second, third, fourth] = texts;
|
|
3286
|
+
await show(page, { status: "info", text: first, actionLabel: "Undo" });
|
|
3287
|
+
for (const text of [second, third, fourth]) await show(page, { status: "success", text });
|
|
3288
|
+
await settle(page);
|
|
3289
|
+
const problems = toastStackProblems((await toasts(page, root))?.toasts ?? [], texts, [
|
|
3290
|
+
first,
|
|
3291
|
+
third,
|
|
3292
|
+
fourth
|
|
3293
|
+
]);
|
|
3294
|
+
await show(page, { status: "success", text: third });
|
|
3295
|
+
await settle(page);
|
|
3296
|
+
problems.push(...repeatedToastProblems((await toasts(page, root))?.toasts ?? [], third));
|
|
3297
|
+
await dismissAll(page, root);
|
|
1416
3298
|
return problems;
|
|
1417
3299
|
}
|
|
1418
|
-
function
|
|
3300
|
+
async function toastTiming(page, root, show) {
|
|
3301
|
+
const lifetime = TOAST_DURATION_MS.success;
|
|
3302
|
+
const plain = "Conformance: a plain toast";
|
|
3303
|
+
const hovered = "Conformance: a hovered toast";
|
|
3304
|
+
const focused = "Conformance: a focused toast";
|
|
3305
|
+
const texts = [plain, hovered, focused];
|
|
3306
|
+
for (const text of texts) await show(page, { status: "success", text });
|
|
3307
|
+
const shownAt = Date.now();
|
|
3308
|
+
await settle(page);
|
|
3309
|
+
const target = (await toasts(page, root))?.toasts.find((toast) => toast.text.includes(hovered));
|
|
3310
|
+
if (target) await page.mouse.move(target.client.x, target.client.y);
|
|
3311
|
+
const focusTarget2 = (await toasts(page, root))?.toasts.find((toast) => toast.text.includes(focused));
|
|
3312
|
+
const closeName = focusTarget2?.buttons.at(-1)?.name ?? "";
|
|
3313
|
+
await inPage(page, focusToastButtonProbe, { root, text: focused, name: closeName });
|
|
3314
|
+
const timings = [];
|
|
3315
|
+
const record = (shown, after, expect) => {
|
|
3316
|
+
for (const [text, expectation, condition] of expect) {
|
|
3317
|
+
timings.push({ text, expectation, after, present: shown.has(text), condition });
|
|
3318
|
+
}
|
|
3319
|
+
};
|
|
3320
|
+
const early = lifetime - 800;
|
|
3321
|
+
record(await presentAt(page, root, shownAt, early, texts), early, [[plain, "present", "plain"]]);
|
|
3322
|
+
const late = lifetime + TOAST_MARGIN_MS;
|
|
3323
|
+
record(await presentAt(page, root, shownAt, late, texts), late, [
|
|
3324
|
+
[plain, "gone", "plain"],
|
|
3325
|
+
[hovered, "present", "hovered"],
|
|
3326
|
+
[focused, "present", "focused"]
|
|
3327
|
+
]);
|
|
3328
|
+
await page.mouse.move(PARK.x, PARK.y);
|
|
3329
|
+
await inPage(page, blurProbe, null);
|
|
3330
|
+
const leftAt = Date.now();
|
|
3331
|
+
const resumed = TOAST_RESUME_MS - 500;
|
|
3332
|
+
record(await presentAt(page, root, leftAt, resumed, texts), resumed, [
|
|
3333
|
+
[hovered, "present", "resumed"],
|
|
3334
|
+
[focused, "present", "resumed"]
|
|
3335
|
+
]);
|
|
3336
|
+
const gone = lifetime + TOAST_MARGIN_MS;
|
|
3337
|
+
record(await presentAt(page, root, leftAt, gone, texts), gone, [
|
|
3338
|
+
[hovered, "gone", "resumed"],
|
|
3339
|
+
[focused, "gone", "resumed"]
|
|
3340
|
+
]);
|
|
3341
|
+
const action = "Conformance: a toast with an action";
|
|
3342
|
+
await show(page, { status: "info", text: action, actionLabel: "Undo" });
|
|
3343
|
+
const actionAt = Date.now();
|
|
3344
|
+
const actionWait = TOAST_DURATION_MS.info + TOAST_MARGIN_MS;
|
|
3345
|
+
record(await presentAt(page, root, actionAt, actionWait, [action]), actionWait, [
|
|
3346
|
+
[action, "present", "action"]
|
|
3347
|
+
]);
|
|
3348
|
+
await dismissAll(page, root);
|
|
3349
|
+
return toastTimingProblems(timings);
|
|
3350
|
+
}
|
|
3351
|
+
async function toastAboveModal(page, root, show) {
|
|
3352
|
+
const before = "Conformance: a toast before the dialog";
|
|
3353
|
+
const text = "Conformance: a toast above the dialog";
|
|
3354
|
+
await show(page, { status: "info", text: before, actionLabel: "Undo" });
|
|
3355
|
+
await settle(page);
|
|
1419
3356
|
const problems = [];
|
|
1420
|
-
|
|
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") {
|
|
3357
|
+
if ((await toasts(page, root))?.stack?.popover !== "manual") {
|
|
1426
3358
|
problems.push(
|
|
1427
|
-
`
|
|
3359
|
+
`toasts: the web app's stack is no popover="manual"; it stays above a showModal() dialog only in the top layer (API.md, Toasts)`
|
|
1428
3360
|
);
|
|
3361
|
+
} else if (await inPage(page, watchToastStackProbe, { root })) {
|
|
3362
|
+
if (await inPage(page, showModalProbe, null)) {
|
|
3363
|
+
await show(page, { status: "success", text });
|
|
3364
|
+
await settle(page);
|
|
3365
|
+
const shown = (await toasts(page, root))?.toasts.some((toast) => toast.text.includes(text)) ?? false;
|
|
3366
|
+
const { toggles, open } = await inPage(page, toastStackTogglesProbe, { root });
|
|
3367
|
+
if (!shown) problems.push("toasts: show.toast with a showModal() dialog open shows nothing");
|
|
3368
|
+
else if (!open || toggles.at(-1) !== "open") {
|
|
3369
|
+
problems.push(
|
|
3370
|
+
"toasts: a toast shown with a showModal() dialog open does not raise the stack above it; the stack is shown again with every toast (API.md, Toasts)"
|
|
3371
|
+
);
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
1429
3374
|
}
|
|
3375
|
+
await inPage(page, removeInjectedProbe, null);
|
|
3376
|
+
await settle(page);
|
|
3377
|
+
await dismissAll(page, root);
|
|
1430
3378
|
return problems;
|
|
1431
3379
|
}
|
|
1432
|
-
var
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
3380
|
+
var toastsCase = {
|
|
3381
|
+
name: "toasts",
|
|
3382
|
+
async run(page, harness) {
|
|
3383
|
+
assertHooksConsistent("toasts", harness, ["toast"]);
|
|
3384
|
+
const root = harness.root ?? DEFAULT_ROOT;
|
|
3385
|
+
const size = sizeOf(harness);
|
|
3386
|
+
const show = harness.show?.toast;
|
|
3387
|
+
if (harness.host === "ide") {
|
|
3388
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
3389
|
+
if (show) await show(page, { status: "success", text: "Conformance: a toast in the IDE host" });
|
|
3390
|
+
await settle(page);
|
|
3391
|
+
const facts = await toasts(page, root);
|
|
3392
|
+
const problems2 = [];
|
|
3393
|
+
if (facts?.stack) {
|
|
3394
|
+
problems2.push(
|
|
3395
|
+
'toasts: the IDE host renders a toast stack [data-chrome="toasts"]; it shows no toasts (guide \xA713.5)'
|
|
3396
|
+
);
|
|
3397
|
+
}
|
|
3398
|
+
if ((facts?.toasts.length ?? 0) > 0 || (facts?.strays.length ?? 0) > 0) {
|
|
3399
|
+
problems2.push(
|
|
3400
|
+
"toasts: show.toast shows a toast in the IDE host; the extension shows the host's notification (guide \xA713.5)"
|
|
3401
|
+
);
|
|
3402
|
+
}
|
|
3403
|
+
return finish("toasts", harness.host, problems2, []);
|
|
1443
3404
|
}
|
|
1444
|
-
return
|
|
1445
|
-
|
|
3405
|
+
if (!show) return unfed(harness, "toast") ? skipFor("toast", "the toasts") : void 0;
|
|
3406
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
3407
|
+
await settle(page);
|
|
3408
|
+
const lang = await inPage(page, documentLangProbe, null);
|
|
3409
|
+
await dismissAll(page, root);
|
|
3410
|
+
const problems = [
|
|
3411
|
+
...await toastAnatomy(page, root, show, lang),
|
|
3412
|
+
...await toastStack(page, root, show),
|
|
3413
|
+
...await toastTiming(page, root, show)
|
|
3414
|
+
];
|
|
3415
|
+
if (harness.host === "web") problems.push(...await toastAboveModal(page, root, show));
|
|
3416
|
+
return finish("toasts", harness.host, problems, []);
|
|
3417
|
+
}
|
|
3418
|
+
};
|
|
3419
|
+
function popover(page, root) {
|
|
3420
|
+
return inPage(page, popoverProbe, { root });
|
|
1446
3421
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
3422
|
+
async function clickPill(page, root) {
|
|
3423
|
+
const pill = (await popover(page, root))?.pill;
|
|
3424
|
+
if (!pill?.visible) return null;
|
|
3425
|
+
await page.mouse.click(pill.client.x, pill.client.y);
|
|
3426
|
+
await settle(page);
|
|
3427
|
+
return popover(page, root);
|
|
3428
|
+
}
|
|
3429
|
+
async function keyboardRows(page, root) {
|
|
3430
|
+
if (!await focusTarget(page, root, "pill")) return ["problems: the pill cannot take the focus"];
|
|
3431
|
+
await page.keyboard.press("Enter");
|
|
3432
|
+
await settle(page);
|
|
3433
|
+
const opened = await popover(page, root);
|
|
3434
|
+
const count = opened?.rows.length ?? 0;
|
|
3435
|
+
if (!opened?.popover?.visible || count === 0) {
|
|
3436
|
+
await closeByEscape(page);
|
|
3437
|
+
return opened?.popover?.visible ? [] : ["problems-popover: Enter on the pill does not open the list"];
|
|
3438
|
+
}
|
|
3439
|
+
const steps = [{ key: "Enter on the pill", expected: 0, focused: opened.focusedRow }];
|
|
3440
|
+
for (let index = 1; index < count; index++) {
|
|
3441
|
+
await page.keyboard.press("ArrowDown");
|
|
3442
|
+
steps.push({
|
|
3443
|
+
key: "ArrowDown",
|
|
3444
|
+
expected: index,
|
|
3445
|
+
focused: (await popover(page, root))?.focusedRow ?? null
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
if (count > 1) {
|
|
3449
|
+
await page.keyboard.press("Home");
|
|
3450
|
+
steps.push({ key: "Home", expected: 0, focused: (await popover(page, root))?.focusedRow ?? null });
|
|
3451
|
+
await page.keyboard.press("End");
|
|
3452
|
+
steps.push({ key: "End", expected: count - 1, focused: (await popover(page, root))?.focusedRow ?? null });
|
|
3453
|
+
}
|
|
3454
|
+
await closeByEscape(page);
|
|
3455
|
+
return popoverKeyboardProblems(steps, count);
|
|
3456
|
+
}
|
|
3457
|
+
var problemsListCase = {
|
|
3458
|
+
name: "problemsList",
|
|
3459
|
+
async run(page, harness) {
|
|
3460
|
+
assertHooksConsistent("problemsList", harness, ["problems"]);
|
|
3461
|
+
const root = harness.root ?? DEFAULT_ROOT;
|
|
3462
|
+
const size = sizeOf(harness);
|
|
3463
|
+
const show = harness.show?.problems;
|
|
3464
|
+
if (!show) return unfed(harness, "problems") ? skipFor("problems", "the problems list") : void 0;
|
|
3465
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
3466
|
+
await settle(page);
|
|
3467
|
+
await show(page);
|
|
3468
|
+
await settle(page);
|
|
3469
|
+
const before = await popover(page, root);
|
|
3470
|
+
const problems = [];
|
|
3471
|
+
if (!before?.pill?.visible) {
|
|
3472
|
+
return finish(
|
|
3473
|
+
"problemsList",
|
|
3474
|
+
harness.host,
|
|
3475
|
+
["problems: the pill is not visible after show.problems"],
|
|
3476
|
+
[]
|
|
1461
3477
|
);
|
|
1462
3478
|
}
|
|
3479
|
+
const opensList = harness.host === "web" || before.pill.attributes["aria-haspopup"] === "dialog";
|
|
3480
|
+
if (!opensList) {
|
|
3481
|
+
const after = await clickPill(page, root);
|
|
3482
|
+
if (after?.popover?.visible) {
|
|
3483
|
+
problems.push(
|
|
3484
|
+
`problems-popover: the ${harness.host} host shows a problems list; its pill opens the host's own view (guide \xA75.10)`
|
|
3485
|
+
);
|
|
3486
|
+
}
|
|
3487
|
+
return finish("problemsList", harness.host, problems, []);
|
|
3488
|
+
}
|
|
3489
|
+
const lang = await inPage(page, documentLangProbe, null);
|
|
3490
|
+
const container = (await measureChrome(page, root)).container;
|
|
3491
|
+
const opened = await clickPill(page, root);
|
|
3492
|
+
if (!opened) return finish("problemsList", harness.host, ["problems: the pill cannot be clicked"], []);
|
|
3493
|
+
problems.push(...openPopoverProblems(opened, container, lang));
|
|
3494
|
+
await closeByEscape(page);
|
|
3495
|
+
const escaped = await popover(page, root);
|
|
3496
|
+
if (escaped) problems.push(...closedPopoverProblems(escaped, "Escape"));
|
|
3497
|
+
await clickPill(page, root);
|
|
3498
|
+
await page.mouse.click(PARK.x, PARK.y);
|
|
3499
|
+
await settle(page);
|
|
3500
|
+
const outside = await popover(page, root);
|
|
3501
|
+
if (outside) problems.push(...closedPopoverProblems(outside, "an outside click"));
|
|
3502
|
+
problems.push(...await keyboardRows(page, root));
|
|
3503
|
+
const skipped = [];
|
|
3504
|
+
const reopened = await clickPill(page, root);
|
|
3505
|
+
const row = reopened?.rows.find((candidate) => candidate.button);
|
|
3506
|
+
if (row) {
|
|
3507
|
+
await page.mouse.click(row.client.x, row.client.y);
|
|
3508
|
+
await settle(page);
|
|
3509
|
+
const selected = await popover(page, root);
|
|
3510
|
+
if (selected) problems.push(...closedPopoverProblems(selected, "a row with an element"));
|
|
3511
|
+
} else {
|
|
3512
|
+
await closeByEscape(page);
|
|
3513
|
+
if ((reopened?.rows.length ?? 0) > 0 && !harness.absent?.includes("problemSelection")) {
|
|
3514
|
+
skipped.push({
|
|
3515
|
+
hook: "problems",
|
|
3516
|
+
reason: 'show.problems gives no item with an elementId: a row that selects its element was not verified (absent: ["problemSelection"] for a modeler whose problems never point at an element)'
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
return finish("problemsList", harness.host, problems, skipped);
|
|
1463
3521
|
}
|
|
1464
|
-
|
|
3522
|
+
};
|
|
3523
|
+
function drag(page, spec) {
|
|
3524
|
+
return inPage(page, dragProbe, spec);
|
|
1465
3525
|
}
|
|
1466
|
-
function
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
);
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
);
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
3526
|
+
function secondFile(file) {
|
|
3527
|
+
return { ...file, name: `second-${file.name}` };
|
|
3528
|
+
}
|
|
3529
|
+
var dropZoneCase = {
|
|
3530
|
+
name: "dropZone",
|
|
3531
|
+
async run(page, harness) {
|
|
3532
|
+
assertHooksConsistent("dropZone", harness, ["drop", "emptyState"]);
|
|
3533
|
+
const root = harness.root ?? DEFAULT_ROOT;
|
|
3534
|
+
const size = sizeOf(harness);
|
|
3535
|
+
const fixtures = harness.drop;
|
|
3536
|
+
if (harness.host === "web" && !fixtures) {
|
|
3537
|
+
return harness.absent?.includes("drop") ? void 0 : skipFor("drop", "the drop zone");
|
|
3538
|
+
}
|
|
3539
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
3540
|
+
await settle(page);
|
|
3541
|
+
const problems = [];
|
|
3542
|
+
const probeFile = { name: "conformance.txt", type: "text/plain", text: "conformance" };
|
|
3543
|
+
if (harness.host !== "web" || !fixtures) {
|
|
3544
|
+
const facts = await drag(page, { root, files: [probeFile], steps: ["dragenter", "dragover"] });
|
|
3545
|
+
if (facts && (facts.state !== null || facts.zone?.visible)) {
|
|
3546
|
+
problems.push(
|
|
3547
|
+
`drop-zone: a file drag shows a drop zone in the ${harness.host} host; its files belong to the host (guide \xA75.2)`
|
|
3548
|
+
);
|
|
3549
|
+
}
|
|
3550
|
+
await drag(page, { root, files: [probeFile], steps: ["dragleave"] });
|
|
3551
|
+
return finish("dropZone", harness.host, problems, []);
|
|
3552
|
+
}
|
|
3553
|
+
const container = (await measureChrome(page, root)).container;
|
|
3554
|
+
const { accepted, rejected } = fixtures;
|
|
3555
|
+
const check = async (files, strings, expected, context) => {
|
|
3556
|
+
const over = await drag(page, { root, files, strings, steps: ["dragenter", "dragover"] });
|
|
3557
|
+
if (over) problems.push(...dropStateProblems(over, expected, context, container));
|
|
3558
|
+
const left = await drag(page, { root, files, strings, steps: ["dragleave"] });
|
|
3559
|
+
if (left && expected !== "none") {
|
|
3560
|
+
problems.push(...dropStateProblems(left, "none", `${context}, once it left the page,`, container));
|
|
3561
|
+
}
|
|
3562
|
+
};
|
|
3563
|
+
await check([accepted], [], "over", `a drag of "${accepted.name}"`);
|
|
3564
|
+
await check(
|
|
3565
|
+
[accepted, secondFile(accepted)],
|
|
3566
|
+
[],
|
|
3567
|
+
fixtures.multiple ? "over" : "forbidden",
|
|
3568
|
+
fixtures.multiple ? "a drag of two files (multiple)" : "a drag of two files"
|
|
1484
3569
|
);
|
|
3570
|
+
if (await inPage(page, injectProbe, { root, kind: "modal" })) {
|
|
3571
|
+
await check([accepted], [], "forbidden", "a file drag with a modal dialog open");
|
|
3572
|
+
}
|
|
3573
|
+
await inPage(page, removeInjectedProbe, null);
|
|
3574
|
+
await check([], ["Conformance text"], "none", "a text drag");
|
|
3575
|
+
const emptyState = harness.show?.emptyState;
|
|
3576
|
+
if (emptyState) {
|
|
3577
|
+
await emptyState(page);
|
|
3578
|
+
await settle(page);
|
|
3579
|
+
}
|
|
3580
|
+
const onStartScreen = async () => (await inPage(page, emptyStateProbe, { root, work: [] }))?.shown ?? false;
|
|
3581
|
+
const started = await onStartScreen();
|
|
3582
|
+
if (rejected) {
|
|
3583
|
+
await drag(page, { root, files: [rejected], steps: ["dragenter", "dragover", "drop"] });
|
|
3584
|
+
const shown = await pollUntil(
|
|
3585
|
+
page,
|
|
3586
|
+
() => toasts(page, root),
|
|
3587
|
+
(facts) => facts?.toasts.some((toast) => toast.status === "danger") ?? false,
|
|
3588
|
+
DROP_WAIT_MS
|
|
3589
|
+
);
|
|
3590
|
+
if (!shown?.toasts.some((toast) => toast.status === "danger")) {
|
|
3591
|
+
problems.push(
|
|
3592
|
+
`drop-zone: dropping "${rejected.name}" shows no danger toast (guide \xA79, or the modeler's onReject)`
|
|
3593
|
+
);
|
|
3594
|
+
}
|
|
3595
|
+
if (started && !await onStartScreen()) {
|
|
3596
|
+
problems.push(
|
|
3597
|
+
`drop-zone: dropping "${rejected.name}" opened it; a file outside accept opens nothing`
|
|
3598
|
+
);
|
|
3599
|
+
}
|
|
3600
|
+
await dismissAll(page, root);
|
|
3601
|
+
}
|
|
3602
|
+
const dropped = await drag(page, { root, files: [accepted], steps: ["dragenter", "dragover", "drop"] });
|
|
3603
|
+
if (dropped?.events.find((event) => event.type === "drop")?.defaultPrevented === false) {
|
|
3604
|
+
problems.push(
|
|
3605
|
+
`drop-zone: the drop of "${accepted.name}" is not prevented, so the browser would open the file`
|
|
3606
|
+
);
|
|
3607
|
+
}
|
|
3608
|
+
if (started) {
|
|
3609
|
+
const opened = await pollUntil(page, onStartScreen, (shown) => !shown, DROP_WAIT_MS);
|
|
3610
|
+
if (opened)
|
|
3611
|
+
problems.push(
|
|
3612
|
+
`drop-zone: dropping "${accepted.name}" on the start screen does not open it (the card stays)`
|
|
3613
|
+
);
|
|
3614
|
+
}
|
|
3615
|
+
const after = await drag(page, { root, files: [], steps: [] });
|
|
3616
|
+
if (after && (after.state !== null || after.zone?.visible)) {
|
|
3617
|
+
problems.push("drop-zone: the drop zone still shows after the drop");
|
|
3618
|
+
}
|
|
3619
|
+
return finish("dropZone", harness.host, problems, []);
|
|
1485
3620
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
}
|
|
1490
|
-
return problems;
|
|
3621
|
+
};
|
|
3622
|
+
function createWebCases() {
|
|
3623
|
+
return [webChromeCase, emptyStateCase, toastsCase, problemsListCase, dropZoneCase];
|
|
1491
3624
|
}
|
|
1492
3625
|
|
|
1493
3626
|
// 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
3627
|
var LOCKED_DRAG_PX = 40;
|
|
1502
3628
|
var CREATE_WAIT_MS = 1e3;
|
|
1503
3629
|
var READ_ONLY_WAIT_MS = 1e3;
|
|
1504
3630
|
var COLOR_WAIT_MS = 600;
|
|
1505
|
-
var POLL_MS = 50;
|
|
1506
3631
|
var REQUESTED_APPEARANCES = [
|
|
1507
3632
|
{ theme: "dark" },
|
|
1508
3633
|
{ theme: "dark", kontrast: "hoch" },
|
|
1509
3634
|
{ theme: "light", kontrast: "hoch" }
|
|
1510
3635
|
];
|
|
1511
|
-
var HOVER_MARGIN_MS = 200;
|
|
1512
|
-
var PARK = { x: 1, y: 1 };
|
|
1513
3636
|
var TEXT_FIELDS = [
|
|
1514
3637
|
["input", "a text field"],
|
|
1515
3638
|
["textarea", "a textarea"],
|
|
@@ -1526,71 +3649,20 @@ var ONE_STEP_UP = 135;
|
|
|
1526
3649
|
var TWO_STEPS_UP = 182;
|
|
1527
3650
|
var ZOOM_LIMIT_MAX = 400;
|
|
1528
3651
|
var ZOOM_LIMIT_MIN = 20;
|
|
1529
|
-
function sizeLabel(size) {
|
|
1530
|
-
return `${size.width} \xD7 ${size.height}`;
|
|
1531
|
-
}
|
|
1532
3652
|
function appearanceLabel(appearance) {
|
|
1533
3653
|
return appearance.kontrast ? `${appearance.theme} + ${appearance.kontrast}` : appearance.theme;
|
|
1534
3654
|
}
|
|
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
3655
|
function missingHooks(harness, what) {
|
|
1556
3656
|
return SHOWN_ISLANDS.filter((island) => unfed(harness, island)).map((island) => ({
|
|
1557
3657
|
hook: island,
|
|
1558
3658
|
reason: `show.${island} is undefined: ${what} of the ${island} island not verified`
|
|
1559
3659
|
}));
|
|
1560
3660
|
}
|
|
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
3661
|
function visibleIslands(facts) {
|
|
1570
3662
|
const islands = /* @__PURE__ */ new Set();
|
|
1571
3663
|
for (const island of SHOWN_ISLANDS) if (facts[island]?.visible) islands.add(island);
|
|
1572
3664
|
return islands;
|
|
1573
3665
|
}
|
|
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
3666
|
async function popupGeometry(page, root, measurement) {
|
|
1595
3667
|
const problems = [];
|
|
1596
3668
|
if (!await openMenuByClick(page, root)) return ["dropdown: no visible menu trigger to open it"];
|
|
@@ -1618,8 +3690,10 @@ async function popupGeometry(page, root, measurement) {
|
|
|
1618
3690
|
if (anchor.label && !tooltip.text.startsWith(anchor.label)) {
|
|
1619
3691
|
problems.push(`tooltip: hovering ${target} ("${anchor.label}") shows "${tooltip.text}"`);
|
|
1620
3692
|
}
|
|
3693
|
+
const legalHeight = legalLineHeight(measurement.islands.legal?.height);
|
|
3694
|
+
const bottomRow = measurement.open.zoomRaised ? SHELL.edge + legalHeight + SHELL.gap : SHELL.edge;
|
|
1621
3695
|
problems.push(
|
|
1622
|
-
...tooltipProblems(tooltip.box, anchor.box, row, measurement.container).map(
|
|
3696
|
+
...tooltipProblems(tooltip.box, anchor.box, row, measurement.container, void 0, bottomRow).map(
|
|
1623
3697
|
(problem) => `${problem} (on ${target})`
|
|
1624
3698
|
)
|
|
1625
3699
|
);
|
|
@@ -1627,15 +3701,28 @@ async function popupGeometry(page, root, measurement) {
|
|
|
1627
3701
|
await page.mouse.move(PARK.x, PARK.y);
|
|
1628
3702
|
return problems;
|
|
1629
3703
|
}
|
|
3704
|
+
var GEOMETRY_TOAST = {
|
|
3705
|
+
status: "info",
|
|
3706
|
+
text: "Conformance: the toast stack's geometry",
|
|
3707
|
+
actionLabel: "Undo"
|
|
3708
|
+
};
|
|
3709
|
+
async function withProblemsListOpen(page, root, check) {
|
|
3710
|
+
const pill = await findTarget(page, root, "pill");
|
|
3711
|
+
if (!pill) return;
|
|
3712
|
+
await page.mouse.click(pill.client.x, pill.client.y);
|
|
3713
|
+
await settle(page);
|
|
3714
|
+
await check();
|
|
3715
|
+
await closeByEscape(page);
|
|
3716
|
+
}
|
|
1630
3717
|
var geometryCase = {
|
|
1631
3718
|
name: "geometry",
|
|
1632
3719
|
async run(page, harness) {
|
|
1633
|
-
assertHooksConsistent("geometry", harness, SHOWN_ISLANDS);
|
|
3720
|
+
assertHooksConsistent("geometry", harness, [...SHOWN_ISLANDS, "toast", "emptyState"]);
|
|
1634
3721
|
const root = harness.root ?? DEFAULT_ROOT;
|
|
1635
3722
|
const problems = [];
|
|
1636
3723
|
for (const size of harness.sizes ?? DEFAULT_SIZES) {
|
|
1637
3724
|
const at = sizeLabel(size);
|
|
1638
|
-
await
|
|
3725
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
1639
3726
|
await settle(page);
|
|
1640
3727
|
const check = async (state) => {
|
|
1641
3728
|
const measurement = await measureChrome(page, root);
|
|
@@ -1649,15 +3736,36 @@ var geometryCase = {
|
|
|
1649
3736
|
};
|
|
1650
3737
|
const opened = await check("as opened");
|
|
1651
3738
|
problems.push(...(await popupGeometry(page, root, opened)).map((problem) => `[${at}] ${problem}`));
|
|
3739
|
+
const toast = harness.show?.toast;
|
|
3740
|
+
if (toast) {
|
|
3741
|
+
await toast(page, GEOMETRY_TOAST);
|
|
3742
|
+
await settle(page);
|
|
3743
|
+
await check("with a toast");
|
|
3744
|
+
}
|
|
1652
3745
|
for (const island of SHOWN_ISLANDS) {
|
|
1653
3746
|
const show = harness.show?.[island];
|
|
1654
3747
|
if (!show) continue;
|
|
1655
3748
|
await show(page);
|
|
1656
3749
|
await settle(page);
|
|
1657
3750
|
await check(`after show.${island}`);
|
|
3751
|
+
if (island === "problems" && harness.host !== "ide") {
|
|
3752
|
+
await withProblemsListOpen(page, root, () => check("with the problems list open"));
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
const emptyState = harness.show?.emptyState;
|
|
3756
|
+
if (emptyState) {
|
|
3757
|
+
await emptyState(page);
|
|
3758
|
+
await settle(page);
|
|
3759
|
+
await check("after show.emptyState");
|
|
1658
3760
|
}
|
|
1659
3761
|
}
|
|
1660
|
-
return finish("geometry", harness.host, problems,
|
|
3762
|
+
return finish("geometry", harness.host, problems, [
|
|
3763
|
+
...missingHooks(harness, "the geometry"),
|
|
3764
|
+
...webHookSkips(harness, {
|
|
3765
|
+
toast: "the geometry of the toast stack",
|
|
3766
|
+
emptyState: "the geometry of the start screen"
|
|
3767
|
+
})
|
|
3768
|
+
]);
|
|
1661
3769
|
}
|
|
1662
3770
|
};
|
|
1663
3771
|
async function menuState(page, root) {
|
|
@@ -1760,7 +3868,7 @@ async function paletteShortcutWalk(page, root) {
|
|
|
1760
3868
|
}
|
|
1761
3869
|
return paletteShortcutProblems(seen);
|
|
1762
3870
|
}
|
|
1763
|
-
async function tabSequenceFromMenu(page, root) {
|
|
3871
|
+
async function tabSequenceFromMenu(page, root, primary) {
|
|
1764
3872
|
if (!await focusTarget(page, root, "menu")) return [];
|
|
1765
3873
|
const stops = [];
|
|
1766
3874
|
for (let step = 0; step < 3; step++) {
|
|
@@ -1768,7 +3876,7 @@ async function tabSequenceFromMenu(page, root) {
|
|
|
1768
3876
|
const stop = await inPage(page, activeStopProbe, { root });
|
|
1769
3877
|
stops.push({ description: stop.description, kind: stop.kind });
|
|
1770
3878
|
}
|
|
1771
|
-
return tabSequenceProblems(stops);
|
|
3879
|
+
return tabSequenceProblems(stops, primary);
|
|
1772
3880
|
}
|
|
1773
3881
|
async function zoomLimitProblems(page, root) {
|
|
1774
3882
|
const problems = [];
|
|
@@ -1813,15 +3921,18 @@ var ariaCase = {
|
|
|
1813
3921
|
assertHooksConsistent("aria", harness, SHOWN_ISLANDS);
|
|
1814
3922
|
const root = harness.root ?? DEFAULT_ROOT;
|
|
1815
3923
|
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
|
|
1816
|
-
await
|
|
3924
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
1817
3925
|
await settle(page);
|
|
1818
3926
|
const facts = await inPage(page, ariaProbe, { root });
|
|
1819
3927
|
if (!facts) fail("aria", harness.host, [`no element matches the shell root selector "${root}"`]);
|
|
1820
3928
|
const problems = ariaProblems(facts, harness.host);
|
|
3929
|
+
problems.push(...menuGroupProblems(await menuEntries(page, root), harness.host, harness.themes));
|
|
1821
3930
|
problems.push(...await menuKeyboardProblems(page, root));
|
|
1822
3931
|
problems.push(...await paletteKeyboardProblems(page, root));
|
|
1823
3932
|
problems.push(...await paletteShortcutWalk(page, root));
|
|
1824
|
-
problems.push(
|
|
3933
|
+
problems.push(
|
|
3934
|
+
...await tabSequenceFromMenu(page, root, harness.host === "web" && Boolean(facts.primary?.visible))
|
|
3935
|
+
);
|
|
1825
3936
|
problems.push(...await zoomLimitProblems(page, root));
|
|
1826
3937
|
const shown = /* @__PURE__ */ new Set();
|
|
1827
3938
|
for (const island of SHOWN_ISLANDS) {
|
|
@@ -1837,6 +3948,7 @@ var ariaCase = {
|
|
|
1837
3948
|
if (withIslands) {
|
|
1838
3949
|
final = withIslands;
|
|
1839
3950
|
problems.push(...optionalIslandProblems(withIslands, shown));
|
|
3951
|
+
if (shown.has("problems")) problems.push(...pillPopupAriaProblems(withIslands, harness.host));
|
|
1840
3952
|
const known = new Set(problems);
|
|
1841
3953
|
problems.push(
|
|
1842
3954
|
...ariaProblems(withIslands, harness.host).filter((problem) => !known.has(problem)).map((problem) => `with every island shown: ${problem}`)
|
|
@@ -1853,7 +3965,7 @@ var keysCase = {
|
|
|
1853
3965
|
const root = harness.root ?? DEFAULT_ROOT;
|
|
1854
3966
|
const host = harness.host;
|
|
1855
3967
|
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
|
|
1856
|
-
await
|
|
3968
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
1857
3969
|
await settle(page);
|
|
1858
3970
|
const problems = [];
|
|
1859
3971
|
if (!await focusTarget(page, root, "canvas")) {
|
|
@@ -2021,15 +4133,76 @@ async function paletteColorWalk(page, root) {
|
|
|
2021
4133
|
}
|
|
2022
4134
|
return [...new Set(problems)];
|
|
2023
4135
|
}
|
|
4136
|
+
function webMode(page) {
|
|
4137
|
+
return inPage(page, webModeProbe, { key: APPEARANCE_STORAGE_KEY });
|
|
4138
|
+
}
|
|
4139
|
+
var APPEARANCE_LABELS = {
|
|
4140
|
+
system: "System",
|
|
4141
|
+
light: "Light",
|
|
4142
|
+
dark: "Dark"
|
|
4143
|
+
};
|
|
4144
|
+
async function appearanceSwitchProblems(page, harness, root, requested) {
|
|
4145
|
+
const problems = [];
|
|
4146
|
+
const system = await webMode(page);
|
|
4147
|
+
const media = (scheme) => system.themeColors.find((meta) => meta.media?.replace(/\s+/g, "") === `(prefers-color-scheme:${scheme})`)?.content;
|
|
4148
|
+
const light = media("light");
|
|
4149
|
+
const dark = media("dark");
|
|
4150
|
+
const colors = light && dark ? { light, dark } : void 0;
|
|
4151
|
+
for (const choice of ["dark", "light", "system"]) {
|
|
4152
|
+
const label = APPEARANCE_LABELS[choice];
|
|
4153
|
+
if (!await openMenuByClick(page, root)) {
|
|
4154
|
+
problems.push(`menu: no visible menu trigger to choose ${label}`);
|
|
4155
|
+
break;
|
|
4156
|
+
}
|
|
4157
|
+
const option = await inPage(page, targetProbe, {
|
|
4158
|
+
root,
|
|
4159
|
+
selector: `[data-menu-id="appearance"] [data-menu-value="${choice}"]`
|
|
4160
|
+
});
|
|
4161
|
+
if (!option) {
|
|
4162
|
+
problems.push(`menu: no Appearance option "${choice}" to choose (guide \xA713.3)`);
|
|
4163
|
+
await closeByEscape(page);
|
|
4164
|
+
break;
|
|
4165
|
+
}
|
|
4166
|
+
await inPage(page, watchModeProbe, null);
|
|
4167
|
+
await page.mouse.click(option.client.x, option.client.y);
|
|
4168
|
+
await settle(page);
|
|
4169
|
+
problems.push(...modeSwitchProblems(await inPage(page, modeChangesProbe, null), label));
|
|
4170
|
+
problems.push(
|
|
4171
|
+
...appearanceChoiceProblems(await inPage(page, menuEntriesProbe, { root }) ?? [], choice)
|
|
4172
|
+
);
|
|
4173
|
+
await closeByEscape(page);
|
|
4174
|
+
problems.push(...webModeProblems(await webMode(page), harness.themes, choice, colors));
|
|
4175
|
+
const facts = await appearanceFacts(page, root);
|
|
4176
|
+
if (facts) {
|
|
4177
|
+
const expected = expectedWebMode(requested, harness.themes, choice);
|
|
4178
|
+
problems.push(
|
|
4179
|
+
...appearanceProblems(facts, "web", requested, harness.themes, expected).problems.map(
|
|
4180
|
+
(problem) => `[chose ${label}] ${problem}`
|
|
4181
|
+
)
|
|
4182
|
+
);
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
return problems;
|
|
4186
|
+
}
|
|
2024
4187
|
var appearanceCase = {
|
|
2025
4188
|
name: "appearance",
|
|
2026
4189
|
async run(page, harness) {
|
|
2027
4190
|
const root = harness.root ?? DEFAULT_ROOT;
|
|
2028
4191
|
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
|
|
2029
4192
|
const problems = [];
|
|
2030
|
-
|
|
4193
|
+
const skipped = [];
|
|
4194
|
+
const web = harness.host === "web";
|
|
4195
|
+
const emulated = !web || page.emulateMedia !== void 0;
|
|
4196
|
+
if (!emulated) {
|
|
4197
|
+
skipped.push({
|
|
4198
|
+
hook: "open",
|
|
4199
|
+
reason: "page.emulateMedia is undefined: the web app's mode under a dark system and under high contrast was not verified"
|
|
4200
|
+
});
|
|
4201
|
+
}
|
|
4202
|
+
let switched = false;
|
|
4203
|
+
for (const requested of emulated ? REQUESTED_APPEARANCES : [LIGHT]) {
|
|
2031
4204
|
const at = `[requested ${appearanceLabel(requested)}]`;
|
|
2032
|
-
await
|
|
4205
|
+
await openHarness(page, harness, { size, requested });
|
|
2033
4206
|
if (harness.host === "embedded") {
|
|
2034
4207
|
await inPage(page, hostPageModeProbe, {
|
|
2035
4208
|
theme: requested.theme,
|
|
@@ -2042,18 +4215,34 @@ var appearanceCase = {
|
|
|
2042
4215
|
problems.push(`${at} no element matches the shell root selector "${root}"`);
|
|
2043
4216
|
continue;
|
|
2044
4217
|
}
|
|
2045
|
-
const
|
|
4218
|
+
const expected = web ? expectedWebMode(requested, harness.themes, "system") : void 0;
|
|
4219
|
+
const verdict = appearanceProblems(facts, harness.host, requested, harness.themes, expected);
|
|
2046
4220
|
problems.push(...verdict.problems.map((problem) => `${at} ${problem}`));
|
|
2047
4221
|
const surround = await inPage(page, surroundProbe, { root });
|
|
2048
4222
|
if (surround) problems.push(...surroundProblems(surround).map((problem) => `${at} ${problem}`));
|
|
2049
4223
|
problems.push(...(await paletteColorWalk(page, root)).map((problem) => `${at} ${problem}`));
|
|
4224
|
+
if (web) {
|
|
4225
|
+
problems.push(
|
|
4226
|
+
...webModeProblems(await webMode(page), harness.themes, "system", void 0).map(
|
|
4227
|
+
(problem) => `${at} ${problem}`
|
|
4228
|
+
),
|
|
4229
|
+
...lightOnlyMenuProblems(await menuEntries(page, root), harness.themes).map(
|
|
4230
|
+
(problem) => `${at} ${problem}`
|
|
4231
|
+
)
|
|
4232
|
+
);
|
|
4233
|
+
if (harness.themes.includes("dark") && !switched && requested.theme === "dark") {
|
|
4234
|
+
switched = true;
|
|
4235
|
+
problems.push(
|
|
4236
|
+
...(await appearanceSwitchProblems(page, harness, root, requested)).map(
|
|
4237
|
+
(problem) => `${at} ${problem}`
|
|
4238
|
+
)
|
|
4239
|
+
);
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
2050
4242
|
}
|
|
2051
|
-
return finish("appearance", harness.host, problems,
|
|
4243
|
+
return finish("appearance", harness.host, problems, skipped);
|
|
2052
4244
|
}
|
|
2053
4245
|
};
|
|
2054
|
-
function shapes(page, root) {
|
|
2055
|
-
return inPage(page, shapesProbe, { root });
|
|
2056
|
-
}
|
|
2057
4246
|
async function focusPaletteEntry(page, root, action) {
|
|
2058
4247
|
const before = await inPage(page, paletteFocusProbe, { root });
|
|
2059
4248
|
if (!before || !before.entries.includes(action)) return false;
|
|
@@ -2085,7 +4274,7 @@ var createCase = {
|
|
|
2085
4274
|
async run(page, harness) {
|
|
2086
4275
|
const root = harness.root ?? DEFAULT_ROOT;
|
|
2087
4276
|
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
|
|
2088
|
-
await
|
|
4277
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
2089
4278
|
await settle(page);
|
|
2090
4279
|
const entry = firstCreatingEntry(await inPage(page, paletteEntriesProbe, { root }));
|
|
2091
4280
|
if (!entry) return void 0;
|
|
@@ -2102,12 +4291,6 @@ var createCase = {
|
|
|
2102
4291
|
return finish("create", harness.host, problems, []);
|
|
2103
4292
|
}
|
|
2104
4293
|
};
|
|
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
4294
|
async function reachableShape(page, root, id) {
|
|
2112
4295
|
const all = await shapes(page, root);
|
|
2113
4296
|
return id === void 0 ? all.find((shape) => shape.hit) : all.find((shape) => shape.id === id);
|
|
@@ -2168,7 +4351,7 @@ var readOnlyCase = {
|
|
|
2168
4351
|
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
|
|
2169
4352
|
const problems = [];
|
|
2170
4353
|
const skipped = [];
|
|
2171
|
-
await
|
|
4354
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
2172
4355
|
await settle(page);
|
|
2173
4356
|
const menuBefore = await menuEntries(page, root);
|
|
2174
4357
|
await invalid(page);
|
|
@@ -2226,7 +4409,7 @@ var cleanupCase = {
|
|
|
2226
4409
|
}
|
|
2227
4410
|
const root = harness.root ?? DEFAULT_ROOT;
|
|
2228
4411
|
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0];
|
|
2229
|
-
await
|
|
4412
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
2230
4413
|
await settle(page);
|
|
2231
4414
|
const armed = await inPage(page, armCleanupProbe, { root });
|
|
2232
4415
|
if (!armed) fail("cleanup", harness.host, [`no element matches the shell root selector "${root}"`]);
|
|
@@ -2258,9 +4441,12 @@ var cleanupCase = {
|
|
|
2258
4441
|
return finish("cleanup", harness.host, problems, []);
|
|
2259
4442
|
}
|
|
2260
4443
|
};
|
|
2261
|
-
function
|
|
4444
|
+
function createBaseCases() {
|
|
2262
4445
|
return [geometryCase, ariaCase, keysCase, createCase, readOnlyCase, appearanceCase, cleanupCase];
|
|
2263
4446
|
}
|
|
4447
|
+
function createCases() {
|
|
4448
|
+
return [...createBaseCases(), ...createWebCases()];
|
|
4449
|
+
}
|
|
2264
4450
|
|
|
2265
4451
|
// src/testing/harness.ts
|
|
2266
4452
|
function shellConformanceCases() {
|