@miragon/modeler-api 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -20
- package/dist/chunk-32BMH76T.js +7 -0
- package/dist/chunk-32BMH76T.js.map +1 -0
- package/dist/conformance.cjs +361 -1
- package/dist/conformance.cjs.map +1 -1
- package/dist/conformance.d.cts +66 -6
- package/dist/conformance.d.ts +66 -6
- package/dist/conformance.js +361 -1
- 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 +36 -6
- package/dist/index.d.ts +36 -6
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-HWB2EYDZ.js +0 -7
- package/dist/chunk-HWB2EYDZ.js.map +0 -1
package/dist/conformance.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MODELER_API_VERSION
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-32BMH76T.js";
|
|
4
4
|
|
|
5
5
|
// src/conformance.ts
|
|
6
6
|
function attachedHost(width = 800, height = 600) {
|
|
@@ -42,6 +42,19 @@ function assertFinite(state, when) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
var defaultSettle = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
45
|
+
var PRE_IMPORT_CALL_BOUND_MS = 2e3;
|
|
46
|
+
async function toleratedCall(action, ms) {
|
|
47
|
+
let timer;
|
|
48
|
+
const call = Promise.resolve().then(action).catch(() => void 0);
|
|
49
|
+
const bound = new Promise((resolve) => {
|
|
50
|
+
timer = setTimeout(resolve, ms);
|
|
51
|
+
});
|
|
52
|
+
try {
|
|
53
|
+
await Promise.race([call, bound]);
|
|
54
|
+
} finally {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
45
58
|
async function changeEvents(harness, modeler, action) {
|
|
46
59
|
const settle = harness.settle ?? defaultSettle;
|
|
47
60
|
let calls = 0;
|
|
@@ -58,12 +71,42 @@ async function changeEvents(harness, modeler, action) {
|
|
|
58
71
|
}
|
|
59
72
|
return { calls, exportAtEmit };
|
|
60
73
|
}
|
|
74
|
+
var lettersAndDigits = (value) => value.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
|
|
61
75
|
var secondText = (h) => h.canonicalTexts[1] ?? h.canonicalTexts[0] ?? "";
|
|
62
76
|
var mutationsOf = (h) => [
|
|
63
77
|
{ name: "mutate (element command)", run: (m) => h.mutate(m) },
|
|
64
78
|
...h.configMutations ?? []
|
|
65
79
|
];
|
|
66
80
|
var sortedIds = (m) => m.elements ? [...m.elements.list().map((e) => e.id)].sort() : void 0;
|
|
81
|
+
var namesById = (m) => m.elements ? new Map(m.elements.list().map((e) => [e.id, e.name])) : void 0;
|
|
82
|
+
async function importOrFail(m, text, failure) {
|
|
83
|
+
await m.importText(text).catch((e) => fail(`${failure}: ${String(e)}`));
|
|
84
|
+
}
|
|
85
|
+
var describeProbe = (probe) => {
|
|
86
|
+
const excerpt = probe.text.length > 40 ? `${probe.text.slice(0, 40)}\u2026` : probe.text;
|
|
87
|
+
return `${probe.label} (${JSON.stringify(excerpt)})`;
|
|
88
|
+
};
|
|
89
|
+
function probesOf(h) {
|
|
90
|
+
const probes = [];
|
|
91
|
+
h.canonicalTexts.forEach((text, index) => {
|
|
92
|
+
const source = `canonicalTexts[${index}]`;
|
|
93
|
+
const codePoints = Array.from(text);
|
|
94
|
+
const cut = (length) => codePoints.slice(0, length).join("");
|
|
95
|
+
probes.push(
|
|
96
|
+
{ label: `the first half of ${source}`, text: cut(Math.floor(codePoints.length / 2)) },
|
|
97
|
+
{ label: `the first third of ${source}`, text: cut(Math.floor(codePoints.length / 3)) },
|
|
98
|
+
{ label: `${source} without its last character`, text: cut(codePoints.length - 1) },
|
|
99
|
+
{ label: `${source} followed by a NUL character`, text: `${text}\0` }
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
(h.probeTexts ?? []).forEach((text, index) => probes.push({ label: `probeTexts[${index}]`, text }));
|
|
103
|
+
const seen = /* @__PURE__ */ new Set();
|
|
104
|
+
return probes.filter((probe) => {
|
|
105
|
+
if (seen.has(probe.text)) return false;
|
|
106
|
+
seen.add(probe.text);
|
|
107
|
+
return true;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
67
110
|
async function importContentText(h, m) {
|
|
68
111
|
for (const text of h.canonicalTexts) {
|
|
69
112
|
await m.importText(text);
|
|
@@ -89,6 +132,56 @@ function conformanceCases() {
|
|
|
89
132
|
}
|
|
90
133
|
}
|
|
91
134
|
},
|
|
135
|
+
{
|
|
136
|
+
name: "calls before the first import never wedge the instance",
|
|
137
|
+
async run(h) {
|
|
138
|
+
const m = await h.create();
|
|
139
|
+
let control;
|
|
140
|
+
try {
|
|
141
|
+
try {
|
|
142
|
+
m.exportText();
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
await toleratedCall(() => h.mutate(m), PRE_IMPORT_CALL_BOUND_MS);
|
|
146
|
+
await (h.settle ?? defaultSettle)();
|
|
147
|
+
control = await h.create();
|
|
148
|
+
const texts = h.canonicalTexts.length > 0 ? h.canonicalTexts.slice(0, 2) : [""];
|
|
149
|
+
let last = "";
|
|
150
|
+
for (const [index, text] of texts.entries()) {
|
|
151
|
+
const source = `canonicalTexts[${index}]`;
|
|
152
|
+
await control.importText(text);
|
|
153
|
+
const expected = exportOrFail(control, `after importing ${source} into a fresh instance`);
|
|
154
|
+
await importOrFail(
|
|
155
|
+
m,
|
|
156
|
+
text,
|
|
157
|
+
`importText(${source}) rejected on an instance that ran exportText() and a user command before its first import (wedged)`
|
|
158
|
+
);
|
|
159
|
+
last = exportOrFail(m, `after importing ${source} into an instance used before its first import`);
|
|
160
|
+
if (last !== expected) {
|
|
161
|
+
fail(
|
|
162
|
+
`importText(${source}) on an instance that ran exportText() and a user command before its first import did not render that document:
|
|
163
|
+
--- expected (a fresh instance's export)
|
|
164
|
+
${expected}
|
|
165
|
+
--- got
|
|
166
|
+
${last}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const { calls } = await changeEvents(h, m, () => h.mutate(m));
|
|
171
|
+
if (calls === 0) {
|
|
172
|
+
fail(
|
|
173
|
+
"a user mutation fired no onContentChanged on an instance used before its first import (wedged)"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (exportOrFail(m, "after a user mutation on an instance used before its first import") === last) {
|
|
177
|
+
fail("a user mutation did not apply on an instance used before its first import (wedged)");
|
|
178
|
+
}
|
|
179
|
+
} finally {
|
|
180
|
+
m.destroy();
|
|
181
|
+
control?.destroy();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
},
|
|
92
185
|
{
|
|
93
186
|
name: "L1: importText is silent (no change events, for canonical, loose and foreign inputs)",
|
|
94
187
|
async run(h) {
|
|
@@ -123,6 +216,87 @@ function conformanceCases() {
|
|
|
123
216
|
}
|
|
124
217
|
}
|
|
125
218
|
},
|
|
219
|
+
{
|
|
220
|
+
name: "L1: an interaction open during importText never commits",
|
|
221
|
+
async run(h) {
|
|
222
|
+
if (!h.openInteraction) {
|
|
223
|
+
return skip(
|
|
224
|
+
"openInteraction is undefined \u2014 an interaction open during an import (a label editor with typed input) was not verified"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const openInteraction = h.openInteraction;
|
|
228
|
+
const first = h.canonicalTexts[0] ?? "";
|
|
229
|
+
const second = h.canonicalTexts[1];
|
|
230
|
+
const m = await h.create();
|
|
231
|
+
try {
|
|
232
|
+
const legs = [];
|
|
233
|
+
if (second !== void 0) {
|
|
234
|
+
await m.importText(second);
|
|
235
|
+
const expected = exportOrFail(m, "after importing canonicalTexts[1]");
|
|
236
|
+
legs.push({ text: second, expected, what: "a different document (canonicalTexts[1])" });
|
|
237
|
+
}
|
|
238
|
+
await m.importText(first);
|
|
239
|
+
const firstExport = exportOrFail(m, "after importing canonicalTexts[0]");
|
|
240
|
+
legs.push({
|
|
241
|
+
text: first,
|
|
242
|
+
expected: firstExport,
|
|
243
|
+
what: "the same document again (canonicalTexts[0])"
|
|
244
|
+
});
|
|
245
|
+
let advice;
|
|
246
|
+
for (const [index, leg] of legs.entries()) {
|
|
247
|
+
await m.importText(first);
|
|
248
|
+
await openInteraction(m);
|
|
249
|
+
await (h.settle ?? defaultSettle)();
|
|
250
|
+
const opened = h.interactionState?.(m);
|
|
251
|
+
if (h.interactionState && opened === void 0) {
|
|
252
|
+
fail("interactionState reports no interaction after openInteraction \u2014 the case proves nothing");
|
|
253
|
+
}
|
|
254
|
+
const { calls } = await changeEvents(h, m, () => m.importText(leg.text).then(() => void 0));
|
|
255
|
+
if (calls > 0) {
|
|
256
|
+
fail(
|
|
257
|
+
`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)`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const reopened = h.interactionState?.(m);
|
|
261
|
+
if (exportOrFail(m, `after importing ${leg.what} over an open interaction`) !== leg.expected) {
|
|
262
|
+
fail(
|
|
263
|
+
`importing ${leg.what} over an open interaction did not leave that document \u2014 the interaction's input was applied to it`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (h.undo) {
|
|
267
|
+
await h.undo(m);
|
|
268
|
+
await (h.settle ?? defaultSettle)();
|
|
269
|
+
if (exportOrFail(m, `after an undo that followed importing ${leg.what}`) !== leg.expected) {
|
|
270
|
+
fail(
|
|
271
|
+
`undo after importing ${leg.what} over an open interaction changed the document \u2014 the interaction was committed onto the fresh undo history (L4)`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const sameDocument = index === legs.length - 1;
|
|
276
|
+
const kept = reopened?.elementId === opened?.elementId && reopened?.text === opened?.text;
|
|
277
|
+
if (sameDocument && opened !== void 0 && !kept) {
|
|
278
|
+
const input = opened.text === void 0 ? "" : ` with the input ${JSON.stringify(opened.text)}`;
|
|
279
|
+
const after = reopened === void 0 ? "closed it" : `left ${JSON.stringify(reopened)} open`;
|
|
280
|
+
advice = `an interaction on '${opened.elementId}'${input} did not survive a re-import of the same document (the import ${after}) \u2014 RECOMMENDED: keep the typed input and reopen the interaction on the element with the same id, so a remote re-import does not cost the user what they were typing`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const unverified = [
|
|
284
|
+
...second === void 0 ? [
|
|
285
|
+
"canonicalTexts holds one text \u2014 importing a DIFFERENT document over an open interaction was not verified (add a second canonical text)"
|
|
286
|
+
] : [],
|
|
287
|
+
...h.interactionState ? [] : [
|
|
288
|
+
"interactionState is undefined \u2014 the RECOMMENDED keep-and-reopen of an interaction across an import was not checked (pass interactionState to check it)"
|
|
289
|
+
]
|
|
290
|
+
];
|
|
291
|
+
return {
|
|
292
|
+
...unverified.length > 0 ? { skipped: unverified.join("; ") } : {},
|
|
293
|
+
...advice === void 0 ? {} : { advisories: [advice] }
|
|
294
|
+
};
|
|
295
|
+
} finally {
|
|
296
|
+
m.destroy();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
},
|
|
126
300
|
{
|
|
127
301
|
name: "L2: canonical texts are byte fixpoints of import \u2192 export",
|
|
128
302
|
async run(h) {
|
|
@@ -445,6 +619,110 @@ ${out}`);
|
|
|
445
619
|
}
|
|
446
620
|
}
|
|
447
621
|
},
|
|
622
|
+
{
|
|
623
|
+
name: "elements (when present): ids and names survive export \u2192 import, in this and in a fresh instance",
|
|
624
|
+
async run(h) {
|
|
625
|
+
const m = await h.create();
|
|
626
|
+
const others = [];
|
|
627
|
+
try {
|
|
628
|
+
await m.importText(h.canonicalTexts[0] ?? "");
|
|
629
|
+
const elements = m.elements;
|
|
630
|
+
if (!elements)
|
|
631
|
+
return skip("the modeler exposes no elements surface (optional) \u2014 nothing to verify");
|
|
632
|
+
await h.mutate(m);
|
|
633
|
+
await (h.settle ?? defaultSettle)();
|
|
634
|
+
const session = new Map(elements.list().map((e) => [e.id, e.name]));
|
|
635
|
+
const exported = exportOrFail(m, "after a user mutation");
|
|
636
|
+
const expectSessionIds = (target, where) => {
|
|
637
|
+
const after = namesById(target);
|
|
638
|
+
if (!after) fail(`${where} exposes no elements surface \u2014 the editor does`);
|
|
639
|
+
for (const [id, name] of session) {
|
|
640
|
+
if (!after.has(id)) {
|
|
641
|
+
fail(
|
|
642
|
+
`element id '${id}' is gone after export \u2192 import ${where} \u2014 the id a session reports must be the element's id in the document (write it back)`
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
if (name !== void 0 && (after.get(id) ?? "") !== name) {
|
|
646
|
+
fail(
|
|
647
|
+
`element id '${id}' names a different element after export \u2192 import ${where} ('${name}' \u2192 '${String(after.get(id))}')`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
await importOrFail(m, exported, "importText rejected the modeler's own export");
|
|
653
|
+
expectSessionIds(m, "into the same instance");
|
|
654
|
+
const fresh = await h.create();
|
|
655
|
+
others.push(fresh);
|
|
656
|
+
await importOrFail(fresh, exported, "a fresh instance rejected the modeler's own export");
|
|
657
|
+
expectSessionIds(fresh, "into a fresh instance");
|
|
658
|
+
if (h.createReadonly) {
|
|
659
|
+
const viewer = await h.createReadonly();
|
|
660
|
+
others.push(viewer);
|
|
661
|
+
await importOrFail(viewer, exported, "the viewer rejected the editor's export");
|
|
662
|
+
if (viewer.elements) expectSessionIds(viewer, "into the viewer");
|
|
663
|
+
}
|
|
664
|
+
} finally {
|
|
665
|
+
m.destroy();
|
|
666
|
+
for (const other of others) other.destroy();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
name: "elements (when present, recommended): ids created on two peers at the same time do not collide",
|
|
672
|
+
async run(h) {
|
|
673
|
+
const text = h.canonicalTexts[0] ?? "";
|
|
674
|
+
const peers = [];
|
|
675
|
+
try {
|
|
676
|
+
const created = [];
|
|
677
|
+
for (let count = 0; count < 2; count += 1) {
|
|
678
|
+
const peer = await h.create();
|
|
679
|
+
peers.push(peer);
|
|
680
|
+
await peer.importText(text);
|
|
681
|
+
if (!peer.elements)
|
|
682
|
+
return skip("the modeler exposes no elements surface (optional) \u2014 nothing to verify");
|
|
683
|
+
const before = new Set(peer.elements.list().map((e) => e.id));
|
|
684
|
+
await h.mutate(peer);
|
|
685
|
+
await (h.settle ?? defaultSettle)();
|
|
686
|
+
const added = new Map(
|
|
687
|
+
peer.elements.list().filter((e) => !before.has(e.id)).map((e) => [e.id, e.name])
|
|
688
|
+
);
|
|
689
|
+
if (added.size === 0) {
|
|
690
|
+
fail(
|
|
691
|
+
"the harness mutate must ADD an element (the elements cases compare the ids two instances allocate)"
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
created.push(added);
|
|
695
|
+
}
|
|
696
|
+
const [
|
|
697
|
+
onFirst = /* @__PURE__ */ new Map(),
|
|
698
|
+
onSecond = /* @__PURE__ */ new Map()
|
|
699
|
+
] = created;
|
|
700
|
+
const shared = [...onFirst.keys()].filter((id) => onSecond.has(id));
|
|
701
|
+
const derivedFromContent = (id) => {
|
|
702
|
+
const name = onFirst.get(id) ?? "";
|
|
703
|
+
if (name === "" || name !== (onSecond.get(id) ?? "")) return false;
|
|
704
|
+
const spelled = lettersAndDigits(name);
|
|
705
|
+
return spelled !== "" && lettersAndDigits(id).includes(spelled);
|
|
706
|
+
};
|
|
707
|
+
const allocated = shared.filter((id) => !derivedFromContent(id));
|
|
708
|
+
const quoted = (ids) => ids.map((id) => `'${id}'`).join(", ");
|
|
709
|
+
if (allocated.length > 0) {
|
|
710
|
+
return {
|
|
711
|
+
advisories: [
|
|
712
|
+
`two instances that imported the same document and each created one element, not of the same name, allocated the same id (${quoted(allocated)}) \u2014 RECOMMENDED: allocate ids for new elements that do not collide across peers (random or peer-scoped, not a per-instance counter); once both edits merge, two elements share one id`
|
|
713
|
+
]
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
if (shared.length > 0) {
|
|
717
|
+
return skip(
|
|
718
|
+
`both instances created an element of the same name and allocated the same id, one derived from that name (${quoted(shared)}) \u2014 such ids collide for identical content by design, so the check is inconclusive: let mutate's elements differ in content (a counter the harness shares across instances)`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
} finally {
|
|
722
|
+
for (const peer of peers) peer.destroy();
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
},
|
|
448
726
|
{
|
|
449
727
|
name: "L6: export is total after imports, user mutations and undo",
|
|
450
728
|
async run(h) {
|
|
@@ -566,6 +844,84 @@ ${out}`);
|
|
|
566
844
|
}
|
|
567
845
|
}
|
|
568
846
|
},
|
|
847
|
+
{
|
|
848
|
+
name: "L7: every rejection of a derived probe leaves no trace",
|
|
849
|
+
async run(h) {
|
|
850
|
+
const base = h.canonicalTexts[0] ?? "";
|
|
851
|
+
const m = await h.create();
|
|
852
|
+
try {
|
|
853
|
+
await m.importText(base);
|
|
854
|
+
const baseline = exportOrFail(m, "after importing canonicalTexts[0]");
|
|
855
|
+
const baseIds = sortedIds(m);
|
|
856
|
+
let previous;
|
|
857
|
+
const reimportBase = async () => {
|
|
858
|
+
if (previous === void 0) return;
|
|
859
|
+
const after = `after importing ${describeProbe(previous)}`;
|
|
860
|
+
await importOrFail(m, base, `the next valid import ${after} did not render`);
|
|
861
|
+
const doc = exportOrFail(m, `on the next valid import ${after}`);
|
|
862
|
+
if (doc !== baseline || baseIds && !same(baseIds, sortedIds(m))) {
|
|
863
|
+
fail(`the next valid import ${after} did not replace the document (wedged)`);
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
const mutateAndExpectApplied = async () => {
|
|
867
|
+
await h.mutate(m);
|
|
868
|
+
await (h.settle ?? defaultSettle)();
|
|
869
|
+
const doc = exportOrFail(m, "after a user mutation");
|
|
870
|
+
if (doc === baseline) {
|
|
871
|
+
fail(
|
|
872
|
+
previous === void 0 ? "the harness mutation did not change the export \u2014 the case proves nothing" : `a user mutation after importing ${describeProbe(previous)} did not apply (wedged)`
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
return doc;
|
|
876
|
+
};
|
|
877
|
+
for (const probe of probesOf(h)) {
|
|
878
|
+
await reimportBase();
|
|
879
|
+
const doc = await mutateAndExpectApplied();
|
|
880
|
+
const ids = sortedIds(m);
|
|
881
|
+
const what = `importText(${describeProbe(probe)})`;
|
|
882
|
+
let rejected = false;
|
|
883
|
+
const { calls } = await changeEvents(
|
|
884
|
+
h,
|
|
885
|
+
m,
|
|
886
|
+
() => m.importText(probe.text).then(
|
|
887
|
+
() => void 0,
|
|
888
|
+
() => {
|
|
889
|
+
rejected = true;
|
|
890
|
+
}
|
|
891
|
+
)
|
|
892
|
+
);
|
|
893
|
+
if (rejected) {
|
|
894
|
+
if (calls > 0) fail(`a rejected ${what} fired onContentChanged ${calls}x (L7)`);
|
|
895
|
+
if (exportOrFail(m, `after a rejected ${what}`) !== doc) {
|
|
896
|
+
fail(`a rejected ${what} changed the document \u2014 the previous state must stay intact (L7)`);
|
|
897
|
+
}
|
|
898
|
+
if (ids && !same(ids, sortedIds(m))) {
|
|
899
|
+
fail(
|
|
900
|
+
`a rejected ${what} changed the rendered elements \u2014 the canvas must keep the previous document (L7)`
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
if (h.undo) {
|
|
904
|
+
await h.undo(m);
|
|
905
|
+
if (exportOrFail(m, `after an undo that followed a rejected ${what}`) !== baseline) {
|
|
906
|
+
fail(
|
|
907
|
+
`a rejected ${what} erased the undo history \u2014 the previous document's history must stay intact (L7)`
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
} else {
|
|
912
|
+
if (calls > 0)
|
|
913
|
+
fail(`${what} resolved and fired onContentChanged ${calls}x \u2014 an import is silent (L1)`);
|
|
914
|
+
exportOrFail(m, `after ${what} resolved (L6)`);
|
|
915
|
+
}
|
|
916
|
+
previous = probe;
|
|
917
|
+
}
|
|
918
|
+
await reimportBase();
|
|
919
|
+
await mutateAndExpectApplied();
|
|
920
|
+
} finally {
|
|
921
|
+
m.destroy();
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
},
|
|
569
925
|
{
|
|
570
926
|
name: "viewer (when shipped): read-only, silent, canonical, fits, reveals, keeps foreign content, unshaken by a rejected import",
|
|
571
927
|
async run(h) {
|
|
@@ -673,10 +1029,14 @@ function registerConformance(harness, test, options = {}) {
|
|
|
673
1029
|
const onSkip = options.onSkip ?? ((name, reason) => {
|
|
674
1030
|
console.warn(`modeler-api conformance: SKIPPED "${name}" \u2014 ${reason}`);
|
|
675
1031
|
});
|
|
1032
|
+
const onAdvisory = options.onAdvisory ?? ((name, advice) => {
|
|
1033
|
+
console.warn(`modeler-api conformance: ADVISORY "${name}" \u2014 ${advice}`);
|
|
1034
|
+
});
|
|
676
1035
|
for (const conformanceCase of conformanceCases()) {
|
|
677
1036
|
test(`conformance: ${conformanceCase.name}`, async () => {
|
|
678
1037
|
const outcome = await conformanceCase.run(harness);
|
|
679
1038
|
if (outcome?.skipped) onSkip(conformanceCase.name, outcome.skipped);
|
|
1039
|
+
for (const advice of outcome?.advisories ?? []) onAdvisory(conformanceCase.name, advice);
|
|
680
1040
|
});
|
|
681
1041
|
}
|
|
682
1042
|
}
|