@miragon/modeler-api 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -57
- package/dist/{chunk-E6KJB7QH.js → chunk-N4TQ7UHQ.js} +2 -2
- package/dist/chunk-N4TQ7UHQ.js.map +1 -0
- package/dist/conformance.cjs +600 -34
- package/dist/conformance.cjs.map +1 -1
- package/dist/conformance.d.cts +32 -4
- package/dist/conformance.d.ts +32 -4
- package/dist/conformance.js +600 -34
- package/dist/conformance.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -8
- package/dist/index.d.ts +73 -8
- package/dist/index.js +1 -1
- package/package.json +5 -5
- package/dist/chunk-E6KJB7QH.js.map +0 -1
package/dist/conformance.cjs
CHANGED
|
@@ -28,7 +28,7 @@ __export(conformance_exports, {
|
|
|
28
28
|
module.exports = __toCommonJS(conformance_exports);
|
|
29
29
|
|
|
30
30
|
// src/index.ts
|
|
31
|
-
var MODELER_API_VERSION = "1.
|
|
31
|
+
var MODELER_API_VERSION = "1.4.0";
|
|
32
32
|
function apiVersionsCompatible(a, b) {
|
|
33
33
|
const [aMajor = "", aMinor = ""] = a.split(".");
|
|
34
34
|
const [bMajor = "", bMinor = ""] = b.split(".");
|
|
@@ -37,6 +37,8 @@ function apiVersionsCompatible(a, b) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// src/conformance.ts
|
|
40
|
+
var nextHostSerial = 0;
|
|
41
|
+
var attachedHosts = [];
|
|
40
42
|
function attachedHost(width = 800, height = 600) {
|
|
41
43
|
const wrapper = document.createElement("div");
|
|
42
44
|
wrapper.style.cssText = `width:${width}px;height:${height}px;position:absolute;left:0;top:0;overflow:hidden`;
|
|
@@ -44,6 +46,8 @@ function attachedHost(width = 800, height = 600) {
|
|
|
44
46
|
host.style.cssText = "width:100%;height:100%";
|
|
45
47
|
wrapper.append(host);
|
|
46
48
|
document.body.append(wrapper);
|
|
49
|
+
attachedHosts = attachedHosts.filter((entry) => entry.wrapper.isConnected);
|
|
50
|
+
attachedHosts.push({ serial: nextHostSerial++, host, wrapper });
|
|
47
51
|
return host;
|
|
48
52
|
}
|
|
49
53
|
function fail(message) {
|
|
@@ -61,6 +65,52 @@ var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
|
61
65
|
var hasBounds = (elements) => typeof elements.bounds === "function";
|
|
62
66
|
var isFiniteBox = (box) => box !== void 0 && [box.x, box.y, box.width, box.height].every((n) => typeof n === "number" && Number.isFinite(n)) && box.width >= 0 && box.height >= 0;
|
|
63
67
|
var describeBox = (box) => box === void 0 ? "undefined" : `{ x: ${box.x}, y: ${box.y}, width: ${box.width}, height: ${box.height} }`;
|
|
68
|
+
var hasContentBounds = (m) => typeof m.contentBounds === "function";
|
|
69
|
+
var edgeTolerance = (edge) => Math.max(1e-3, Math.abs(edge) * 2 ** -20);
|
|
70
|
+
var covers = (outer, inner) => {
|
|
71
|
+
const right = outer.x + outer.width;
|
|
72
|
+
const bottom = outer.y + outer.height;
|
|
73
|
+
return inner.x >= outer.x - edgeTolerance(outer.x) && inner.y >= outer.y - edgeTolerance(outer.y) && inner.x + inner.width <= right + edgeTolerance(right) && inner.y + inner.height <= bottom + edgeTolerance(bottom);
|
|
74
|
+
};
|
|
75
|
+
function contentBoxOf(m, when) {
|
|
76
|
+
let box;
|
|
77
|
+
try {
|
|
78
|
+
box = m.contentBounds();
|
|
79
|
+
} catch (e) {
|
|
80
|
+
fail(`contentBounds() threw ${when}: ${String(e)}`);
|
|
81
|
+
}
|
|
82
|
+
if (box === void 0) return void 0;
|
|
83
|
+
if (!isFiniteBox(box)) {
|
|
84
|
+
fail(
|
|
85
|
+
`contentBounds() ${when} is ${describeBox(box)} \u2014 the content box must be finite with a non-negative size, or undefined when there is nothing to fit`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
89
|
+
}
|
|
90
|
+
function expectContentAroundElements(m, when) {
|
|
91
|
+
const box = contentBoxOf(m, when);
|
|
92
|
+
const listed2 = m.elements?.list() ?? [];
|
|
93
|
+
if (box === void 0) {
|
|
94
|
+
if (listed2.length > 0) {
|
|
95
|
+
fail(
|
|
96
|
+
`contentBounds() ${when} is undefined while elements.list() reports ${listed2.length} element(s) \u2014 undefined means there is nothing to fit`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
101
|
+
if (m.elements && hasBounds(m.elements)) {
|
|
102
|
+
for (const { id } of listed2) {
|
|
103
|
+
const element = m.elements.bounds(id);
|
|
104
|
+
if (!isFiniteBox(element)) continue;
|
|
105
|
+
if (!covers(box, element)) {
|
|
106
|
+
fail(
|
|
107
|
+
`contentBounds() ${when} is ${describeBox(box)}, which does not cover elements.bounds('${id}') = ${describeBox(element)} \u2014 the content box holds every element (and reads the live canvas)`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return box;
|
|
113
|
+
}
|
|
64
114
|
function boxesOf(elements, when) {
|
|
65
115
|
const boxes = /* @__PURE__ */ new Map();
|
|
66
116
|
for (const { id } of elements.list()) {
|
|
@@ -143,7 +193,9 @@ function probesOf(h) {
|
|
|
143
193
|
{ label: `${source} followed by a NUL character`, text: `${text}\0` }
|
|
144
194
|
);
|
|
145
195
|
});
|
|
146
|
-
(h.probeTexts ?? []).forEach(
|
|
196
|
+
(h.probeTexts ?? []).forEach(
|
|
197
|
+
(text, index) => probes.push({ label: `probeTexts[${index}]`, text })
|
|
198
|
+
);
|
|
147
199
|
const seen = /* @__PURE__ */ new Set();
|
|
148
200
|
return probes.filter((probe) => {
|
|
149
201
|
if (seen.has(probe.text)) return false;
|
|
@@ -156,7 +208,9 @@ async function importContentText(h, m) {
|
|
|
156
208
|
await m.importText(text);
|
|
157
209
|
if (!m.elements || m.elements.list().length > 0) return text;
|
|
158
210
|
}
|
|
159
|
-
fail(
|
|
211
|
+
fail(
|
|
212
|
+
"L5 needs a canonical text that renders at least one element \u2014 an empty document has no fitted view"
|
|
213
|
+
);
|
|
160
214
|
}
|
|
161
215
|
function conformanceCases() {
|
|
162
216
|
return [
|
|
@@ -194,13 +248,19 @@ function conformanceCases() {
|
|
|
194
248
|
for (const [index, text] of texts.entries()) {
|
|
195
249
|
const source = `canonicalTexts[${index}]`;
|
|
196
250
|
await control.importText(text);
|
|
197
|
-
const expected = exportOrFail(
|
|
251
|
+
const expected = exportOrFail(
|
|
252
|
+
control,
|
|
253
|
+
`after importing ${source} into a fresh instance`
|
|
254
|
+
);
|
|
198
255
|
await importOrFail(
|
|
199
256
|
m,
|
|
200
257
|
text,
|
|
201
258
|
`importText(${source}) rejected on an instance that ran exportText() and a user command before its first import (wedged)`
|
|
202
259
|
);
|
|
203
|
-
last = exportOrFail(
|
|
260
|
+
last = exportOrFail(
|
|
261
|
+
m,
|
|
262
|
+
`after importing ${source} into an instance used before its first import`
|
|
263
|
+
);
|
|
204
264
|
if (last !== expected) {
|
|
205
265
|
fail(
|
|
206
266
|
`importText(${source}) on an instance that ran exportText() and a user command before its first import did not render that document:
|
|
@@ -218,7 +278,9 @@ ${last}`
|
|
|
218
278
|
);
|
|
219
279
|
}
|
|
220
280
|
if (exportOrFail(m, "after a user mutation on an instance used before its first import") === last) {
|
|
221
|
-
fail(
|
|
281
|
+
fail(
|
|
282
|
+
"a user mutation did not apply on an instance used before its first import (wedged)"
|
|
283
|
+
);
|
|
222
284
|
}
|
|
223
285
|
} finally {
|
|
224
286
|
m.destroy();
|
|
@@ -237,7 +299,11 @@ ${last}`
|
|
|
237
299
|
...(h.foreignTexts ?? []).map((f) => f.text)
|
|
238
300
|
];
|
|
239
301
|
for (const text of texts) {
|
|
240
|
-
const { calls } = await changeEvents(
|
|
302
|
+
const { calls } = await changeEvents(
|
|
303
|
+
h,
|
|
304
|
+
m,
|
|
305
|
+
() => m.importText(text).then(() => void 0)
|
|
306
|
+
);
|
|
241
307
|
if (calls > 0) fail(`importText fired onContentChanged ${calls}x`);
|
|
242
308
|
}
|
|
243
309
|
} finally {
|
|
@@ -253,8 +319,13 @@ ${last}`
|
|
|
253
319
|
await m.importText(h.canonicalTexts[0] ?? "");
|
|
254
320
|
await h.mutate(m);
|
|
255
321
|
await (h.settle ?? defaultSettle)();
|
|
256
|
-
const { calls } = await changeEvents(
|
|
257
|
-
|
|
322
|
+
const { calls } = await changeEvents(
|
|
323
|
+
h,
|
|
324
|
+
m,
|
|
325
|
+
() => m.importText(secondText(h)).then(() => void 0)
|
|
326
|
+
);
|
|
327
|
+
if (calls > 0)
|
|
328
|
+
fail(`importText over a non-empty undo history fired onContentChanged ${calls}x`);
|
|
258
329
|
} finally {
|
|
259
330
|
m.destroy();
|
|
260
331
|
}
|
|
@@ -293,9 +364,15 @@ ${last}`
|
|
|
293
364
|
await (h.settle ?? defaultSettle)();
|
|
294
365
|
const opened = h.interactionState?.(m);
|
|
295
366
|
if (h.interactionState && opened === void 0) {
|
|
296
|
-
fail(
|
|
367
|
+
fail(
|
|
368
|
+
"interactionState reports no interaction after openInteraction \u2014 the case proves nothing"
|
|
369
|
+
);
|
|
297
370
|
}
|
|
298
|
-
const { calls } = await changeEvents(
|
|
371
|
+
const { calls } = await changeEvents(
|
|
372
|
+
h,
|
|
373
|
+
m,
|
|
374
|
+
() => m.importText(leg.text).then(() => void 0)
|
|
375
|
+
);
|
|
299
376
|
if (calls > 0) {
|
|
300
377
|
fail(
|
|
301
378
|
`importing ${leg.what} over an open interaction fired onContentChanged ${calls}x \u2014 the interaction committed during or because of the import (if every import emits, see the L1 cases first)`
|
|
@@ -351,11 +428,13 @@ ${last}`
|
|
|
351
428
|
await m.importText(text);
|
|
352
429
|
const out = m.exportText();
|
|
353
430
|
if (out !== text)
|
|
354
|
-
fail(
|
|
431
|
+
fail(
|
|
432
|
+
`export after import differs from the canonical input:
|
|
355
433
|
--- in
|
|
356
434
|
${text}
|
|
357
435
|
--- out
|
|
358
|
-
${out}`
|
|
436
|
+
${out}`
|
|
437
|
+
);
|
|
359
438
|
if (m.exportText() !== out)
|
|
360
439
|
fail("exportText is not deterministic \u2014 two exports of one document differ");
|
|
361
440
|
}
|
|
@@ -423,7 +502,9 @@ ${out}`);
|
|
|
423
502
|
if (calls === 0) fail(`config mutation "${mutation.name}" fired no onContentChanged`);
|
|
424
503
|
const after = m.exportText();
|
|
425
504
|
if (after === before) {
|
|
426
|
-
fail(
|
|
505
|
+
fail(
|
|
506
|
+
`config mutation "${mutation.name}" did not change the export \u2014 the case proves nothing`
|
|
507
|
+
);
|
|
427
508
|
}
|
|
428
509
|
if (exportAtEmit !== after) {
|
|
429
510
|
fail(
|
|
@@ -496,7 +577,9 @@ ${out}`);
|
|
|
496
577
|
const first = h.canonicalTexts[0];
|
|
497
578
|
const second = h.canonicalTexts[1];
|
|
498
579
|
if (first === void 0 || second === void 0 || first === second) {
|
|
499
|
-
fail(
|
|
580
|
+
fail(
|
|
581
|
+
"the L4 case needs TWO DISTINCT canonicalTexts \u2014 with one, stale undo is undetectable"
|
|
582
|
+
);
|
|
500
583
|
}
|
|
501
584
|
const m = await h.create();
|
|
502
585
|
try {
|
|
@@ -523,7 +606,9 @@ ${out}`);
|
|
|
523
606
|
await h.undo(m);
|
|
524
607
|
await h.undo(m);
|
|
525
608
|
if (m.exportText() !== baseline) {
|
|
526
|
-
fail(
|
|
609
|
+
fail(
|
|
610
|
+
"undo after importText mutated the freshly imported document (stale history applied)"
|
|
611
|
+
);
|
|
527
612
|
}
|
|
528
613
|
} finally {
|
|
529
614
|
m.destroy();
|
|
@@ -566,7 +651,9 @@ ${out}`);
|
|
|
566
651
|
const applied = m.getViewState();
|
|
567
652
|
assertFinite(applied, "after setViewState");
|
|
568
653
|
if (same(applied, fitted)) {
|
|
569
|
-
fail(
|
|
654
|
+
fail(
|
|
655
|
+
"setViewState(viewState) had no effect \u2014 supply a state that differs from the fitted view"
|
|
656
|
+
);
|
|
570
657
|
}
|
|
571
658
|
await m.importText(text);
|
|
572
659
|
if (!same(m.getViewState(), fitted)) {
|
|
@@ -575,7 +662,8 @@ ${out}`);
|
|
|
575
662
|
);
|
|
576
663
|
}
|
|
577
664
|
const { calls } = await changeEvents(h, m, () => m.setViewState(applied));
|
|
578
|
-
if (calls > 0)
|
|
665
|
+
if (calls > 0)
|
|
666
|
+
fail("setViewState fired onContentChanged \u2014 view state is not document content");
|
|
579
667
|
if (m.exportText() !== doc)
|
|
580
668
|
fail("setViewState changed the export \u2014 view state leaked into the document");
|
|
581
669
|
if (!same(m.getViewState(), applied)) {
|
|
@@ -615,7 +703,9 @@ ${out}`);
|
|
|
615
703
|
await settle();
|
|
616
704
|
const opened2 = h.interactionState?.(m);
|
|
617
705
|
if (h.interactionState && opened2 === void 0) {
|
|
618
|
-
fail(
|
|
706
|
+
fail(
|
|
707
|
+
"interactionState reports no interaction after openInteraction \u2014 the case proves nothing"
|
|
708
|
+
);
|
|
619
709
|
}
|
|
620
710
|
return opened2;
|
|
621
711
|
};
|
|
@@ -630,7 +720,11 @@ ${out}`);
|
|
|
630
720
|
"committing the interaction openInteraction opened did not write its typed input into the export \u2014 the case proves nothing (commitInteraction must commit the way a user does, openInteraction type input the serializer writes verbatim)"
|
|
631
721
|
);
|
|
632
722
|
}
|
|
633
|
-
await importOrFail(
|
|
723
|
+
await importOrFail(
|
|
724
|
+
m,
|
|
725
|
+
first,
|
|
726
|
+
"re-importing canonicalTexts[0] after the commit rejected"
|
|
727
|
+
);
|
|
634
728
|
}
|
|
635
729
|
const opened = await open();
|
|
636
730
|
const view = m.getViewState();
|
|
@@ -704,7 +798,8 @@ ${out}`;
|
|
|
704
798
|
const { calls } = await changeEvents(h, m, () => {
|
|
705
799
|
if (elements.reveal(firstId) !== true) fail("reveal(known id) must return true");
|
|
706
800
|
});
|
|
707
|
-
if (calls > 0)
|
|
801
|
+
if (calls > 0)
|
|
802
|
+
fail("reveal fired onContentChanged \u2014 revealing is view behavior, not an edit");
|
|
708
803
|
if (!deliveries.some((d) => d.includes(firstId))) {
|
|
709
804
|
fail(
|
|
710
805
|
`reveal('${firstId}') delivered no selection containing the id \u2014 is it a selectable element (not a root or a label)?`
|
|
@@ -719,7 +814,8 @@ ${out}`;
|
|
|
719
814
|
}
|
|
720
815
|
}
|
|
721
816
|
}
|
|
722
|
-
if (!elements.selection().includes(firstId))
|
|
817
|
+
if (!elements.selection().includes(firstId))
|
|
818
|
+
fail("selection() does not contain the revealed id");
|
|
723
819
|
const seen = deliveries.length;
|
|
724
820
|
off();
|
|
725
821
|
const otherId = ids[1];
|
|
@@ -893,7 +989,9 @@ ${out}`;
|
|
|
893
989
|
await settle();
|
|
894
990
|
const was = before.get(movedId);
|
|
895
991
|
if (was === void 0)
|
|
896
|
-
fail(
|
|
992
|
+
fail(
|
|
993
|
+
`moveElement returned '${movedId}', an id elements.list() did not report before the move`
|
|
994
|
+
);
|
|
897
995
|
const box = boxesOf(elements, "after a move").get(movedId);
|
|
898
996
|
if (box === void 0)
|
|
899
997
|
fail(`'${movedId}' is gone from elements.list() after moveElement moved it`);
|
|
@@ -903,7 +1001,9 @@ ${out}`;
|
|
|
903
1001
|
);
|
|
904
1002
|
}
|
|
905
1003
|
} else {
|
|
906
|
-
unverified.push(
|
|
1004
|
+
unverified.push(
|
|
1005
|
+
"moveElement is undefined \u2014 that a box follows a move was not verified"
|
|
1006
|
+
);
|
|
907
1007
|
}
|
|
908
1008
|
if (h.viewState !== void 0) {
|
|
909
1009
|
const atRest = boxesOf(elements, "before a view change");
|
|
@@ -936,6 +1036,66 @@ ${out}`;
|
|
|
936
1036
|
}
|
|
937
1037
|
}
|
|
938
1038
|
},
|
|
1039
|
+
{
|
|
1040
|
+
name: "contentBounds (when present): a finite box in diagram coordinates around every listed element, live after a new element, unchanged by zoom and scroll",
|
|
1041
|
+
async run(h) {
|
|
1042
|
+
const settle = h.settle ?? defaultSettle;
|
|
1043
|
+
const m = await h.create();
|
|
1044
|
+
let viewer;
|
|
1045
|
+
try {
|
|
1046
|
+
if (!hasContentBounds(m))
|
|
1047
|
+
return skip("the modeler exposes no contentBounds (optional, 1.3) \u2014 nothing to verify");
|
|
1048
|
+
contentBoxOf(m, "before the first import");
|
|
1049
|
+
for (const [index, text] of h.canonicalTexts.entries()) {
|
|
1050
|
+
await importOrFail(m, text, `importText(canonicalTexts[${index}]) rejected`);
|
|
1051
|
+
expectContentAroundElements(m, `after importing canonicalTexts[${index}]`);
|
|
1052
|
+
}
|
|
1053
|
+
await importOrFail(
|
|
1054
|
+
m,
|
|
1055
|
+
h.canonicalTexts[0] ?? "",
|
|
1056
|
+
"importText(canonicalTexts[0]) rejected"
|
|
1057
|
+
);
|
|
1058
|
+
await h.mutate(m);
|
|
1059
|
+
await settle();
|
|
1060
|
+
const atRest = expectContentAroundElements(m, "after a user mutation added an element");
|
|
1061
|
+
const unverified = [];
|
|
1062
|
+
if (h.viewState !== void 0) {
|
|
1063
|
+
m.setViewState(h.viewState);
|
|
1064
|
+
const viewed = contentBoxOf(m, "after setViewState");
|
|
1065
|
+
if (!same(viewed, atRest)) {
|
|
1066
|
+
fail(
|
|
1067
|
+
`contentBounds() changed with the view (${describeBox(atRest)} \u2192 ${describeBox(viewed)}) \u2014 the content box is in diagram coordinates, which zoom and scroll leave alone`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
} else {
|
|
1071
|
+
unverified.push(
|
|
1072
|
+
"viewState is undefined \u2014 that the content box is independent of the view was not verified"
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
if (!m.elements || !hasBounds(m.elements)) {
|
|
1076
|
+
unverified.push(
|
|
1077
|
+
"the modeler exposes no elements.bounds \u2014 that the content box covers every element was not verified"
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
if (h.createReadonly) {
|
|
1081
|
+
viewer = await h.createReadonly();
|
|
1082
|
+
if (hasContentBounds(viewer)) {
|
|
1083
|
+
contentBoxOf(viewer, "on the viewer before the first import");
|
|
1084
|
+
await importOrFail(
|
|
1085
|
+
viewer,
|
|
1086
|
+
h.canonicalTexts[0] ?? "",
|
|
1087
|
+
"the viewer rejected canonicalTexts[0]"
|
|
1088
|
+
);
|
|
1089
|
+
expectContentAroundElements(viewer, "on the viewer after an import");
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (unverified.length > 0) return skip(unverified.join("; "));
|
|
1093
|
+
} finally {
|
|
1094
|
+
m.destroy();
|
|
1095
|
+
viewer?.destroy();
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
939
1099
|
{
|
|
940
1100
|
name: "L6: export is total after imports, user mutations and undo",
|
|
941
1101
|
async run(h) {
|
|
@@ -974,7 +1134,9 @@ ${out}`;
|
|
|
974
1134
|
name: "L7: a rejected import leaves no trace",
|
|
975
1135
|
async run(h) {
|
|
976
1136
|
if (h.invalidTexts === void 0) {
|
|
977
|
-
return skip(
|
|
1137
|
+
return skip(
|
|
1138
|
+
"invalidTexts is undefined \u2014 pass [] if the notation has no non-importable text"
|
|
1139
|
+
);
|
|
978
1140
|
}
|
|
979
1141
|
if (h.invalidTexts.length === 0) return;
|
|
980
1142
|
const m = await h.create();
|
|
@@ -1027,7 +1189,9 @@ ${out}`;
|
|
|
1027
1189
|
name: "L7: the modeler stays usable after a rejected import",
|
|
1028
1190
|
async run(h) {
|
|
1029
1191
|
if (h.invalidTexts === void 0) {
|
|
1030
|
-
return skip(
|
|
1192
|
+
return skip(
|
|
1193
|
+
"invalidTexts is undefined \u2014 pass [] if the notation has no non-importable text"
|
|
1194
|
+
);
|
|
1031
1195
|
}
|
|
1032
1196
|
const bad = h.invalidTexts[0];
|
|
1033
1197
|
if (bad === void 0) return;
|
|
@@ -1039,12 +1203,15 @@ ${out}`;
|
|
|
1039
1203
|
() => void 0
|
|
1040
1204
|
);
|
|
1041
1205
|
const next = secondText(h);
|
|
1042
|
-
await m.importText(next).catch(
|
|
1206
|
+
await m.importText(next).catch(
|
|
1207
|
+
(e) => fail(`the next VALID import after a rejection did not render: ${String(e)}`)
|
|
1208
|
+
);
|
|
1043
1209
|
if (m.exportText() !== next)
|
|
1044
1210
|
fail("the next valid import after a rejection did not replace the document");
|
|
1045
1211
|
const before = m.exportText();
|
|
1046
1212
|
const { calls } = await changeEvents(h, m, () => h.mutate(m));
|
|
1047
|
-
if (calls === 0)
|
|
1213
|
+
if (calls === 0)
|
|
1214
|
+
fail("a user mutation after a rejected import fired no onContentChanged (wedged)");
|
|
1048
1215
|
if (exportOrFail(m, "after a mutation that followed a rejected import") === before) {
|
|
1049
1216
|
fail("a user mutation after a rejected import did not apply (wedged)");
|
|
1050
1217
|
}
|
|
@@ -1106,7 +1273,9 @@ ${out}`;
|
|
|
1106
1273
|
if (rejected) {
|
|
1107
1274
|
if (calls > 0) fail(`a rejected ${what} fired onContentChanged ${calls}x (L7)`);
|
|
1108
1275
|
if (exportOrFail(m, `after a rejected ${what}`) !== doc) {
|
|
1109
|
-
fail(
|
|
1276
|
+
fail(
|
|
1277
|
+
`a rejected ${what} changed the document \u2014 the previous state must stay intact (L7)`
|
|
1278
|
+
);
|
|
1110
1279
|
}
|
|
1111
1280
|
if (ids && !same(ids, sortedIds(m))) {
|
|
1112
1281
|
fail(
|
|
@@ -1123,7 +1292,9 @@ ${out}`;
|
|
|
1123
1292
|
}
|
|
1124
1293
|
} else {
|
|
1125
1294
|
if (calls > 0)
|
|
1126
|
-
fail(
|
|
1295
|
+
fail(
|
|
1296
|
+
`${what} resolved and fired onContentChanged ${calls}x \u2014 an import is silent (L1)`
|
|
1297
|
+
);
|
|
1127
1298
|
exportOrFail(m, `after ${what} resolved (L6)`);
|
|
1128
1299
|
}
|
|
1129
1300
|
previous = probe;
|
|
@@ -1138,7 +1309,8 @@ ${out}`;
|
|
|
1138
1309
|
{
|
|
1139
1310
|
name: "viewer (when shipped): read-only, silent, canonical, fits, reveals, keeps foreign content, unshaken by a rejected import",
|
|
1140
1311
|
async run(h) {
|
|
1141
|
-
if (!h.createReadonly)
|
|
1312
|
+
if (!h.createReadonly)
|
|
1313
|
+
return skip("createReadonly is undefined \u2014 the package ships no viewer mount");
|
|
1142
1314
|
const v = await h.createReadonly();
|
|
1143
1315
|
try {
|
|
1144
1316
|
if (v.meta.editable !== false)
|
|
@@ -1214,6 +1386,10 @@ ${out}`;
|
|
|
1214
1386
|
}
|
|
1215
1387
|
}
|
|
1216
1388
|
},
|
|
1389
|
+
{
|
|
1390
|
+
name: "keys (recommended): a key aimed at the host's controls and editors or at <body> is left alone, a keydown or keyup without a key throws nothing, a key on the focused canvas acts",
|
|
1391
|
+
run: keysStayOnTheCanvas
|
|
1392
|
+
},
|
|
1217
1393
|
{
|
|
1218
1394
|
name: "destroy is safe to call twice",
|
|
1219
1395
|
async run(h) {
|
|
@@ -1226,7 +1402,8 @@ ${out}`;
|
|
|
1226
1402
|
}
|
|
1227
1403
|
async function foreignRoundTrips(h, m, who) {
|
|
1228
1404
|
for (const { text, mustSurvive } of h.foreignTexts ?? []) {
|
|
1229
|
-
if (mustSurvive.length === 0)
|
|
1405
|
+
if (mustSurvive.length === 0)
|
|
1406
|
+
fail("a foreignTexts entry must name at least one fragment in mustSurvive");
|
|
1230
1407
|
await m.importText(text);
|
|
1231
1408
|
const first = exportOrFail(m, `${who}after importing a document with foreign content`);
|
|
1232
1409
|
for (const fragment of mustSurvive) {
|
|
@@ -1235,8 +1412,397 @@ async function foreignRoundTrips(h, m, who) {
|
|
|
1235
1412
|
}
|
|
1236
1413
|
await m.importText(first);
|
|
1237
1414
|
const second = exportOrFail(m, `${who}after re-importing its own export of foreign content`);
|
|
1238
|
-
if (second !== first)
|
|
1415
|
+
if (second !== first)
|
|
1416
|
+
fail(`${who}foreign content did not survive a second import \u2192 export round-trip`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
var CANVAS_SELECTOR = "svg[tabindex]";
|
|
1420
|
+
var NAMED_CODES = {
|
|
1421
|
+
" ": "Space",
|
|
1422
|
+
"+": "Equal",
|
|
1423
|
+
"=": "Equal",
|
|
1424
|
+
"-": "Minus",
|
|
1425
|
+
"!": "Digit1"
|
|
1426
|
+
};
|
|
1427
|
+
var codeOf = (key) => {
|
|
1428
|
+
if (/^[a-z]$/i.test(key)) return `Key${key.toUpperCase()}`;
|
|
1429
|
+
if (/^\d$/.test(key)) return `Digit${key}`;
|
|
1430
|
+
return NAMED_CODES[key] ?? key;
|
|
1431
|
+
};
|
|
1432
|
+
var ARROWS = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"];
|
|
1433
|
+
var KEY_STROKES = [
|
|
1434
|
+
...[
|
|
1435
|
+
"Backspace",
|
|
1436
|
+
"Delete",
|
|
1437
|
+
...ARROWS,
|
|
1438
|
+
"Enter",
|
|
1439
|
+
"Escape",
|
|
1440
|
+
" ",
|
|
1441
|
+
"Tab",
|
|
1442
|
+
"a",
|
|
1443
|
+
"c",
|
|
1444
|
+
"e",
|
|
1445
|
+
"h",
|
|
1446
|
+
"l",
|
|
1447
|
+
"n",
|
|
1448
|
+
"p",
|
|
1449
|
+
"s",
|
|
1450
|
+
"u"
|
|
1451
|
+
].map((key) => ({ key, code: codeOf(key) })),
|
|
1452
|
+
...ARROWS.map((key) => ({ key, code: key, shiftKey: true })),
|
|
1453
|
+
{ key: "!", code: codeOf("!"), shiftKey: true },
|
|
1454
|
+
...[
|
|
1455
|
+
{ key: "a" },
|
|
1456
|
+
{ key: "c" },
|
|
1457
|
+
{ key: "v" },
|
|
1458
|
+
{ key: "x" },
|
|
1459
|
+
{ key: "d" },
|
|
1460
|
+
{ key: "z" },
|
|
1461
|
+
{ key: "Z", shiftKey: true },
|
|
1462
|
+
{ key: "y" },
|
|
1463
|
+
{ key: "+" },
|
|
1464
|
+
{ key: "=" },
|
|
1465
|
+
{ key: "-" },
|
|
1466
|
+
{ key: "0" },
|
|
1467
|
+
...ARROWS.flatMap((key) => [{ key }, { key, shiftKey: true }])
|
|
1468
|
+
].flatMap((stroke) => [
|
|
1469
|
+
{ ...stroke, code: codeOf(stroke.key), ctrlKey: true },
|
|
1470
|
+
{ ...stroke, code: codeOf(stroke.key), metaKey: true }
|
|
1471
|
+
])
|
|
1472
|
+
];
|
|
1473
|
+
var describeStroke = (stroke) => [
|
|
1474
|
+
stroke.ctrlKey ? "Ctrl" : "",
|
|
1475
|
+
stroke.metaKey ? "Meta" : "",
|
|
1476
|
+
stroke.shiftKey ? "Shift" : "",
|
|
1477
|
+
stroke.key === " " ? "Space" : stroke.key === "!" ? "1" : stroke.key
|
|
1478
|
+
].filter((part) => part !== "").join("+");
|
|
1479
|
+
var focusOn = (element) => {
|
|
1480
|
+
element.focus({ preventScroll: true });
|
|
1481
|
+
};
|
|
1482
|
+
var blurActive = (doc) => {
|
|
1483
|
+
const active = doc.activeElement;
|
|
1484
|
+
if (active !== null && active !== doc.body) active.blur();
|
|
1485
|
+
};
|
|
1486
|
+
var stayPut = () => void 0;
|
|
1487
|
+
function hostControls(parent, where) {
|
|
1488
|
+
const doc = parent.ownerDocument;
|
|
1489
|
+
const button = doc.createElement("button");
|
|
1490
|
+
button.type = "button";
|
|
1491
|
+
button.textContent = "Host button";
|
|
1492
|
+
const input = doc.createElement("input");
|
|
1493
|
+
input.value = "host text";
|
|
1494
|
+
const textarea = doc.createElement("textarea");
|
|
1495
|
+
textarea.value = "host text";
|
|
1496
|
+
const select = doc.createElement("select");
|
|
1497
|
+
for (const label of ["one", "two"]) {
|
|
1498
|
+
const option = doc.createElement("option");
|
|
1499
|
+
option.textContent = label;
|
|
1500
|
+
select.append(option);
|
|
1501
|
+
}
|
|
1502
|
+
const editable = doc.createElement("div");
|
|
1503
|
+
editable.setAttribute("contenteditable", "true");
|
|
1504
|
+
editable.textContent = "host text";
|
|
1505
|
+
const monaco = doc.createElement("div");
|
|
1506
|
+
monaco.className = "monaco-editor";
|
|
1507
|
+
const monacoInput = doc.createElement("div");
|
|
1508
|
+
monacoInput.className = "native-edit-context";
|
|
1509
|
+
monacoInput.setAttribute("role", "textbox");
|
|
1510
|
+
monacoInput.tabIndex = 0;
|
|
1511
|
+
monaco.append(monacoInput);
|
|
1512
|
+
const box = doc.createElement("div");
|
|
1513
|
+
box.append(button, input, textarea, select, editable, monaco);
|
|
1514
|
+
const controls = [
|
|
1515
|
+
["a host <button>", button],
|
|
1516
|
+
["a host text <input>", input],
|
|
1517
|
+
["a host <textarea>", textarea],
|
|
1518
|
+
["a host <select>", select],
|
|
1519
|
+
["a host contenteditable", editable],
|
|
1520
|
+
["a focusable element in a host .monaco-editor", monacoInput]
|
|
1521
|
+
];
|
|
1522
|
+
const EditContextClass = globalThis.EditContext;
|
|
1523
|
+
if (typeof EditContextClass === "function") {
|
|
1524
|
+
const editor = doc.createElement("div");
|
|
1525
|
+
editor.tabIndex = 0;
|
|
1526
|
+
Object.assign(editor, { editContext: new EditContextClass() });
|
|
1527
|
+
box.append(editor);
|
|
1528
|
+
controls.push(["a host EditContext editor", editor]);
|
|
1239
1529
|
}
|
|
1530
|
+
parent.append(box);
|
|
1531
|
+
const targets = controls.map(([what, element]) => ({
|
|
1532
|
+
what: `${what} ${where}`,
|
|
1533
|
+
element,
|
|
1534
|
+
enter: () => {
|
|
1535
|
+
focusOn(element);
|
|
1536
|
+
return stayPut;
|
|
1537
|
+
}
|
|
1538
|
+
}));
|
|
1539
|
+
return { targets, box };
|
|
1540
|
+
}
|
|
1541
|
+
function bodyTargets(doc, canvas, wrapper) {
|
|
1542
|
+
const body = doc.body;
|
|
1543
|
+
const hideWith = (hide) => () => {
|
|
1544
|
+
focusOn(canvas);
|
|
1545
|
+
const show = hide();
|
|
1546
|
+
blurActive(doc);
|
|
1547
|
+
return show;
|
|
1548
|
+
};
|
|
1549
|
+
return [
|
|
1550
|
+
{
|
|
1551
|
+
what: "<body> with nothing focused",
|
|
1552
|
+
element: body,
|
|
1553
|
+
enter: () => {
|
|
1554
|
+
blurActive(doc);
|
|
1555
|
+
return stayPut;
|
|
1556
|
+
}
|
|
1557
|
+
},
|
|
1558
|
+
{
|
|
1559
|
+
what: "<body> right after the canvas lost the focus",
|
|
1560
|
+
element: body,
|
|
1561
|
+
enter: () => {
|
|
1562
|
+
focusOn(canvas);
|
|
1563
|
+
blurActive(doc);
|
|
1564
|
+
return stayPut;
|
|
1565
|
+
}
|
|
1566
|
+
},
|
|
1567
|
+
{
|
|
1568
|
+
what: "<body> while the host hides the modeler (display: none)",
|
|
1569
|
+
element: body,
|
|
1570
|
+
enter: hideWith(() => {
|
|
1571
|
+
const display = wrapper.style.display;
|
|
1572
|
+
wrapper.style.display = "none";
|
|
1573
|
+
return () => {
|
|
1574
|
+
wrapper.style.display = display;
|
|
1575
|
+
};
|
|
1576
|
+
})
|
|
1577
|
+
},
|
|
1578
|
+
{
|
|
1579
|
+
what: "<body> while the host makes the modeler inert",
|
|
1580
|
+
element: body,
|
|
1581
|
+
enter: hideWith(() => {
|
|
1582
|
+
wrapper.setAttribute("inert", "");
|
|
1583
|
+
return () => wrapper.removeAttribute("inert");
|
|
1584
|
+
})
|
|
1585
|
+
}
|
|
1586
|
+
];
|
|
1587
|
+
}
|
|
1588
|
+
var listed = (parts) => parts.length < 2 ? parts[0] ?? "" : `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
|
|
1589
|
+
var SHOWN_KEY_FINDINGS = 12;
|
|
1590
|
+
var selectionOf = (m) => m.elements ? JSON.stringify([...m.elements.selection()].sort()) : "";
|
|
1591
|
+
function mountedCanvas(h, m, instance, firstSerial) {
|
|
1592
|
+
const live = attachedHosts.filter((entry) => entry.wrapper.isConnected);
|
|
1593
|
+
if (h.canvasElement) {
|
|
1594
|
+
const named = h.canvasElement(m);
|
|
1595
|
+
if (!named)
|
|
1596
|
+
return `canvasElement() names no canvas for the ${instance}; keys were not verified`;
|
|
1597
|
+
const mount2 = live.find((entry) => entry.host.contains(named));
|
|
1598
|
+
return mount2 === void 0 ? `the canvas canvasElement() names for the ${instance} lies in no attachedHost() \u2014 the kit puts host controls into that host and hides it; keys were not verified` : { canvas: named, host: mount2.host, wrapper: mount2.wrapper };
|
|
1599
|
+
}
|
|
1600
|
+
const made = instance === "modeler" ? "create()" : "createReadonly()";
|
|
1601
|
+
const mounts = live.filter(
|
|
1602
|
+
(entry) => entry.serial >= firstSerial && entry.host.querySelector(CANVAS_SELECTOR) !== null
|
|
1603
|
+
);
|
|
1604
|
+
const [mount] = mounts;
|
|
1605
|
+
const canvas = mount?.host.querySelector(CANVAS_SELECTOR);
|
|
1606
|
+
if (mount && canvas && mounts.length === 1) {
|
|
1607
|
+
return { canvas, host: mount.host, wrapper: mount.wrapper };
|
|
1608
|
+
}
|
|
1609
|
+
return mount === void 0 ? `${made} mounted the ${instance} into no attachedHost() that holds a focusable <svg> (one with a tabindex, diagram-js's canvas) \u2014 mount into one, or name the canvas with canvasElement; keys were not verified` : `${made} made ${mounts.length} attachedHost() elements that hold a focusable <svg> \u2014 the kit cannot tell which one is the ${instance}'s canvas (run the cases one after another, or name the canvas with canvasElement); keys were not verified`;
|
|
1610
|
+
}
|
|
1611
|
+
async function aimKeysAt(h, instance, create, findings) {
|
|
1612
|
+
const settle = h.settle ?? defaultSettle;
|
|
1613
|
+
const text = h.canonicalTexts[0] ?? "";
|
|
1614
|
+
const editable = instance === "modeler";
|
|
1615
|
+
const found = (finding) => {
|
|
1616
|
+
findings.push(editable ? finding : `the viewer: ${finding}`);
|
|
1617
|
+
};
|
|
1618
|
+
const firstSerial = nextHostSerial;
|
|
1619
|
+
const m = await create();
|
|
1620
|
+
const cleanups = [];
|
|
1621
|
+
try {
|
|
1622
|
+
await importOrFail(
|
|
1623
|
+
m,
|
|
1624
|
+
text,
|
|
1625
|
+
editable ? "importText(canonicalTexts[0]) rejected" : "the viewer rejected canonicalTexts[0]"
|
|
1626
|
+
);
|
|
1627
|
+
const mount = mountedCanvas(h, m, instance, firstSerial);
|
|
1628
|
+
if (typeof mount === "string") return mount;
|
|
1629
|
+
const { canvas, host, wrapper } = mount;
|
|
1630
|
+
const doc = host.ownerDocument;
|
|
1631
|
+
const view = doc.defaultView ?? window;
|
|
1632
|
+
const propagated = /* @__PURE__ */ new WeakSet();
|
|
1633
|
+
const reachedWindow = (event) => {
|
|
1634
|
+
propagated.add(event);
|
|
1635
|
+
};
|
|
1636
|
+
view.addEventListener("keydown", reachedWindow);
|
|
1637
|
+
view.addEventListener("keyup", reachedWindow);
|
|
1638
|
+
cleanups.push(() => {
|
|
1639
|
+
view.removeEventListener("keydown", reachedWindow);
|
|
1640
|
+
view.removeEventListener("keyup", reachedWindow);
|
|
1641
|
+
});
|
|
1642
|
+
let changes = 0;
|
|
1643
|
+
cleanups.push(
|
|
1644
|
+
m.onContentChanged(() => {
|
|
1645
|
+
changes += 1;
|
|
1646
|
+
})
|
|
1647
|
+
);
|
|
1648
|
+
const thrownBy = (dispatch) => {
|
|
1649
|
+
const thrown = [];
|
|
1650
|
+
const onError = (event) => {
|
|
1651
|
+
thrown.push(String(event.error ?? event.message));
|
|
1652
|
+
event.preventDefault();
|
|
1653
|
+
};
|
|
1654
|
+
view.addEventListener("error", onError);
|
|
1655
|
+
try {
|
|
1656
|
+
dispatch();
|
|
1657
|
+
} finally {
|
|
1658
|
+
view.removeEventListener("error", onError);
|
|
1659
|
+
}
|
|
1660
|
+
return thrown;
|
|
1661
|
+
};
|
|
1662
|
+
const press = (target, stroke) => {
|
|
1663
|
+
const init = { ...stroke, bubbles: true, cancelable: true, composed: true };
|
|
1664
|
+
const down = new view.KeyboardEvent("keydown", init);
|
|
1665
|
+
const up = new view.KeyboardEvent("keyup", init);
|
|
1666
|
+
const thrown = thrownBy(() => {
|
|
1667
|
+
target.dispatchEvent(down);
|
|
1668
|
+
target.dispatchEvent(up);
|
|
1669
|
+
});
|
|
1670
|
+
return {
|
|
1671
|
+
prevented: down.defaultPrevented || up.defaultPrevented,
|
|
1672
|
+
stopped: !propagated.has(down) || !propagated.has(up),
|
|
1673
|
+
thrown
|
|
1674
|
+
};
|
|
1675
|
+
};
|
|
1676
|
+
let revealed;
|
|
1677
|
+
const setUp = async () => {
|
|
1678
|
+
await importOrFail(m, text, "re-importing canonicalTexts[0] rejected");
|
|
1679
|
+
if (editable) await h.mutate(m);
|
|
1680
|
+
await settle();
|
|
1681
|
+
revealed = m.elements?.list()[0]?.id;
|
|
1682
|
+
if (revealed !== void 0) m.elements?.reveal(revealed);
|
|
1683
|
+
await settle();
|
|
1684
|
+
};
|
|
1685
|
+
await setUp();
|
|
1686
|
+
const placements = [
|
|
1687
|
+
[doc.body, "beside the modeler"],
|
|
1688
|
+
[host, "inside the modeler's container"]
|
|
1689
|
+
];
|
|
1690
|
+
const canvasHolder = canvas.parentElement;
|
|
1691
|
+
if (canvasHolder !== null && canvasHolder !== host) {
|
|
1692
|
+
placements.push([canvasHolder, "next to the canvas"]);
|
|
1693
|
+
}
|
|
1694
|
+
const controls = placements.map(([parent, where]) => hostControls(parent, where));
|
|
1695
|
+
for (const { box } of controls) cleanups.push(() => box.remove());
|
|
1696
|
+
const outside = controls[0]?.targets ?? [];
|
|
1697
|
+
const targets = [
|
|
1698
|
+
...controls.flatMap((placed) => placed.targets),
|
|
1699
|
+
...bodyTargets(doc, canvas, wrapper)
|
|
1700
|
+
];
|
|
1701
|
+
for (const target of targets) {
|
|
1702
|
+
const leave = target.enter();
|
|
1703
|
+
let dirty = false;
|
|
1704
|
+
try {
|
|
1705
|
+
const last = { doc: exportOrFail(m, `with the focus on ${target.what}`), changes };
|
|
1706
|
+
for (const stroke of KEY_STROKES) {
|
|
1707
|
+
const key = `${describeStroke(stroke)} on ${target.what}`;
|
|
1708
|
+
const selection = selectionOf(m);
|
|
1709
|
+
const { prevented, stopped, thrown } = press(target.element, stroke);
|
|
1710
|
+
const changed = [
|
|
1711
|
+
...changes !== last.changes ? ["fired onContentChanged"] : [],
|
|
1712
|
+
...exportOrFail(m, `after ${key}`) !== last.doc ? ["changed the document"] : [],
|
|
1713
|
+
...selectionOf(m) !== selection ? ["changed the selection"] : []
|
|
1714
|
+
];
|
|
1715
|
+
const effects = [
|
|
1716
|
+
...changed,
|
|
1717
|
+
...prevented ? ["prevented it"] : [],
|
|
1718
|
+
...stopped ? ["stopped its propagation"] : [],
|
|
1719
|
+
...thrown.map((error) => `threw ${error}`)
|
|
1720
|
+
];
|
|
1721
|
+
if (effects.length > 0) found(`${key} ${listed(effects)}`);
|
|
1722
|
+
dirty = changed.length > 0;
|
|
1723
|
+
if (dirty) break;
|
|
1724
|
+
}
|
|
1725
|
+
if (!dirty) {
|
|
1726
|
+
await settle();
|
|
1727
|
+
if (exportOrFail(m, `once the keys on ${target.what} settled`) !== last.doc) {
|
|
1728
|
+
found(`a key on ${target.what} changed the document once it settled`);
|
|
1729
|
+
dirty = true;
|
|
1730
|
+
} else if (changes !== last.changes) {
|
|
1731
|
+
found(`a key on ${target.what} fired onContentChanged once it settled`);
|
|
1732
|
+
dirty = true;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
} finally {
|
|
1736
|
+
leave();
|
|
1737
|
+
}
|
|
1738
|
+
if (dirty) await setUp();
|
|
1739
|
+
}
|
|
1740
|
+
const keyless = [
|
|
1741
|
+
["the focused canvas", canvas, () => focusOn(canvas)],
|
|
1742
|
+
["<body> with nothing focused", doc.body, () => blurActive(doc)],
|
|
1743
|
+
...outside.slice(0, 2).map((target) => [
|
|
1744
|
+
target.what,
|
|
1745
|
+
target.element,
|
|
1746
|
+
() => target.enter()
|
|
1747
|
+
])
|
|
1748
|
+
];
|
|
1749
|
+
for (const [what, element, enter] of keyless) {
|
|
1750
|
+
enter();
|
|
1751
|
+
for (const type of ["keydown", "keyup"]) {
|
|
1752
|
+
const thrown = thrownBy(
|
|
1753
|
+
() => element.dispatchEvent(new view.Event(type, { bubbles: true, cancelable: true }))
|
|
1754
|
+
);
|
|
1755
|
+
for (const error of thrown) found(`a ${type} without a key on ${what} threw ${error}`);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
if (!editable) return void 0;
|
|
1759
|
+
if (!m.elements) {
|
|
1760
|
+
return "the modeler exposes no elements surface (optional) \u2014 that Delete on the focused canvas removes a revealed element was not verified, so the case cannot tell a key the modeler leaves alone from one it never sees";
|
|
1761
|
+
}
|
|
1762
|
+
await setUp();
|
|
1763
|
+
focusOn(canvas);
|
|
1764
|
+
if (revealed === void 0) {
|
|
1765
|
+
return "elements.list() reports no element after importing canonicalTexts[0] and one mutate \u2014 that Delete on the focused canvas removes a revealed element was not verified, so the case cannot tell a key the modeler leaves alone from one it never sees";
|
|
1766
|
+
}
|
|
1767
|
+
if (doc.activeElement !== canvas) {
|
|
1768
|
+
return "the canvas did not take the focus \u2014 that a key on the focused canvas acts was not verified, so the case cannot tell a key the modeler leaves alone from one it never sees";
|
|
1769
|
+
}
|
|
1770
|
+
const before = exportOrFail(m, `with '${revealed}' revealed`);
|
|
1771
|
+
for (const key of ["Delete", "Backspace"]) {
|
|
1772
|
+
press(canvas, { key, code: key });
|
|
1773
|
+
await settle();
|
|
1774
|
+
if (exportOrFail(m, `after ${key} on the focused canvas`) !== before) return void 0;
|
|
1775
|
+
}
|
|
1776
|
+
return `Delete and Backspace on the focused canvas, with '${revealed}' revealed (selected), left the document as it was \u2014 the case cannot tell a key the modeler leaves alone from one it never sees`;
|
|
1777
|
+
} finally {
|
|
1778
|
+
for (const cleanup of cleanups) cleanup();
|
|
1779
|
+
m.destroy();
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
async function keysStayOnTheCanvas(h) {
|
|
1783
|
+
if (typeof document === "undefined") {
|
|
1784
|
+
return skip(
|
|
1785
|
+
"no DOM \u2014 keys need a document to be aimed at (run the kit in a browser, the modeler mounted into attachedHost())"
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
const findings = [];
|
|
1789
|
+
const unverified = [];
|
|
1790
|
+
const modelerGap = await aimKeysAt(h, "modeler", () => h.create(), findings);
|
|
1791
|
+
if (modelerGap !== void 0) unverified.push(modelerGap);
|
|
1792
|
+
if (h.createReadonly) {
|
|
1793
|
+
const createViewer = () => h.createReadonly?.() ?? fail("createReadonly went away during the keys case");
|
|
1794
|
+
const viewerGap = await aimKeysAt(h, "viewer", createViewer, findings);
|
|
1795
|
+
if (viewerGap !== void 0) unverified.push(viewerGap);
|
|
1796
|
+
}
|
|
1797
|
+
const more = findings.length > SHOWN_KEY_FINDINGS ? `; and ${findings.length - SHOWN_KEY_FINDINGS} more` : "";
|
|
1798
|
+
return {
|
|
1799
|
+
...unverified.length === 0 ? {} : { skipped: unverified.join("; ") },
|
|
1800
|
+
...findings.length === 0 ? {} : {
|
|
1801
|
+
advisories: [
|
|
1802
|
+
`keys left the canvas: ${findings.slice(0, SHOWN_KEY_FINDINGS).join("; ")}${more} \u2014 RECOMMENDED: act on a key only while the canvas holds the focus (its target is the canvas or lies inside it), leave every other key alone (no command, no change of the document or the selection, no preventDefault, no stopPropagation) and ignore a keydown or keyup without a key`
|
|
1803
|
+
]
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1240
1806
|
}
|
|
1241
1807
|
function registerConformance(harness, test, options = {}) {
|
|
1242
1808
|
const onSkip = options.onSkip ?? ((name, reason) => {
|
|
@@ -1248,8 +1814,8 @@ function registerConformance(harness, test, options = {}) {
|
|
|
1248
1814
|
for (const conformanceCase of conformanceCases()) {
|
|
1249
1815
|
test(`conformance: ${conformanceCase.name}`, async () => {
|
|
1250
1816
|
const outcome = await conformanceCase.run(harness);
|
|
1251
|
-
if (outcome?.skipped) onSkip(conformanceCase.name, outcome.skipped);
|
|
1252
1817
|
for (const advice of outcome?.advisories ?? []) onAdvisory(conformanceCase.name, advice);
|
|
1818
|
+
if (outcome?.skipped) onSkip(conformanceCase.name, outcome.skipped);
|
|
1253
1819
|
});
|
|
1254
1820
|
}
|
|
1255
1821
|
}
|