@cocreate/elements 1.48.0 → 2.0.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/src/index.js CHANGED
@@ -19,36 +19,456 @@
19
19
  * Commercial Licensing Information:
20
20
  * For commercial use of this software without the copyleft provisions of the AGPLv3,
21
21
  * you must obtain a commercial license from CoCreate LLC.
22
- * For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
22
+ * For details, visit <https://CoCreateJS.com/licenses/> or contact us at sales@CoCreateJS.com.
23
23
  */
24
24
  import {
25
25
  getAttributes
26
26
  } from "@cocreate/utils";
27
- import { init, initElement, initSocket } from "./initHandler.js";
28
- import { initDataKey, getDataKey, getDataElements, getDataKeys, getData, getDataObject } from "./getDataHandler.js";
29
- import { read, save } from "./crudHandler.js";
30
- import { setDataType, setData } from "./setDataHandler.js";
31
- import { reset } from "./formHandler.js";
32
-
33
- import "@cocreate/element-prototype";
34
- import "./dndHandler.js";
35
- import "./srcHandler.js";
36
- import "./socketHandler.js";
37
- import "./valueQuery.js";
27
+ import Observer from "@cocreate/observer";
28
+ import { getDataObject } from "./dataObject.js";
29
+ import {
30
+ socketActionSelector,
31
+ socketOnSelector,
32
+ socketStatusSelector
33
+ } from "./socketSelectors.js";
34
+ import { formSelector } from "./formSelectors.js";
35
+ import { valueQuerySelector } from "./valueQuerySelector.js";
38
36
 
39
37
  import { elements, forms, keys, debounce } from "./mapRegistry.js"
40
38
 
39
+ let hrefHandlerLoading;
40
+ let dataHandlerLoading;
41
+ let srcHandlerLoading;
42
+ let socketHandlerLoading;
43
+ let formHandlerLoading;
44
+ let iframeHandlerLoading;
45
+ let valueQueryHandlerLoading;
46
+ const socketSelectors = [socketOnSelector, socketStatusSelector, socketActionSelector].join(", ");
47
+
48
+ if (document.clickedElement === undefined) {
49
+ document.clickedElement = null;
50
+ document.addEventListener("click", event => {
51
+ document.clickedElement = event.target;
52
+ });
53
+ }
54
+
55
+ const hrefSelector = "[href], [target]";
56
+ const dataSelector = "[storage], [database], [array], [render-json]";
57
+ const srcSelector = "img, [src], [source], [stream]";
58
+
59
+ function loadHrefHandler() {
60
+ if (!hrefHandlerLoading) {
61
+ hrefHandlerLoading = import(/* webpackChunkName: "elements-href-handler" */ "./hrefHandler.js")
62
+ .catch(error => {
63
+ hrefHandlerLoading = undefined;
64
+ throw error;
65
+ });
66
+ }
67
+ hrefHandlerLoading.catch(error => console.error("CoCreate: unable to load href handler.", error));
68
+ }
69
+
70
+ function loadDataHandler() {
71
+ if (!dataHandlerLoading) {
72
+ dataHandlerLoading = import(/* webpackChunkName: "elements-data-handler" */ "./dataHandler.js")
73
+ .catch(error => {
74
+ dataHandlerLoading = undefined;
75
+ throw error;
76
+ });
77
+ }
78
+ return dataHandlerLoading;
79
+ }
80
+
81
+ function initializeDataHandler() {
82
+ loadDataHandler().catch(error => console.error("CoCreate: unable to load data handler.", error));
83
+ }
84
+
85
+ function loadSrcHandler() {
86
+ if (!srcHandlerLoading) {
87
+ srcHandlerLoading = import(/* webpackChunkName: "elements-src-handler" */ "./srcHandler.js")
88
+ .catch(error => {
89
+ srcHandlerLoading = undefined;
90
+ throw error;
91
+ });
92
+ }
93
+ srcHandlerLoading.catch(error => console.error("CoCreate: unable to load source handler.", error));
94
+ }
95
+
96
+ function loadIframeHandler() {
97
+ if (!iframeHandlerLoading) {
98
+ iframeHandlerLoading = import(/* webpackChunkName: "elements-iframe-handler" */ "./iframeHandler.js")
99
+ .catch(error => {
100
+ iframeHandlerLoading = undefined;
101
+ throw error;
102
+ });
103
+ }
104
+ iframeHandlerLoading.catch(error => console.error("CoCreate: unable to load iframe handler.", error));
105
+ }
106
+
107
+ function loadValueQueryHandler() {
108
+ if (!valueQueryHandlerLoading) {
109
+ valueQueryHandlerLoading = import(/* webpackChunkName: "elements-value-query" */ "./valueQuery.js")
110
+ .catch(error => {
111
+ valueQueryHandlerLoading = undefined;
112
+ throw error;
113
+ });
114
+ }
115
+ valueQueryHandlerLoading.catch(error => console.error("CoCreate: unable to load value query handler.", error));
116
+ }
117
+
118
+ function loadFormHandler() {
119
+ if (!formHandlerLoading) {
120
+ formHandlerLoading = import(/* webpackChunkName: "elements-form-handler" */ "./formHandler.js")
121
+ .catch(error => {
122
+ formHandlerLoading = undefined;
123
+ throw error;
124
+ });
125
+ }
126
+ return formHandlerLoading;
127
+ }
128
+
129
+ function initializeFormHandler() {
130
+ loadFormHandler().catch(error => console.error("CoCreate: unable to load form handler.", error));
131
+ }
132
+
133
+ function usesSocket(element) {
134
+ if (element.matches(socketOnSelector) || element.matches(socketStatusSelector)) return true;
135
+ return (element.getAttribute("actions") || "").split(",").some(action =>
136
+ action.trim().split("(", 1)[0].trim() === "send"
137
+ );
138
+ }
139
+
140
+ function loadSocketHandler(element) {
141
+ if (!usesSocket(element)) return;
142
+ if (!socketHandlerLoading) {
143
+ socketHandlerLoading = import(/* webpackChunkName: "elements-socket-handler" */ "./socketHandler.js")
144
+ .catch(error => {
145
+ socketHandlerLoading = undefined;
146
+ throw error;
147
+ });
148
+ }
149
+ socketHandlerLoading.catch(error => {
150
+ element.dispatchEvent(new CustomEvent("socketerror", {
151
+ bubbles: true, detail: { error, element }
152
+ }));
153
+ });
154
+ }
155
+
156
+ Observer.init([
157
+ {
158
+ name: "CoCreateElementsHrefLoaderAddedNodes",
159
+ types: ["addedNodes"],
160
+ selector: hrefSelector,
161
+ callback: loadHrefHandler
162
+ },
163
+ {
164
+ name: "CoCreateElementsHrefLoaderAttributes",
165
+ types: ["attributes"],
166
+ attributeFilter: ["href", "target"],
167
+ selector: hrefSelector,
168
+ callback: loadHrefHandler
169
+ },
170
+ {
171
+ name: "CoCreateElementsDataLoaderAddedNodes",
172
+ types: ["addedNodes"],
173
+ selector: dataSelector,
174
+ callback: initializeDataHandler
175
+ },
176
+ {
177
+ name: "CoCreateElementsDataLoaderAttributes",
178
+ types: ["attributes"],
179
+ attributeFilter: ["storage", "database", "array", "render-json"],
180
+ selector: dataSelector,
181
+ callback: initializeDataHandler
182
+ },
183
+ {
184
+ name: "CoCreateElementsSrcLoaderAddedNodes",
185
+ types: ["addedNodes"],
186
+ selector: srcSelector,
187
+ callback: loadSrcHandler
188
+ },
189
+ {
190
+ name: "CoCreateElementsSrcLoaderAttributes",
191
+ types: ["attributes"],
192
+ attributeFilter: ["src", "source", "stream"],
193
+ selector: srcSelector,
194
+ callback: loadSrcHandler
195
+ },
196
+ {
197
+ name: "CoCreateElementsActionLoaderAddedNodes",
198
+ types: ["addedNodes"],
199
+ selector: "[actions]",
200
+ callback: mutation => loadActionHandler(mutation.target)
201
+ },
202
+ {
203
+ name: "CoCreateElementsActionLoaderAttributes",
204
+ types: ["attributes"],
205
+ attributeFilter: ["actions"],
206
+ selector: "[actions]",
207
+ callback: mutation => loadActionHandler(mutation.target)
208
+ },
209
+ {
210
+ name: "CoCreateElementsFormLoader",
211
+ types: ["addedNodes"],
212
+ selector: formSelector,
213
+ callback: initializeFormHandler
214
+ },
215
+ {
216
+ name: "CoCreateElementsSocketLoaderAddedNodes",
217
+ types: ["addedNodes"],
218
+ selector: socketSelectors,
219
+ callback: mutation => loadSocketHandler(mutation.target)
220
+ },
221
+ {
222
+ name: "CoCreateElementsSocketLoaderAttributes",
223
+ types: ["attributes"],
224
+ attributeFilter: ["socket-on", "socket-status", "actions"],
225
+ selector: socketSelectors,
226
+ callback: mutation => loadSocketHandler(mutation.target)
227
+ },
228
+ {
229
+ name: "CoCreateElementsIframeLoader",
230
+ types: ["addedNodes"],
231
+ selector: "iframe",
232
+ callback: loadIframeHandler
233
+ },
234
+ {
235
+ name: "CoCreateElementsValueQueryLoaderAddedNodes",
236
+ types: ["addedNodes"],
237
+ selector: valueQuerySelector,
238
+ callback: loadValueQueryHandler
239
+ },
240
+ {
241
+ name: "CoCreateElementsValueQueryLoaderAttributes",
242
+ types: ["attributes"],
243
+ attributeFilter: ["value-query"],
244
+ selector: valueQuerySelector,
245
+ callback: loadValueQueryHandler
246
+ }
247
+ ]);
248
+
249
+ function loadActionHandler(element) {
250
+ const actions = (element.getAttribute("actions") || "").split(",");
251
+ for (const action of actions) {
252
+ const name = action.trim().split("(", 1)[0].trim();
253
+ if (name === "save" || name === "delete") initializeDataHandler();
254
+ if (name === "reset") initializeFormHandler();
255
+ if (name === "disableHref" || name === "enableHref") loadHrefHandler();
256
+ }
257
+ }
258
+
259
+ document.querySelectorAll("[actions]").forEach(loadActionHandler);
260
+ if (document.querySelector(hrefSelector)) loadHrefHandler();
261
+ if (document.querySelector(dataSelector)) initializeDataHandler();
262
+ if (document.querySelector(srcSelector)) loadSrcHandler();
263
+ document.querySelectorAll(socketSelectors).forEach(loadSocketHandler);
264
+ if (document.querySelector(formSelector)) initializeFormHandler();
265
+ if (document.querySelector("iframe")) loadIframeHandler();
266
+ if (document.querySelector(valueQuerySelector)) loadValueQueryHandler();
267
+
268
+ // Preserve the asynchronous public APIs while loading their implementation on demand.
269
+ const callDataHandler = (name, args) => loadDataHandler().then(module => module[name](...args));
270
+ /**
271
+ *
272
+ * @public
273
+ * @environment client
274
+ */
275
+ const init = (...args) => callDataHandler("init", args);
276
+ /**
277
+ *
278
+ * @public
279
+ * @environment client
280
+ */
281
+ const read = (...args) => callDataHandler("read", args);
282
+ /**
283
+ *
284
+ * @public
285
+ * @environment client
286
+ */
287
+ const save = (...args) => callDataHandler("save", args);
288
+ /**
289
+ *
290
+ * @public
291
+ * @environment client
292
+ */
293
+ const getData = (...args) => callDataHandler("getData", args);
294
+ /**
295
+ *
296
+ * @public
297
+ * @environment client
298
+ */
299
+ const setDataType = (...args) => callDataHandler("setDataType", args);
300
+
301
+ /**
302
+ *
303
+ * @public
304
+ * @environment client
305
+ */
306
+ export function reset(action) {
307
+ return loadFormHandler().then(module => module.reset(action));
308
+ }
309
+
41
310
  export default {
42
311
  init,
43
312
  read,
44
313
  save,
45
314
  getData,
315
+ /**
316
+ *
317
+ * @public
318
+ * @environment client
319
+ */
46
320
  getDataObject,
47
321
  reset,
322
+ /**
323
+ *
324
+ * @public
325
+ * @environment client
326
+ */
48
327
  elements,
328
+ /**
329
+ *
330
+ * @public
331
+ * @environment client
332
+ */
49
333
  keys,
334
+ /**
335
+ *
336
+ * @public
337
+ * @environment client
338
+ */
50
339
  forms,
340
+ /**
341
+ *
342
+ * @public
343
+ * @environment client
344
+ */
51
345
  debounce,
346
+ /**
347
+ *
348
+ * @public
349
+ * @environment client
350
+ */
52
351
  getAttributes,
53
352
  setDataType
54
- };
353
+ };
354
+
355
+ /**
356
+ *
357
+ * @name actions
358
+ * @kind attribute
359
+ * @environment client
360
+ */
361
+
362
+ /**
363
+ *
364
+ * @name array
365
+ * @kind attribute
366
+ * @environment client
367
+ */
368
+
369
+ /**
370
+ *
371
+ * @name database
372
+ * @kind attribute
373
+ * @environment client
374
+ */
375
+
376
+ /**
377
+ *
378
+ * @name href
379
+ * @kind attribute
380
+ * @environment client
381
+ */
382
+
383
+ /**
384
+ *
385
+ * @name render-json
386
+ * @kind attribute
387
+ * @environment client
388
+ */
389
+
390
+ /**
391
+ *
392
+ * @name socket-on
393
+ * @kind attribute
394
+ * @environment client
395
+ */
396
+
397
+ /**
398
+ *
399
+ * @name socket-status
400
+ * @kind attribute
401
+ * @environment client
402
+ */
403
+
404
+ /**
405
+ *
406
+ * @name source
407
+ * @kind attribute
408
+ * @environment client
409
+ */
410
+
411
+ /**
412
+ *
413
+ * @name src
414
+ * @kind attribute
415
+ * @environment client
416
+ */
417
+
418
+ /**
419
+ *
420
+ * @name storage
421
+ * @kind attribute
422
+ * @environment client
423
+ */
424
+
425
+ /**
426
+ *
427
+ * @name stream
428
+ * @kind attribute
429
+ * @environment client
430
+ */
431
+
432
+ /**
433
+ *
434
+ * @name target
435
+ * @kind attribute
436
+ * @environment client
437
+ */
438
+
439
+ /**
440
+ *
441
+ * @name value-query
442
+ * @kind attribute
443
+ * @environment client
444
+ */
445
+
446
+ /**
447
+ * Fetch content into any html element by using html5 attributes to query data stored in CoCreate headless CMS. If data is changed it will instantly reflect in html tags as changes are broadcasted in realtime
448
+ *
449
+ * @module elements
450
+ * @environment client
451
+ */
452
+
453
+ /**
454
+ * Declarative selectors recognized by this module.
455
+ *
456
+ * @name declarative-selectors
457
+ * @kind member
458
+ * @selectors
459
+ * @environment client
460
+ * @example
461
+ * ```json
462
+ * [
463
+ * "[actions]",
464
+ * "iframe",
465
+ * "[href], [target]",
466
+ * "[storage], [database], [array], [render-json]",
467
+ * "img, [src], [source], [stream]",
468
+ * "[value-query]",
469
+ * "form, fieldset",
470
+ * "[socket-on]",
471
+ * "[socket-status]"
472
+ * ]
473
+ * ```
474
+ */
@@ -157,11 +157,16 @@ export async function setData(element, data) {
157
157
  if (action && ["database", "array", "object", "key"].includes(action))
158
158
  continue;
159
159
 
160
- if (data.array.includes("aiConversations")) {
161
- console.log("testing ai conversations");
160
+ const { key, isRead, isUpdate, isListen, isCrdt } = getAttributes(el);
161
+ if (key !== undefined && typeof key !== "string") {
162
+ console.error("[CoCreate elements] Ignoring element with a non-string key", {
163
+ key,
164
+ keyType: typeof key,
165
+ element: el
166
+ });
167
+ continue;
162
168
  }
163
169
 
164
- const { key, isRead, isUpdate, isListen, isCrdt } = getAttributes(el);
165
170
  const hasRenderController =
166
171
  el.hasAttribute("render-query") || el.hasAttribute("render-json");
167
172
 
@@ -1,12 +1,12 @@
1
+ import "@cocreate/element-prototype";
1
2
  import Actions from "@cocreate/actions";
2
3
  import Observer from "@cocreate/observer";
3
4
  import Socket from "@cocreate/socket-client";
4
5
  import { dotNotationToObject, getValueFromObject, queryData, uuid } from "@cocreate/utils";
6
+ import { socketOnSelector, socketStatusSelector } from "./socketSelectors.js";
5
7
 
6
- const socketOnSelector = "[socket-on]";
7
8
  const socketOnElements = new WeakMap();
8
9
  const socketOnListeners = new Map();
9
- const socketStatusSelector = "[socket-status]";
10
10
  const socketStatusElements = new Set();
11
11
  let socketStatusListenersInitialized = false;
12
12
 
@@ -0,0 +1,3 @@
1
+ export const socketOnSelector = "[socket-on]";
2
+ export const socketStatusSelector = "[socket-status]";
3
+ export const socketActionSelector = "[actions]";
package/src/srcHandler.js CHANGED
@@ -1,3 +1,4 @@
1
+ import "@cocreate/element-prototype";
1
2
  import observer from "@cocreate/observer";
2
3
  import { getRelativePath, ObjectId } from "@cocreate/utils";
3
4
 
package/src/valueQuery.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import Observer from "@cocreate/observer";
2
2
  import "@cocreate/element-prototype";
3
3
  import { queryElements } from "@cocreate/utils";
4
-
5
- const selector = "[value-query]";
4
+ import { valueQuerySelector } from "./valueQuerySelector.js";
6
5
 
7
6
  /**
8
7
  * Initializes elements with value-* attributes to enable dynamic value retrieval and setting.
@@ -18,7 +17,7 @@ async function init(element) {
18
17
  ) {
19
18
  element = [element];
20
19
  } else if (!element) {
21
- element = document.querySelectorAll(selector);
20
+ element = document.querySelectorAll(valueQuerySelector);
22
21
  }
23
22
 
24
23
  for (let i = 0; i < element.length; i++) {
@@ -87,7 +86,7 @@ init();
87
86
  Observer.init({
88
87
  name: "CoCreateElementsValueAdded",
89
88
  types: ["addedNodes"],
90
- selector: selector,
89
+ selector: valueQuerySelector,
91
90
  callback: function (mutation) {
92
91
  init(mutation.target);
93
92
  }
@@ -0,0 +1 @@
1
+ export const valueQuerySelector = "[value-query]";
@@ -0,0 +1,132 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const test = require('node:test');
5
+ const vm = require('node:vm');
6
+
7
+ function documentStub() {
8
+ const listeners = new Set();
9
+ return {
10
+ nodeType: 9,
11
+ addEventListener: (type, callback) => listeners.add(callback),
12
+ removeEventListener: (type, callback) => listeners.delete(callback),
13
+ click(target) {
14
+ const event = { target, prevented: false, preventDefault() { this.prevented = true; } };
15
+ for (const listener of listeners) listener(event);
16
+ return event;
17
+ }
18
+ };
19
+ }
20
+
21
+ async function setup() {
22
+ const document = documentStub(), opened = [], definitions = [];
23
+ const location = { href: 'https://example.test/' };
24
+ const actions = { init: entries => definitions.push(...entries), href() { throw new Error('Default href'); } };
25
+ let targets = [];
26
+ const stubs = {
27
+ '@cocreate/actions': { default: actions },
28
+ '@cocreate/utils': { queryElements: () => targets }
29
+ };
30
+ const context = vm.createContext({ document, location, URL, window: { open: (...args) => opened.push(args) } });
31
+ const module = new vm.SourceTextModule(fs.readFileSync(path.join(__dirname, '../src/hrefHandler.js'), 'utf8'), { context });
32
+ await module.link(name => {
33
+ const exports = stubs[name];
34
+ return new vm.SyntheticModule(Object.keys(exports), function () {
35
+ for (const [key, value] of Object.entries(exports)) this.setExport(key, value);
36
+ }, { context });
37
+ });
38
+ await module.evaluate();
39
+ function element(attributes = {}, parent = null) {
40
+ const node = {
41
+ ownerDocument: document, parent, children: [],
42
+ hasAttribute: name => Object.hasOwn(attributes, name),
43
+ getAttribute: name => attributes[name] ?? null,
44
+ matches: selector => selector.split(',').some(part => Object.hasOwn(attributes, part.trim().slice(1, -1))),
45
+ closest(selector) { return this.matches(selector) ? this : parent?.closest(selector) || null; },
46
+ querySelector(selector) { return this.children.find(child => child.matches(selector)) || null; }
47
+ };
48
+ parent?.children.push(node);
49
+ return node;
50
+ }
51
+ return { document, location, opened, actions, definitions, element, module: module.namespace,
52
+ select: value => { targets = value; } };
53
+ }
54
+
55
+ test('automatic setup registers actions and exposes only navigation controls', async () => {
56
+ const f = await setup();
57
+ assert.deepEqual(Object.keys(f.module).sort(), ['default', 'disableHref', 'enableHref', 'open']);
58
+ assert.deepEqual(Object.keys(f.module.default).sort(), ['disableHref', 'enableHref', 'open']);
59
+ for (const name of ['open', 'disableHref', 'enableHref']) {
60
+ assert.equal(f.module.default[name], f.module[name]);
61
+ }
62
+ assert.equal(typeof f.actions.href, 'function');
63
+ assert.equal(f.document.link.islink, 'true');
64
+ assert.deepEqual(f.definitions.map(definition => definition.name), ['disableHref', 'enableHref']);
65
+ });
66
+
67
+ test('delegation handles newly inserted links and preserves window targeting', async () => {
68
+ const f = await setup();
69
+ const link = f.element({ href: '/next', target: '_window' });
70
+ const icon = f.element({}, link);
71
+ assert.equal(f.document.click(icon).prevented, true);
72
+ assert.deepEqual(f.opened, [['/next', '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes']]);
73
+ });
74
+
75
+ test('direct clicks defer action links to the chain href hook', async () => {
76
+ const f = await setup();
77
+ const link = f.element({ actions: 'save', href: '/next' });
78
+ const event = f.document.click(link);
79
+ assert.equal(event.prevented, false);
80
+ assert.equal(f.location.href, 'https://example.test/');
81
+ f.actions.href(link, { event });
82
+ assert.equal(event.prevented, true);
83
+ assert.equal(f.location.href, '/next');
84
+ });
85
+
86
+ test('the chain hook resolves child links and leaves downloads alone', async () => {
87
+ const f = await setup();
88
+ const button = f.element({ actions: 'save' });
89
+ f.element({ href: '/next', target: 'preview' }, button);
90
+ f.actions.href(button);
91
+ assert.deepEqual(f.opened, [['/next', 'preview']]);
92
+ const download = f.element({ href: '/file', download: '' });
93
+ assert.equal(f.document.click(download).prevented, false);
94
+ f.actions.href(download);
95
+ assert.equal(f.location.href, 'https://example.test/');
96
+ });
97
+
98
+ test('enable and disable actions update document state and complete their chains', async () => {
99
+ const f = await setup();
100
+ const button = f.element(), link = f.element({ href: '/next' });
101
+ let completed = 0;
102
+ f.definitions[0].callback({ element: button, end() { completed++; } });
103
+ assert.equal(f.document.click(link).prevented, true);
104
+ f.actions.href(link);
105
+ assert.equal(f.location.href, 'https://example.test/');
106
+ f.definitions[1].callback({ element: button, end() { completed++; } });
107
+ assert.equal(completed, 2);
108
+ f.document.click(link);
109
+ assert.equal(f.location.href, '/next');
110
+ });
111
+
112
+ test('link-query scopes enable and disable to the selected iframe document', async () => {
113
+ const f = await setup();
114
+ const frameDocument = documentStub();
115
+ f.select([{ contentDocument: frameDocument }]);
116
+ const button = f.element({ 'link-query': '#preview' });
117
+ f.module.disableHref(button);
118
+ assert.equal(frameDocument.link.islink, 'false');
119
+ assert.equal(f.document.link.islink, 'true');
120
+ f.module.enableHref(button);
121
+ assert.equal(frameDocument.link.islink, 'true');
122
+ });
123
+
124
+ test('applications can replace the href hook after the feature loads', async () => {
125
+ const f = await setup();
126
+ const custom = () => {};
127
+ f.actions.href = custom;
128
+ const button = f.element();
129
+ f.module.disableHref(button);
130
+ f.module.enableHref(button);
131
+ assert.equal(f.actions.href, custom);
132
+ });