@cocreate/events 1.34.0 → 2.0.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/.github/FUNDING.yml +1 -1
- package/.github/workflows/automated.yml +18 -7
- package/CHANGELOG.md +317 -274
- package/CONTRIBUTING.md +6 -6
- package/README.md +485 -46
- package/demo/click.html +1 -1
- package/demo/hover.html +1 -1
- package/demo/index.html +1 -1
- package/demo/observer.html +1 -1
- package/demo/selected.html +2 -2
- package/demo/set-value.html +1 -1
- package/docs/index.html +7 -7
- package/package.json +8 -8
- package/src/index.js +355 -61
- package/test/method-invocation.test.cjs +465 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const vm = require("node:vm");
|
|
5
|
+
const test = require("node:test");
|
|
6
|
+
|
|
7
|
+
const code = fs.readFileSync(path.join(__dirname, "../src/index.js"), "utf8")
|
|
8
|
+
.replace(/^import .*;\r?\n/gm, "")
|
|
9
|
+
.replace("CoCreateEvents.init();", "")
|
|
10
|
+
.replace("export default CoCreateEvents;", "globalThis.CoCreateEvents = CoCreateEvents;");
|
|
11
|
+
|
|
12
|
+
const context = {
|
|
13
|
+
action: {},
|
|
14
|
+
observer: {},
|
|
15
|
+
queryElements: () => [],
|
|
16
|
+
getAttribute: () => undefined
|
|
17
|
+
};
|
|
18
|
+
const resolverCode = fs.readFileSync(
|
|
19
|
+
path.join(path.dirname(require.resolve("@cocreate/utils")), "modules/resolveProperty.js"), "utf8"
|
|
20
|
+
).replace("export function resolveProperty", "function resolveProperty");
|
|
21
|
+
vm.runInNewContext(resolverCode + "\n" + code, context);
|
|
22
|
+
|
|
23
|
+
test.before(async () => {
|
|
24
|
+
// Events executes with the browser AST scope, including $value.
|
|
25
|
+
const previousWindow = globalThis.window;
|
|
26
|
+
globalThis.window = {};
|
|
27
|
+
try {
|
|
28
|
+
context.astSync = (await import(path.join(
|
|
29
|
+
path.dirname(require.resolve("@cocreate/utils")), "modules/ast/index.js"
|
|
30
|
+
))).astSync;
|
|
31
|
+
} finally {
|
|
32
|
+
if (previousWindow === undefined) delete globalThis.window;
|
|
33
|
+
else globalThis.window = previousWindow;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("prefix objects expand between dotted attributes in declaration order", async () => {
|
|
38
|
+
const entries = [
|
|
39
|
+
["click.$record", "before"],
|
|
40
|
+
["click", JSON.stringify({ "$record": "inside", "$other": "second" })],
|
|
41
|
+
["click.$finish", "after"]
|
|
42
|
+
];
|
|
43
|
+
const calls = [];
|
|
44
|
+
const source = {
|
|
45
|
+
attributes: entries.map(([name, value]) => ({ name, value })),
|
|
46
|
+
getAttribute(name) {
|
|
47
|
+
assert.notEqual(name, "click", "Read the prefix through raw attributes");
|
|
48
|
+
return entries.find(([key]) => key === name)?.[1] ?? null;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const target = {
|
|
52
|
+
record: value => calls.push(value),
|
|
53
|
+
other: value => calls.push(value),
|
|
54
|
+
finish: value => calls.push(value)
|
|
55
|
+
};
|
|
56
|
+
await context.CoCreateEvents.setValue(source, source, target, "click");
|
|
57
|
+
assert.deepEqual(calls, ["before", "inside", "second", "after"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("prefix values evaluate at execution time without executing their keys on the source", async () => {
|
|
61
|
+
const source = {
|
|
62
|
+
attributes: [{ name: "input", value: JSON.stringify({ "$copy": "$value", "$textContent": "$this.current" }) }],
|
|
63
|
+
current: "before",
|
|
64
|
+
getValue() { return this.current; },
|
|
65
|
+
copy() { assert.fail("Do not execute object keys on the source"); },
|
|
66
|
+
getAttribute() { assert.fail("Object entries are not DOM attributes"); }
|
|
67
|
+
};
|
|
68
|
+
const target = { textContent: "", copy(value) { this.received = value; source.current = "after"; } };
|
|
69
|
+
await context.CoCreateEvents.setValue(source, source, target, "input");
|
|
70
|
+
assert.equal(target.received, "before");
|
|
71
|
+
assert.equal(target.textContent, "after");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("prefix assignments retain typed values, rotation, and attribute fallback", async () => {
|
|
75
|
+
const source = {
|
|
76
|
+
attributes: [{ name: "click", value: JSON.stringify({
|
|
77
|
+
"$count": 0, "$enabled": false, "$options": { nested: true },
|
|
78
|
+
"$selected": ["one", "two"], "data-state": "active"
|
|
79
|
+
}) }],
|
|
80
|
+
getAttribute() { assert.fail("Must use raw prefix data"); }
|
|
81
|
+
};
|
|
82
|
+
const target = {
|
|
83
|
+
selected: "one",
|
|
84
|
+
getAttribute: () => null,
|
|
85
|
+
setAttribute(name, value) { this.attribute = { name, value }; }
|
|
86
|
+
};
|
|
87
|
+
await context.CoCreateEvents.setValue(source, source, target, "click");
|
|
88
|
+
assert.equal(target.count, 0);
|
|
89
|
+
assert.equal(target.enabled, false);
|
|
90
|
+
assert.equal(target.options.nested, true);
|
|
91
|
+
assert.equal(target.selected, "two");
|
|
92
|
+
assert.deepEqual(target.attribute, { name: "data-state", value: "active" });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("bare markers and unrelated attributes do not become assignments", async () => {
|
|
96
|
+
for (const value of ["", "marker"]) {
|
|
97
|
+
const source = {
|
|
98
|
+
attributes: [{ name: "click", value }, { name: "click-query", value: "#target" }],
|
|
99
|
+
getAttribute() { assert.fail("No assignments to read"); }
|
|
100
|
+
};
|
|
101
|
+
const target = {};
|
|
102
|
+
await context.CoCreateEvents.setValue(source, source, target, "click");
|
|
103
|
+
assert.deepEqual(target, {});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("object insertion bindings use their passed position instead of reading a missing attribute", async () => {
|
|
108
|
+
const source = {
|
|
109
|
+
attributes: [{ name: "input", value: JSON.stringify({ insertAdjacentHTML: "beforeend" }) }],
|
|
110
|
+
hasAttribute: () => false,
|
|
111
|
+
getValue: () => "<span>Content</span>",
|
|
112
|
+
getAttribute() { assert.fail("Object insertion is not a dotted DOM attribute"); }
|
|
113
|
+
};
|
|
114
|
+
source.attributes.getNamedItem = () => null;
|
|
115
|
+
let received;
|
|
116
|
+
await context.CoCreateEvents.setValue(source, source, {
|
|
117
|
+
insertAdjacentHTML(position, content) { received = { position, content }; }
|
|
118
|
+
}, "input");
|
|
119
|
+
assert.deepEqual(received, { position: "beforeend", content: "<span>Content</span>" });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
function bindingSource(name, value) {
|
|
123
|
+
return { attributes: [{ name, value }], getAttribute: () => value };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function editorInsertionSource(position, content) {
|
|
127
|
+
return {
|
|
128
|
+
attributes: [{ name: "click.insertAdjacentHTML", value: position }],
|
|
129
|
+
hasAttribute: name => name === "click-value",
|
|
130
|
+
getAttribute: name => name === "click-value" ? content : position
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
test("editor adjacency submits each shared source boundary without mutating the DOM", async () => {
|
|
135
|
+
const editorDetails = { start: 0, contentStart: 5, contentEnd: 12, end: 18 };
|
|
136
|
+
for (const [position, start] of Object.entries({ beforebegin: 0, afterbegin: 5, beforeend: 12, afterend: 18 })) {
|
|
137
|
+
let received;
|
|
138
|
+
const editor = { editor: { async updateText(data) { await Promise.resolve(); received = data; } } };
|
|
139
|
+
editorDetails.editorElement = editor;
|
|
140
|
+
const target = {
|
|
141
|
+
editorDetails,
|
|
142
|
+
parentElement: { closest() { assert.fail("Ownership should come from editorDetails"); } },
|
|
143
|
+
insertAdjacentHTML() { assert.fail("Editor insertion must update source first"); }
|
|
144
|
+
};
|
|
145
|
+
const source = editorInsertionSource(position, "<p>new</p>");
|
|
146
|
+
await context.CoCreateEvents.setValue(source, source, target, "click");
|
|
147
|
+
assert.equal(received.start, start);
|
|
148
|
+
assert.equal(received.length, 0);
|
|
149
|
+
assert.equal(received.value, "<p>new</p>");
|
|
150
|
+
assert.equal(target.editorDetails, editorDetails);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("insertion inside an editor host uses its own API and current shared position", async () => {
|
|
155
|
+
let received;
|
|
156
|
+
const target = {
|
|
157
|
+
editorDetails: { contentStart: 0, contentEnd: 20 },
|
|
158
|
+
editor: { updateText(data) { received = data; } },
|
|
159
|
+
insertAdjacentHTML() { assert.fail("Must use editor source insertion"); }
|
|
160
|
+
};
|
|
161
|
+
target.editorDetails.editorElement = target;
|
|
162
|
+
const source = editorInsertionSource("BEFOREEND", "tail");
|
|
163
|
+
target.editorDetails.contentEnd = 25;
|
|
164
|
+
await context.CoCreateEvents.setValue(source, source, target, "click");
|
|
165
|
+
assert.equal(received.start, 25);
|
|
166
|
+
assert.equal(received.value, "tail");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("iframe descendants send source insertions to the wrapper's editor API", async () => {
|
|
170
|
+
let received;
|
|
171
|
+
const frame = { editor: { updateText(data) { received = data; } } };
|
|
172
|
+
const target = { editorDetails: { end: 37, editorElement: frame } };
|
|
173
|
+
const source = editorInsertionSource("afterend", "<hr />");
|
|
174
|
+
await context.CoCreateEvents.setValue(source, source, target, "click");
|
|
175
|
+
assert.equal(received.start, 37);
|
|
176
|
+
assert.equal(received.value, "<hr />");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("missing editor details and rejected updates never fall through to DOM insertion", async () => {
|
|
180
|
+
const source = editorInsertionSource("afterbegin", "text");
|
|
181
|
+
const target = {
|
|
182
|
+
editor: { updateText() { assert.fail("Missing positions must not be submitted"); } },
|
|
183
|
+
insertAdjacentHTML() { assert.fail("Must not bypass the editor"); }
|
|
184
|
+
};
|
|
185
|
+
target.editorDetails = { editorElement: target };
|
|
186
|
+
await assert.rejects(context.CoCreateEvents.setValue(source, source, target, "click"), /source position/);
|
|
187
|
+
target.editorDetails.contentStart = 5;
|
|
188
|
+
target.editor.updateText = async () => { throw new Error("commit failed"); };
|
|
189
|
+
await assert.rejects(context.CoCreateEvents.setValue(source, source, target, "click"), /commit failed/);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("lowercased bindings resolve inherited methods and preserve their receiver", async () => {
|
|
193
|
+
class Target {
|
|
194
|
+
customMethod(value) { this.received = value; }
|
|
195
|
+
}
|
|
196
|
+
for (const name of ["click.$custommethod", "click.custommethod"]) {
|
|
197
|
+
const source = bindingSource(name, "argument");
|
|
198
|
+
const target = new Target();
|
|
199
|
+
await context.CoCreateEvents.setValue(source, source, target, "click");
|
|
200
|
+
assert.equal(target.received, "argument");
|
|
201
|
+
assert.equal(Object.hasOwn(target, "custommethod"), false);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("lowercased bindings write mixed-case properties, including undefined properties", async () => {
|
|
206
|
+
for (const initial of ["", undefined]) {
|
|
207
|
+
const source = bindingSource("input.textcontent", "updated");
|
|
208
|
+
const target = { textContent: initial };
|
|
209
|
+
await context.CoCreateEvents.setValue(source, source, target, "input");
|
|
210
|
+
assert.equal(target.textContent, "updated");
|
|
211
|
+
assert.equal(Object.hasOwn(target, "textcontent"), false);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("unmatched ordinary bindings retain attribute fallback", async () => {
|
|
216
|
+
const source = bindingSource("click.data-state", "active");
|
|
217
|
+
let received;
|
|
218
|
+
const target = {
|
|
219
|
+
getAttribute: () => null,
|
|
220
|
+
setAttribute(name, value) { received = { name, value }; }
|
|
221
|
+
};
|
|
222
|
+
await context.CoCreateEvents.setValue(source, source, target, "click");
|
|
223
|
+
assert.deepEqual(received, { name: "data-state", value: "active" });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("ambiguous bindings reject", async () => {
|
|
227
|
+
for (const name of ["click.custommethod", "click.$custommethod"]) {
|
|
228
|
+
const source = bindingSource(name, "argument");
|
|
229
|
+
await assert.rejects(context.CoCreateEvents.setValue(source, source, {
|
|
230
|
+
customMethod() {}, custommethod() {}
|
|
231
|
+
}, "click"), /Ambiguous property/);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("$ bindings assign new and existing properties without falling back to attributes", async () => {
|
|
236
|
+
const source = bindingSource("click.$customproperty", "updated");
|
|
237
|
+
for (const initial of [{}, { customProperty: "old" }, { customProperty: undefined }]) {
|
|
238
|
+
const target = Object.assign(initial, {
|
|
239
|
+
getAttribute() { assert.fail("Must not read an attribute"); },
|
|
240
|
+
setAttribute() { assert.fail("Must not write an attribute"); }
|
|
241
|
+
});
|
|
242
|
+
const key = Object.hasOwn(target, "customProperty") ? "customProperty" : "customproperty";
|
|
243
|
+
await context.CoCreateEvents.setValue(source, source, target, "click");
|
|
244
|
+
assert.equal(target[key], "updated");
|
|
245
|
+
assert.equal(Object.hasOwn(target, "$customproperty"), false);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("$ bindings can assign a function value to a new property without invoking it", async () => {
|
|
250
|
+
const callback = () => assert.fail("Assigned function must not execute");
|
|
251
|
+
const source = bindingSource("click.$callback", callback);
|
|
252
|
+
const target = {};
|
|
253
|
+
await context.CoCreateEvents.setValue(source, source, target, "click");
|
|
254
|
+
assert.equal(target.callback, callback);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("$undefined is passed to a method as undefined", async () => {
|
|
258
|
+
let args;
|
|
259
|
+
const source = {
|
|
260
|
+
attributes: [{ name: "focus.$focus", value: "$undefined" }],
|
|
261
|
+
getAttribute: () => undefined
|
|
262
|
+
};
|
|
263
|
+
const target = {
|
|
264
|
+
focus(...received) {
|
|
265
|
+
args = received;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
await context.CoCreateEvents.setValue(source, source, target, "focus");
|
|
270
|
+
assert.deepEqual(args, [undefined]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("an explicit method attribute preserves a provided argument", async () => {
|
|
274
|
+
let args;
|
|
275
|
+
const source = {
|
|
276
|
+
attributes: [{ name: "click.$method", value: "value" }],
|
|
277
|
+
getAttribute: () => "value"
|
|
278
|
+
};
|
|
279
|
+
const target = {
|
|
280
|
+
method(...received) {
|
|
281
|
+
args = received;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
await context.CoCreateEvents.setValue(source, source, target, "click");
|
|
286
|
+
assert.deepEqual(args, ["value"]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("insertAdjacentElement moves the queried element without reading its value", async () => {
|
|
290
|
+
const movedElement = { id: "preview" };
|
|
291
|
+
let query;
|
|
292
|
+
let inserted;
|
|
293
|
+
const attributes = [
|
|
294
|
+
{ name: "click.insertAdjacentElement", value: "beforeend" },
|
|
295
|
+
{ name: "click-element", value: "$document #preview" }
|
|
296
|
+
];
|
|
297
|
+
attributes.getNamedItem = name => attributes.find(attribute => attribute.name === name);
|
|
298
|
+
context.queryElements = options => {
|
|
299
|
+
query = options;
|
|
300
|
+
return [movedElement];
|
|
301
|
+
};
|
|
302
|
+
const source = {
|
|
303
|
+
attributes,
|
|
304
|
+
getAttribute(name) {
|
|
305
|
+
if (name === "click.insertAdjacentElement") return "beforeend";
|
|
306
|
+
if (name === "click-element") return "$document #preview";
|
|
307
|
+
return null;
|
|
308
|
+
},
|
|
309
|
+
getValue() {
|
|
310
|
+
throw new Error("insertAdjacentElement must use the element itself");
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
const target = {
|
|
314
|
+
insertAdjacentElement(position, element) {
|
|
315
|
+
inserted = { position, element };
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
await context.CoCreateEvents.setValue(source, source, target, "click");
|
|
320
|
+
|
|
321
|
+
assert.equal(query.prefix, "click");
|
|
322
|
+
assert.equal(query.selector, "$document #preview");
|
|
323
|
+
assert.deepEqual(inserted, { position: "beforeend", element: movedElement });
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("an empty method attribute preserves an empty-string argument", async () => {
|
|
327
|
+
let args;
|
|
328
|
+
const source = {
|
|
329
|
+
attributes: [{ name: "click.$method", value: "" }],
|
|
330
|
+
getAttribute: () => ""
|
|
331
|
+
};
|
|
332
|
+
const target = {
|
|
333
|
+
method(...received) {
|
|
334
|
+
args = received;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
await context.CoCreateEvents.setValue(source, source, target, "click");
|
|
339
|
+
assert.deepEqual(args, [""]);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("a false-valued implicit method binding does not invoke the method", async () => {
|
|
343
|
+
let calls = 0;
|
|
344
|
+
const source = {
|
|
345
|
+
attributes: [{ name: "input.save", value: "false" }],
|
|
346
|
+
getAttribute: () => false
|
|
347
|
+
};
|
|
348
|
+
const target = {
|
|
349
|
+
save() {
|
|
350
|
+
calls++;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
await context.CoCreateEvents.setValue(source, source, target, "input");
|
|
355
|
+
assert.equal(calls, 0);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("an explicit $method binding can pass false intentionally", async () => {
|
|
359
|
+
let args;
|
|
360
|
+
const source = {
|
|
361
|
+
attributes: [{ name: "input.$method", value: "false" }],
|
|
362
|
+
getAttribute: () => false
|
|
363
|
+
};
|
|
364
|
+
const target = {
|
|
365
|
+
method(...received) {
|
|
366
|
+
args = received;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
await context.CoCreateEvents.setValue(source, source, target, "input");
|
|
371
|
+
assert.deepEqual(args, [false]);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
function insertionSource(action, options = {}) {
|
|
375
|
+
const entries = { [`input.${action}`]: "beforeend", ...options };
|
|
376
|
+
const attributes = Object.entries(entries).map(([name, value]) => ({ name, value }));
|
|
377
|
+
attributes.getNamedItem = name => attributes.find(attribute => attribute.name === name);
|
|
378
|
+
return {
|
|
379
|
+
attributes,
|
|
380
|
+
hasAttribute: name => Object.hasOwn(entries, name),
|
|
381
|
+
getAttribute: name => entries[name] ?? null,
|
|
382
|
+
getValue: () => "event source"
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
test("HTML and text insertion prefer explicit input-value, including empty values", async () => {
|
|
387
|
+
for (const action of ["insertAdjacentHTML", "insertAdjacentText"]) {
|
|
388
|
+
for (const value of ["<span>Explicit content</span>", "", false, 0]) {
|
|
389
|
+
const source = insertionSource(action, { "input-value": value, "input-element": "#other" });
|
|
390
|
+
source.getValue = () => { throw new Error("Should not read the event source"); };
|
|
391
|
+
context.queryElements = () => { throw new Error("Explicit value should take precedence"); };
|
|
392
|
+
let received;
|
|
393
|
+
const target = { [action](position, content) { received = { position, content }; } };
|
|
394
|
+
await context.CoCreateEvents.setValue(source, source, target, "input");
|
|
395
|
+
assert.deepEqual(received, { position: "beforeend", content: value });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("input-element supplies getValue content while the source element stays in place", async () => {
|
|
401
|
+
const source = insertionSource("insertAdjacentHTML", { "input-element": "$parent $next" });
|
|
402
|
+
const contentSource = { getValue: () => "<span>Copied chip</span>" };
|
|
403
|
+
context.queryElements = ({ element, selector }) => {
|
|
404
|
+
assert.equal(element, source);
|
|
405
|
+
assert.equal(selector, "$parent $next");
|
|
406
|
+
return [contentSource];
|
|
407
|
+
};
|
|
408
|
+
let content;
|
|
409
|
+
await context.CoCreateEvents.setValue(source, source, {
|
|
410
|
+
insertAdjacentHTML(position, value) { content = value; }
|
|
411
|
+
}, "input");
|
|
412
|
+
assert.equal(content, "<span>Copied chip</span>");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("a missing input-element does not fall back to unrelated event-source content", async () => {
|
|
416
|
+
const source = insertionSource("insertAdjacentHTML", { "input-element": "#missing" });
|
|
417
|
+
context.queryElements = () => [];
|
|
418
|
+
let calls = 0;
|
|
419
|
+
await context.CoCreateEvents.setValue(source, source, {
|
|
420
|
+
insertAdjacentHTML() { calls++; }
|
|
421
|
+
}, "input");
|
|
422
|
+
assert.equal(calls, 0);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("insertion retains the event-source fallback and awaits asynchronous content", async () => {
|
|
426
|
+
for (const explicit of [false, true]) {
|
|
427
|
+
const source = insertionSource("insertAdjacentHTML", explicit
|
|
428
|
+
? { "input-value": Promise.resolve("asynchronous content") }
|
|
429
|
+
: {});
|
|
430
|
+
let content;
|
|
431
|
+
await context.CoCreateEvents.setValue(source, source, {
|
|
432
|
+
insertAdjacentHTML(position, value) { content = value; }
|
|
433
|
+
}, "input");
|
|
434
|
+
assert.equal(content, explicit ? "asynchronous content" : "event source");
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("a changed checkbox state propagates after a previous input event", () => {
|
|
439
|
+
const listeners = new Map();
|
|
440
|
+
const attributes = [];
|
|
441
|
+
attributes.getNamedItem = () => null;
|
|
442
|
+
const source = {
|
|
443
|
+
type: "checkbox",
|
|
444
|
+
checked: true,
|
|
445
|
+
attributes,
|
|
446
|
+
getAttribute: () => null,
|
|
447
|
+
addEventListener: (name, listener) => listeners.set(name, listener),
|
|
448
|
+
removeEventListener() {}
|
|
449
|
+
};
|
|
450
|
+
const events = context.CoCreateEvents;
|
|
451
|
+
const originalUpdate = events.__updateElements;
|
|
452
|
+
const states = [];
|
|
453
|
+
events.__updateElements = element => states.push(element.checked);
|
|
454
|
+
try {
|
|
455
|
+
events.initElements([source], "input", ["input"]);
|
|
456
|
+
const input = listeners.get("input");
|
|
457
|
+
input({ currentTarget: source, type: "input", detail: { skip: false } });
|
|
458
|
+
source.checked = false;
|
|
459
|
+
input({ currentTarget: source, type: "input", detail: { skip: true } });
|
|
460
|
+
input({ currentTarget: source, type: "input", detail: { skip: true } });
|
|
461
|
+
assert.deepEqual(states, [true, false]);
|
|
462
|
+
} finally {
|
|
463
|
+
events.__updateElements = originalUpdate;
|
|
464
|
+
}
|
|
465
|
+
});
|