@cocreate/elements 1.48.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/src/index.js CHANGED
@@ -19,36 +19,469 @@
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 member
359
+ * @attribute
360
+ * @environment client
361
+ */
362
+
363
+ /**
364
+ *
365
+ * @name array
366
+ * @kind member
367
+ * @attribute
368
+ * @environment client
369
+ */
370
+
371
+ /**
372
+ *
373
+ * @name database
374
+ * @kind member
375
+ * @attribute
376
+ * @environment client
377
+ */
378
+
379
+ /**
380
+ *
381
+ * @name href
382
+ * @kind member
383
+ * @attribute
384
+ * @environment client
385
+ */
386
+
387
+ /**
388
+ *
389
+ * @name render-json
390
+ * @kind member
391
+ * @attribute
392
+ * @environment client
393
+ */
394
+
395
+ /**
396
+ *
397
+ * @name socket-on
398
+ * @kind member
399
+ * @attribute
400
+ * @environment client
401
+ */
402
+
403
+ /**
404
+ *
405
+ * @name socket-status
406
+ * @kind member
407
+ * @attribute
408
+ * @environment client
409
+ */
410
+
411
+ /**
412
+ *
413
+ * @name source
414
+ * @kind member
415
+ * @attribute
416
+ * @environment client
417
+ */
418
+
419
+ /**
420
+ *
421
+ * @name src
422
+ * @kind member
423
+ * @attribute
424
+ * @environment client
425
+ */
426
+
427
+ /**
428
+ *
429
+ * @name storage
430
+ * @kind member
431
+ * @attribute
432
+ * @environment client
433
+ */
434
+
435
+ /**
436
+ *
437
+ * @name stream
438
+ * @kind member
439
+ * @attribute
440
+ * @environment client
441
+ */
442
+
443
+ /**
444
+ *
445
+ * @name target
446
+ * @kind member
447
+ * @attribute
448
+ * @environment client
449
+ */
450
+
451
+ /**
452
+ *
453
+ * @name value-query
454
+ * @kind member
455
+ * @attribute
456
+ * @environment client
457
+ */
458
+
459
+ /**
460
+ * 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
461
+ *
462
+ * @module elements
463
+ * @environment client
464
+ */
465
+
466
+ /**
467
+ * Declarative selectors recognized by this module.
468
+ *
469
+ * @name declarative-selectors
470
+ * @kind member
471
+ * @selectors
472
+ * @environment client
473
+ * @example
474
+ * ```json
475
+ * [
476
+ * "[actions]",
477
+ * "iframe",
478
+ * "[href], [target]",
479
+ * "[storage], [database], [array], [render-json]",
480
+ * "img, [src], [source], [stream]",
481
+ * "[value-query]",
482
+ * "form, fieldset",
483
+ * "[socket-on]",
484
+ * "[socket-status]"
485
+ * ]
486
+ * ```
487
+ */
@@ -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
+ });