@hyperfixi/behaviors 2.5.1 → 2.7.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 +100 -0
- package/dist/all.browser.global.js +43 -40
- package/dist/all.browser.global.js.map +1 -1
- package/dist/autodismiss.browser.global.js +2 -2
- package/dist/autodismiss.browser.global.js.map +1 -1
- package/dist/behaviors/autodismiss.cjs +5 -54
- package/dist/behaviors/autodismiss.cjs.map +1 -1
- package/dist/behaviors/autodismiss.d.cts +3 -2
- package/dist/behaviors/autodismiss.d.ts +3 -2
- package/dist/behaviors/autodismiss.js +1 -1
- package/dist/behaviors/clickoutside.cjs +6 -28
- package/dist/behaviors/clickoutside.cjs.map +1 -1
- package/dist/behaviors/clickoutside.d.cts +3 -2
- package/dist/behaviors/clickoutside.d.ts +3 -2
- package/dist/behaviors/clickoutside.js +2 -2
- package/dist/behaviors/clipboard.cjs +30 -86
- package/dist/behaviors/clipboard.cjs.map +1 -1
- package/dist/behaviors/clipboard.d.cts +3 -2
- package/dist/behaviors/clipboard.d.ts +3 -2
- package/dist/behaviors/clipboard.js +2 -2
- package/dist/behaviors/draggable.cjs +6 -41
- package/dist/behaviors/draggable.cjs.map +1 -1
- package/dist/behaviors/draggable.d.cts +3 -3
- package/dist/behaviors/draggable.d.ts +3 -3
- package/dist/behaviors/draggable.js +2 -2
- package/dist/behaviors/focustrap.cjs +5 -81
- package/dist/behaviors/focustrap.cjs.map +1 -1
- package/dist/behaviors/focustrap.d.cts +3 -2
- package/dist/behaviors/focustrap.d.ts +3 -2
- package/dist/behaviors/focustrap.js +1 -1
- package/dist/behaviors/removable.d.cts +1 -1
- package/dist/behaviors/removable.d.ts +1 -1
- package/dist/behaviors/resizable.cjs +9 -49
- package/dist/behaviors/resizable.cjs.map +1 -1
- package/dist/behaviors/resizable.d.cts +3 -2
- package/dist/behaviors/resizable.d.ts +3 -2
- package/dist/behaviors/resizable.js +2 -2
- package/dist/behaviors/scrollreveal.cjs +5 -32
- package/dist/behaviors/scrollreveal.cjs.map +1 -1
- package/dist/behaviors/scrollreveal.d.cts +3 -2
- package/dist/behaviors/scrollreveal.d.ts +3 -2
- package/dist/behaviors/scrollreveal.js +1 -1
- package/dist/behaviors/sortable.cjs +8 -43
- package/dist/behaviors/sortable.cjs.map +1 -1
- package/dist/behaviors/sortable.d.cts +3 -2
- package/dist/behaviors/sortable.d.ts +3 -2
- package/dist/behaviors/sortable.js +2 -2
- package/dist/behaviors/tabs.cjs +5 -120
- package/dist/behaviors/tabs.cjs.map +1 -1
- package/dist/behaviors/tabs.d.cts +3 -2
- package/dist/behaviors/tabs.d.ts +3 -2
- package/dist/behaviors/tabs.js +1 -1
- package/dist/behaviors/toggleable.cjs +5 -27
- package/dist/behaviors/toggleable.cjs.map +1 -1
- package/dist/behaviors/toggleable.d.cts +3 -3
- package/dist/behaviors/toggleable.d.ts +3 -3
- package/dist/behaviors/toggleable.js +1 -1
- package/dist/{chunk-SLOXO6IY.js → chunk-26IX7QU7.js} +2 -2
- package/dist/{chunk-SLOXO6IY.js.map → chunk-26IX7QU7.js.map} +1 -1
- package/dist/chunk-2HUF6RMI.js +40 -0
- package/dist/chunk-2HUF6RMI.js.map +1 -0
- package/dist/{chunk-ZBSWTV6I.js → chunk-5LFOFVZ3.js} +5 -5
- package/dist/chunk-5LFOFVZ3.js.map +1 -0
- package/dist/{chunk-BBIIOGMF.js → chunk-6EG2MNC7.js} +6 -6
- package/dist/chunk-7NGOA3T5.js +40 -0
- package/dist/chunk-7NGOA3T5.js.map +1 -0
- package/dist/{chunk-ZZ2WSAX3.js → chunk-7TPDLB7W.js} +7 -29
- package/dist/chunk-7TPDLB7W.js.map +1 -0
- package/dist/chunk-CR3J7VV6.js +40 -0
- package/dist/chunk-CR3J7VV6.js.map +1 -0
- package/dist/chunk-EDBG2F57.js +40 -0
- package/dist/chunk-EDBG2F57.js.map +1 -0
- package/dist/{chunk-G6WR2AYR.js → chunk-FBQ3X3UY.js} +2 -2
- package/dist/chunk-FBQ3X3UY.js.map +1 -0
- package/dist/chunk-HX663BJ4.js +40 -0
- package/dist/chunk-HX663BJ4.js.map +1 -0
- package/dist/chunk-KROW43CG.js +40 -0
- package/dist/chunk-KROW43CG.js.map +1 -0
- package/dist/chunk-ORBJ7MRW.js +40 -0
- package/dist/chunk-ORBJ7MRW.js.map +1 -0
- package/dist/{chunk-V2M4W3LO.js → chunk-PDMOOWFW.js} +4 -4
- package/dist/chunk-PDMOOWFW.js.map +1 -0
- package/dist/chunk-SLSCZV4A.js +40 -0
- package/dist/chunk-SLSCZV4A.js.map +1 -0
- package/dist/chunk-WKBRZGGL.js +40 -0
- package/dist/chunk-WKBRZGGL.js.map +1 -0
- package/dist/{chunk-WOTZO7K4.js → chunk-YXS27XM6.js} +26 -23
- package/dist/chunk-YXS27XM6.js.map +1 -0
- package/dist/clickoutside.browser.global.js +3 -3
- package/dist/clickoutside.browser.global.js.map +1 -1
- package/dist/clipboard.browser.global.js +27 -24
- package/dist/clipboard.browser.global.js.map +1 -1
- package/dist/core.browser.global.js +3 -3
- package/dist/core.browser.global.js.map +1 -1
- package/dist/draggable.browser.global.js +2 -2
- package/dist/draggable.browser.global.js.map +1 -1
- package/dist/focustrap.browser.global.js +2 -2
- package/dist/focustrap.browser.global.js.map +1 -1
- package/dist/index.cjs +120 -563
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -3
- package/dist/index.d.ts +41 -3
- package/dist/index.js +44 -16
- package/dist/index.js.map +1 -1
- package/dist/loaders.cjs +86 -563
- package/dist/loaders.cjs.map +1 -1
- package/dist/loaders.js +6 -6
- package/dist/registry.d.cts +1 -1
- package/dist/registry.d.ts +1 -1
- package/dist/resizable.browser.global.js +5 -5
- package/dist/resizable.browser.global.js.map +1 -1
- package/dist/resolver.browser.global.js +34 -31
- package/dist/resolver.browser.global.js.map +1 -1
- package/dist/schemas/index.cjs +34 -31
- package/dist/schemas/index.cjs.map +1 -1
- package/dist/schemas/index.d.cts +2 -2
- package/dist/schemas/index.d.ts +2 -2
- package/dist/schemas/index.js +5 -5
- package/dist/scrollreveal.browser.global.js +2 -2
- package/dist/scrollreveal.browser.global.js.map +1 -1
- package/dist/sortable.browser.global.js +4 -4
- package/dist/sortable.browser.global.js.map +1 -1
- package/dist/tabs.browser.global.js +2 -2
- package/dist/tabs.browser.global.js.map +1 -1
- package/dist/toggleable.browser.global.js +2 -2
- package/dist/toggleable.browser.global.js.map +1 -1
- package/dist/{types-BY-jB5FV.d.ts → types-DCCtVQS5.d.cts} +1 -1
- package/dist/{types-BY-jB5FV.d.cts → types-DCCtVQS5.d.ts} +1 -1
- package/package.json +4 -4
- package/src/behaviors/autodismiss.test.ts +15 -9
- package/src/behaviors/autodismiss.ts +15 -70
- package/src/behaviors/clickoutside.test.ts +17 -9
- package/src/behaviors/clickoutside.ts +13 -39
- package/src/behaviors/clipboard.test.ts +16 -10
- package/src/behaviors/clipboard.ts +15 -93
- package/src/behaviors/curated-runtime.test.ts +158 -0
- package/src/behaviors/draggable.ts +15 -59
- package/src/behaviors/experimental-runtime.test.ts +136 -0
- package/src/behaviors/focustrap.test.ts +15 -9
- package/src/behaviors/focustrap.ts +17 -121
- package/src/behaviors/integration.test.ts +9 -64
- package/src/behaviors/optional-runtime.test.ts +251 -0
- package/src/behaviors/resizable.ts +14 -61
- package/src/behaviors/scrollreveal.test.ts +17 -9
- package/src/behaviors/scrollreveal.ts +17 -41
- package/src/behaviors/sortable.ts +15 -58
- package/src/behaviors/tabs.test.ts +16 -10
- package/src/behaviors/tabs.ts +18 -160
- package/src/behaviors/toggleable.ts +14 -41
- package/src/curation.test.ts +45 -0
- package/src/curation.ts +58 -0
- package/src/index.ts +14 -0
- package/src/registry.test.ts +6 -11
- package/src/schemas/clickoutside.schema.ts +1 -1
- package/src/schemas/clipboard.schema.ts +25 -22
- package/src/schemas/draggable.schema.ts +2 -1
- package/src/schemas/resizable.schema.ts +5 -4
- package/src/schemas/sortable.schema.ts +4 -3
- package/dist/chunk-7O7UOSBH.js +0 -99
- package/dist/chunk-7O7UOSBH.js.map +0 -1
- package/dist/chunk-EKKJVXDZ.js +0 -67
- package/dist/chunk-EKKJVXDZ.js.map +0 -1
- package/dist/chunk-F3CASJ7Q.js +0 -116
- package/dist/chunk-F3CASJ7Q.js.map +0 -1
- package/dist/chunk-G6WR2AYR.js.map +0 -1
- package/dist/chunk-GALKOMJ5.js +0 -62
- package/dist/chunk-GALKOMJ5.js.map +0 -1
- package/dist/chunk-IMDFEGIT.js +0 -75
- package/dist/chunk-IMDFEGIT.js.map +0 -1
- package/dist/chunk-IT6TYVMW.js +0 -80
- package/dist/chunk-IT6TYVMW.js.map +0 -1
- package/dist/chunk-LQJOQTBZ.js +0 -89
- package/dist/chunk-LQJOQTBZ.js.map +0 -1
- package/dist/chunk-S4QDV2MH.js +0 -75
- package/dist/chunk-S4QDV2MH.js.map +0 -1
- package/dist/chunk-V2M4W3LO.js.map +0 -1
- package/dist/chunk-WOTZO7K4.js.map +0 -1
- package/dist/chunk-WPOQIMCC.js +0 -155
- package/dist/chunk-WPOQIMCC.js.map +0 -1
- package/dist/chunk-ZBSWTV6I.js.map +0 -1
- package/dist/chunk-ZZ2WSAX3.js.map +0 -1
- /package/dist/{chunk-BBIIOGMF.js.map → chunk-6EG2MNC7.js.map} +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
/**
|
|
3
|
+
* Curated behaviors — REAL runtime behavior tests (Phase 2).
|
|
4
|
+
*
|
|
5
|
+
* These exercise each curated behavior's actual hyperscript `source` through the
|
|
6
|
+
* real @hyperfixi/core runtime: register → install on an element → drive the DOM →
|
|
7
|
+
* assert both the effect AND the documented lifecycle events. This is the
|
|
8
|
+
* "parses ≠ works" guard (BEHAVIORS_CONSOLIDATION_PLAN.md §2/§5) — the mock-based
|
|
9
|
+
* unit tests only prove `register*()` calls compileSync; these prove the js()
|
|
10
|
+
* bodies actually do the right thing at runtime.
|
|
11
|
+
*
|
|
12
|
+
* These tests already paid for themselves: they surfaced two source bugs the
|
|
13
|
+
* imperative path was masking — ClickOutside read `event.target` without passing
|
|
14
|
+
* `event` into its js() block, and Clipboard used top-level `await` (invalid in a
|
|
15
|
+
* js() block). Both are fixed in the schema sources.
|
|
16
|
+
*/
|
|
17
|
+
import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest';
|
|
18
|
+
import { hyperscript } from '@hyperfixi/core';
|
|
19
|
+
import { registerToggleable } from './toggleable';
|
|
20
|
+
import { registerRemovable } from './removable';
|
|
21
|
+
import { registerClickOutside } from './clickoutside';
|
|
22
|
+
import { registerClipboard } from './clipboard';
|
|
23
|
+
import { registerAutoDismiss } from './autodismiss';
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
const hf = hyperscript as any;
|
|
27
|
+
|
|
28
|
+
const tick = (ms = 30) => new Promise(r => setTimeout(r, ms));
|
|
29
|
+
|
|
30
|
+
async function install(code: string, el: HTMLElement): Promise<void> {
|
|
31
|
+
const r = hyperscript.compileSync(code, { traditional: true });
|
|
32
|
+
if (!r.ok) throw new Error(`install compile failed: ${JSON.stringify(r.errors)}`);
|
|
33
|
+
await hyperscript.execute(r.ast, hyperscript.createContext(el));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
// Behavior registry is a runtime singleton; register the curated set once.
|
|
38
|
+
await registerToggleable(hf);
|
|
39
|
+
await registerRemovable(hf);
|
|
40
|
+
await registerClickOutside(hf);
|
|
41
|
+
await registerClipboard(hf);
|
|
42
|
+
await registerAutoDismiss(hf);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
document.body.innerHTML = '';
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('Toggleable — runtime', () => {
|
|
50
|
+
it('toggles the parameterized class and fires on/off lifecycle events', async () => {
|
|
51
|
+
const el = document.createElement('button');
|
|
52
|
+
document.body.appendChild(el);
|
|
53
|
+
const on = vi.fn();
|
|
54
|
+
const off = vi.fn();
|
|
55
|
+
el.addEventListener('toggleable:on', on);
|
|
56
|
+
el.addEventListener('toggleable:off', off);
|
|
57
|
+
|
|
58
|
+
await install('install Toggleable(cls: "hl")', el);
|
|
59
|
+
|
|
60
|
+
el.click();
|
|
61
|
+
await tick();
|
|
62
|
+
expect(el.classList.contains('hl')).toBe(true);
|
|
63
|
+
expect(on).toHaveBeenCalledTimes(1);
|
|
64
|
+
|
|
65
|
+
el.click();
|
|
66
|
+
await tick();
|
|
67
|
+
expect(el.classList.contains('hl')).toBe(false);
|
|
68
|
+
expect(off).toHaveBeenCalledTimes(1);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('Removable — runtime', () => {
|
|
73
|
+
it('removes the element on click and fires before/removed events', async () => {
|
|
74
|
+
const el = document.createElement('div');
|
|
75
|
+
el.id = 'rm-target';
|
|
76
|
+
document.body.appendChild(el);
|
|
77
|
+
const before = vi.fn();
|
|
78
|
+
const removed = vi.fn();
|
|
79
|
+
el.addEventListener('removable:before', before);
|
|
80
|
+
document.addEventListener('removable:removed', removed, { once: true });
|
|
81
|
+
|
|
82
|
+
await install('install Removable', el);
|
|
83
|
+
|
|
84
|
+
el.click();
|
|
85
|
+
await tick();
|
|
86
|
+
|
|
87
|
+
expect(document.getElementById('rm-target')).toBeNull();
|
|
88
|
+
expect(before).toHaveBeenCalledTimes(1);
|
|
89
|
+
expect(removed).toHaveBeenCalledTimes(1);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('ClickOutside — runtime', () => {
|
|
94
|
+
it('fires clickoutside for outside presses but not inside', async () => {
|
|
95
|
+
const el = document.createElement('div');
|
|
96
|
+
const outside = document.createElement('button');
|
|
97
|
+
document.body.append(el, outside);
|
|
98
|
+
const fired = vi.fn();
|
|
99
|
+
el.addEventListener('clickoutside', fired);
|
|
100
|
+
|
|
101
|
+
await install('install ClickOutside', el);
|
|
102
|
+
|
|
103
|
+
// The source passes `event` into its js() block, so dispatch sets event.target.
|
|
104
|
+
const press = (target: HTMLElement) =>
|
|
105
|
+
target.dispatchEvent(new Event('pointerdown', { bubbles: true }));
|
|
106
|
+
|
|
107
|
+
press(outside);
|
|
108
|
+
await tick();
|
|
109
|
+
expect(fired).toHaveBeenCalledTimes(1);
|
|
110
|
+
|
|
111
|
+
fired.mockClear();
|
|
112
|
+
press(el);
|
|
113
|
+
await tick();
|
|
114
|
+
expect(fired).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('Clipboard — runtime', () => {
|
|
119
|
+
it('writes literal text, adds .copied feedback, and fires clipboard:copied', async () => {
|
|
120
|
+
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
121
|
+
Object.defineProperty(navigator, 'clipboard', { value: { writeText }, configurable: true });
|
|
122
|
+
|
|
123
|
+
const el = document.createElement('button');
|
|
124
|
+
document.body.appendChild(el);
|
|
125
|
+
const copied = vi.fn();
|
|
126
|
+
el.addEventListener('clipboard:copied', copied);
|
|
127
|
+
|
|
128
|
+
await install('install Clipboard(text: "hello")', el);
|
|
129
|
+
|
|
130
|
+
el.click();
|
|
131
|
+
await tick();
|
|
132
|
+
|
|
133
|
+
expect(writeText).toHaveBeenCalledWith('hello');
|
|
134
|
+
expect(el.classList.contains('copied')).toBe(true);
|
|
135
|
+
expect(copied).toHaveBeenCalledTimes(1);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('AutoDismiss — runtime', () => {
|
|
140
|
+
it('fires start, then removes the element after the delay and fires dismissed', async () => {
|
|
141
|
+
const el = document.createElement('div');
|
|
142
|
+
el.id = 'ad-target';
|
|
143
|
+
document.body.appendChild(el);
|
|
144
|
+
const start = vi.fn();
|
|
145
|
+
const dismissed = vi.fn();
|
|
146
|
+
el.addEventListener('autodismiss:start', start);
|
|
147
|
+
document.addEventListener('autodismiss:dismissed', dismissed, { once: true });
|
|
148
|
+
|
|
149
|
+
await install('install AutoDismiss(delay: 10)', el);
|
|
150
|
+
|
|
151
|
+
expect(start).toHaveBeenCalledTimes(1);
|
|
152
|
+
|
|
153
|
+
await tick(80);
|
|
154
|
+
|
|
155
|
+
expect(document.getElementById('ad-target')).toBeNull();
|
|
156
|
+
expect(dismissed).toHaveBeenCalledTimes(1);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Draggable Behavior
|
|
2
|
+
* Draggable Behavior
|
|
3
3
|
*
|
|
4
|
-
* Makes elements draggable with pointer events.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Makes elements draggable with pointer events. Compiled from its hyperscript
|
|
5
|
+
* `source` (the single source of truth shared with the CDN resolver bundle and
|
|
6
|
+
* patterns-reference) — no imperative installer. The source uses
|
|
7
|
+
* `repeat until event pointerup` + `wait for pointermove or pointerup` to run the
|
|
8
|
+
* drag loop, `measure` for the start offset, and `add { left: ${…}px }` to move.
|
|
7
9
|
*
|
|
8
10
|
* @example
|
|
9
11
|
* ```html
|
|
@@ -27,51 +29,8 @@ export const draggableSource = draggableSchema.source;
|
|
|
27
29
|
export const draggableMetadata = draggableSchema;
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*/
|
|
33
|
-
function installDraggable(element: HTMLElement, params: Record<string, any>): void {
|
|
34
|
-
// Resolve drag handle: parameter value, CSS selector, or the element itself
|
|
35
|
-
let dragHandle: HTMLElement = element;
|
|
36
|
-
if (params.dragHandle && params.dragHandle !== 'me') {
|
|
37
|
-
if (typeof params.dragHandle === 'string') {
|
|
38
|
-
const found = element.querySelector(params.dragHandle);
|
|
39
|
-
if (found instanceof HTMLElement) dragHandle = found;
|
|
40
|
-
} else if (params.dragHandle instanceof HTMLElement) {
|
|
41
|
-
dragHandle = params.dragHandle;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
dragHandle.addEventListener('pointerdown', (e: PointerEvent) => {
|
|
46
|
-
e.preventDefault();
|
|
47
|
-
e.stopPropagation();
|
|
48
|
-
element.dispatchEvent(new CustomEvent('draggable:start', { bubbles: true }));
|
|
49
|
-
|
|
50
|
-
const startX = element.offsetLeft;
|
|
51
|
-
const startY = element.offsetTop;
|
|
52
|
-
const xoff = e.clientX - startX;
|
|
53
|
-
const yoff = e.clientY - startY;
|
|
54
|
-
|
|
55
|
-
function onMove(ev: PointerEvent) {
|
|
56
|
-
element.style.left = `${ev.clientX - xoff}px`;
|
|
57
|
-
element.style.top = `${ev.clientY - yoff}px`;
|
|
58
|
-
element.dispatchEvent(new CustomEvent('draggable:move', { bubbles: true }));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function onUp() {
|
|
62
|
-
document.removeEventListener('pointermove', onMove);
|
|
63
|
-
document.removeEventListener('pointerup', onUp);
|
|
64
|
-
element.dispatchEvent(new CustomEvent('draggable:end', { bubbles: true }));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
document.addEventListener('pointermove', onMove);
|
|
68
|
-
document.addEventListener('pointerup', onUp);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Register the Draggable behavior with LokaScript.
|
|
74
|
-
* Uses imperative installer via synthetic behavior node.
|
|
32
|
+
* Register the Draggable behavior with LokaScript by compiling its hyperscript
|
|
33
|
+
* source and executing the resulting behavior definition.
|
|
75
34
|
*/
|
|
76
35
|
export async function registerDraggable(hyperfixi?: LokaScriptInstance): Promise<void> {
|
|
77
36
|
const hf = hyperfixi || resolveRuntime();
|
|
@@ -82,17 +41,14 @@ export async function registerDraggable(hyperfixi?: LokaScriptInstance): Promise
|
|
|
82
41
|
);
|
|
83
42
|
}
|
|
84
43
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
eventHandlers: [],
|
|
92
|
-
imperativeInstaller: installDraggable,
|
|
93
|
-
};
|
|
44
|
+
const result = hf.compileSync(draggableSchema.source, { traditional: true });
|
|
45
|
+
|
|
46
|
+
if (!result.ok) {
|
|
47
|
+
throw new Error(`Failed to compile Draggable behavior: ${JSON.stringify(result.errors)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
94
50
|
const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
|
|
95
|
-
await hf.execute(
|
|
51
|
+
await hf.execute(result.ast, ctx);
|
|
96
52
|
}
|
|
97
53
|
|
|
98
54
|
// Auto-register when loaded as a script tag
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
/**
|
|
3
|
+
* Experimental behaviors — REAL runtime tests.
|
|
4
|
+
*
|
|
5
|
+
* Draggable / Sortable / Resizable used to run imperative JS installers. They now
|
|
6
|
+
* compile from their hyperscript `source` like every other behavior (the
|
|
7
|
+
* "no imperative JS" rule — no behavior may carry an imperative installer field).
|
|
8
|
+
* These tests are the "parses ≠ works" guard: register → install → drive pointer
|
|
9
|
+
* events → assert the DOM effect AND the documented lifecycle events.
|
|
10
|
+
*
|
|
11
|
+
* happy-dom has no PointerEvent and zero layout, so we drive `MouseEvent`s (which
|
|
12
|
+
* carry clientX/clientY) and assert deltas, which are layout-independent.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, beforeAll, afterEach } from 'vitest';
|
|
15
|
+
import { hyperscript } from '@hyperfixi/core';
|
|
16
|
+
import { registerDraggable } from './draggable';
|
|
17
|
+
import { registerSortable } from './sortable';
|
|
18
|
+
import { registerResizable } from './resizable';
|
|
19
|
+
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
const hf = hyperscript as any;
|
|
22
|
+
const tick = (ms = 30) => new Promise(r => setTimeout(r, ms));
|
|
23
|
+
|
|
24
|
+
function pointer(type: string, x: number, y: number): MouseEvent {
|
|
25
|
+
return new MouseEvent(type, { clientX: x, clientY: y, bubbles: true, cancelable: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function install(code: string, el: HTMLElement): Promise<void> {
|
|
29
|
+
const r = hyperscript.compileSync(code, { traditional: true });
|
|
30
|
+
if (!r.ok) throw new Error(`install compile failed: ${JSON.stringify(r.errors)}`);
|
|
31
|
+
await hyperscript.execute(r.ast, hyperscript.createContext(el));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
await registerDraggable(hf);
|
|
36
|
+
await registerSortable(hf);
|
|
37
|
+
await registerResizable(hf);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
document.body.innerHTML = '';
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('Draggable — runtime (source-compiled)', () => {
|
|
45
|
+
it('moves the element by the pointer delta and fires start/move/end', async () => {
|
|
46
|
+
const el = document.createElement('div');
|
|
47
|
+
el.style.position = 'absolute';
|
|
48
|
+
el.style.left = '0px';
|
|
49
|
+
el.style.top = '0px';
|
|
50
|
+
document.body.appendChild(el);
|
|
51
|
+
|
|
52
|
+
const ev: string[] = [];
|
|
53
|
+
el.addEventListener('draggable:start', () => ev.push('start'));
|
|
54
|
+
el.addEventListener('draggable:move', () => ev.push('move'));
|
|
55
|
+
el.addEventListener('draggable:end', () => ev.push('end'));
|
|
56
|
+
|
|
57
|
+
await install('install Draggable', el);
|
|
58
|
+
|
|
59
|
+
el.dispatchEvent(pointer('pointerdown', 10, 10));
|
|
60
|
+
await tick();
|
|
61
|
+
document.dispatchEvent(pointer('pointermove', 60, 80));
|
|
62
|
+
await tick();
|
|
63
|
+
document.dispatchEvent(pointer('pointerup', 60, 80));
|
|
64
|
+
await tick(40);
|
|
65
|
+
|
|
66
|
+
// measure x/y = 0 in happy-dom → xoff = 10, so left = 60 - 10 = 50, top = 80 - 10 = 70
|
|
67
|
+
expect(el.style.left).toBe('50px');
|
|
68
|
+
expect(el.style.top).toBe('70px');
|
|
69
|
+
expect(ev).toContain('start');
|
|
70
|
+
expect(ev).toContain('move');
|
|
71
|
+
expect(ev).toContain('end');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('Sortable — runtime (source-compiled)', () => {
|
|
76
|
+
it('toggles the drag class on the <li> and fires start/move/end', async () => {
|
|
77
|
+
const ul = document.createElement('ul');
|
|
78
|
+
ul.innerHTML = '<li id="a">A</li><li id="b">B</li>';
|
|
79
|
+
document.body.appendChild(ul);
|
|
80
|
+
|
|
81
|
+
const ev: string[] = [];
|
|
82
|
+
ul.addEventListener('sortable:start', () => ev.push('start'));
|
|
83
|
+
ul.addEventListener('sortable:move', () => ev.push('move'));
|
|
84
|
+
ul.addEventListener('sortable:end', () => ev.push('end'));
|
|
85
|
+
|
|
86
|
+
await install('install Sortable', ul);
|
|
87
|
+
|
|
88
|
+
const li = ul.querySelector('#a') as HTMLElement;
|
|
89
|
+
li.dispatchEvent(pointer('pointerdown', 0, 5));
|
|
90
|
+
await tick();
|
|
91
|
+
const classDuringDrag = li.classList.contains('sorting');
|
|
92
|
+
document.dispatchEvent(pointer('pointermove', 0, 40));
|
|
93
|
+
await tick();
|
|
94
|
+
document.dispatchEvent(pointer('pointerup', 0, 40));
|
|
95
|
+
await tick(40);
|
|
96
|
+
|
|
97
|
+
expect(classDuringDrag).toBe(true); // dragClass added on pointerdown
|
|
98
|
+
expect(li.classList.contains('sorting')).toBe(false); // removed on pointerup
|
|
99
|
+
expect(ev).toContain('start');
|
|
100
|
+
expect(ev).toContain('move');
|
|
101
|
+
expect(ev).toContain('end');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('Resizable — runtime (source-compiled)', () => {
|
|
106
|
+
it('resizes by the pointer delta and fires start/resize/end', async () => {
|
|
107
|
+
const box = document.createElement('div');
|
|
108
|
+
box.style.width = '100px';
|
|
109
|
+
box.style.height = '100px';
|
|
110
|
+
document.body.appendChild(box);
|
|
111
|
+
|
|
112
|
+
const ev: string[] = [];
|
|
113
|
+
box.addEventListener('resizable:start', () => ev.push('start'));
|
|
114
|
+
box.addEventListener('resizable:resize', () => ev.push('resize'));
|
|
115
|
+
box.addEventListener('resizable:end', () => ev.push('end'));
|
|
116
|
+
|
|
117
|
+
await install(
|
|
118
|
+
'install Resizable(minWidth: 10, minHeight: 10, maxWidth: 999, maxHeight: 999)',
|
|
119
|
+
box
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
box.dispatchEvent(pointer('pointerdown', 10, 10));
|
|
123
|
+
await tick();
|
|
124
|
+
document.dispatchEvent(pointer('pointermove', 60, 40));
|
|
125
|
+
await tick();
|
|
126
|
+
document.dispatchEvent(pointer('pointerup', 60, 40));
|
|
127
|
+
await tick(40);
|
|
128
|
+
|
|
129
|
+
// measure width/height = 0 in happy-dom → newWidth = 0 + 60 - 10 = 50, newHeight = 0 + 40 - 10 = 30
|
|
130
|
+
expect(box.style.width).toBe('50px');
|
|
131
|
+
expect(box.style.height).toBe('30px');
|
|
132
|
+
expect(ev).toContain('start');
|
|
133
|
+
expect(ev).toContain('resize');
|
|
134
|
+
expect(ev).toContain('end');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -2,27 +2,33 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
describe('FocusTrap behavior', () => {
|
|
4
4
|
describe('registerFocusTrap', () => {
|
|
5
|
-
it('should
|
|
6
|
-
const { registerFocusTrap } = await import('./focustrap');
|
|
5
|
+
it('should compile its hyperscript source and execute', async () => {
|
|
6
|
+
const { registerFocusTrap, focusTrapSource } = await import('./focustrap');
|
|
7
7
|
const mock = {
|
|
8
|
-
compileSync: vi.fn(),
|
|
8
|
+
compileSync: vi.fn().mockReturnValue({ ok: true, ast: { type: 'behavior' } }),
|
|
9
9
|
execute: vi.fn().mockResolvedValue(undefined),
|
|
10
10
|
createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
await registerFocusTrap(mock);
|
|
14
14
|
|
|
15
|
-
expect(mock.compileSync).
|
|
15
|
+
expect(mock.compileSync).toHaveBeenCalledWith(focusTrapSource, { traditional: true });
|
|
16
16
|
expect(mock.execute).toHaveBeenCalledWith(
|
|
17
|
-
|
|
18
|
-
type: 'behavior',
|
|
19
|
-
name: 'FocusTrap',
|
|
20
|
-
imperativeInstaller: expect.any(Function),
|
|
21
|
-
}),
|
|
17
|
+
{ type: 'behavior' },
|
|
22
18
|
expect.objectContaining({ locals: expect.any(Map), globals: expect.any(Map) })
|
|
23
19
|
);
|
|
24
20
|
});
|
|
25
21
|
|
|
22
|
+
it('should throw on compile failure', async () => {
|
|
23
|
+
const { registerFocusTrap } = await import('./focustrap');
|
|
24
|
+
const mock = {
|
|
25
|
+
compileSync: vi.fn().mockReturnValue({ ok: false, errors: [{ message: 'boom' }] }),
|
|
26
|
+
execute: vi.fn(),
|
|
27
|
+
createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
|
|
28
|
+
};
|
|
29
|
+
await expect(registerFocusTrap(mock)).rejects.toThrowError(/Failed to compile FocusTrap/);
|
|
30
|
+
});
|
|
31
|
+
|
|
26
32
|
it('should throw when no runtime available', async () => {
|
|
27
33
|
const { registerFocusTrap } = await import('./focustrap');
|
|
28
34
|
await expect(registerFocusTrap(undefined)).rejects.toThrowError(/LokaScript not found/);
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FocusTrap Behavior
|
|
2
|
+
* FocusTrap Behavior
|
|
3
3
|
*
|
|
4
4
|
* Confines Tab navigation inside an element, manages aria-modal,
|
|
5
|
-
* and restores focus on deactivation.
|
|
5
|
+
* and restores focus on deactivation. The Tab-trapping / focus-model logic
|
|
6
|
+
* lives in the schema's hyperscript `source` (an `init`-block `js()` body).
|
|
7
|
+
*
|
|
8
|
+
* Compiled from its hyperscript `source` — the single source of truth shared with
|
|
9
|
+
* the CDN resolver bundle and patterns-reference. This collapses FocusTrap onto
|
|
10
|
+
* the one runtime path (no more imperative-installer fork between CDN and npm);
|
|
11
|
+
* see docs-internal/BEHAVIORS_CONSOLIDATION_PLAN.md §3d.
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
14
|
import { focusTrapSchema } from '../schemas/focustrap.schema';
|
|
@@ -13,118 +19,9 @@ import { resolveRuntime } from '../schemas/types';
|
|
|
13
19
|
export const focusTrapSource = focusTrapSchema.source;
|
|
14
20
|
export const focusTrapMetadata = focusTrapSchema;
|
|
15
21
|
|
|
16
|
-
const FOCUSABLE_SELECTOR = [
|
|
17
|
-
'a[href]',
|
|
18
|
-
'button:not(:disabled)',
|
|
19
|
-
'input:not(:disabled)',
|
|
20
|
-
'select:not(:disabled)',
|
|
21
|
-
'textarea:not(:disabled)',
|
|
22
|
-
'[tabindex]:not([tabindex="-1"])',
|
|
23
|
-
].join(', ');
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Resolve a selector parameter to an HTMLElement within the container.
|
|
27
|
-
*/
|
|
28
|
-
function resolveElement(container: HTMLElement, param: unknown): HTMLElement | null {
|
|
29
|
-
if (!param) return null;
|
|
30
|
-
if (param instanceof HTMLElement) return param;
|
|
31
|
-
if (typeof param === 'string') {
|
|
32
|
-
return (
|
|
33
|
-
(container.querySelector(param) as HTMLElement) ||
|
|
34
|
-
(document.querySelector(param) as HTMLElement) ||
|
|
35
|
-
null
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
22
|
/**
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
function installFocusTrap(element: HTMLElement, params: Record<string, any>): void {
|
|
45
|
-
const returnFocus = params.returnFocus !== false;
|
|
46
|
-
let isActive = false;
|
|
47
|
-
let previouslyFocused: Element | null = null;
|
|
48
|
-
|
|
49
|
-
function getFocusableElements(): HTMLElement[] {
|
|
50
|
-
return Array.from(element.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function onKeyDown(e: KeyboardEvent) {
|
|
54
|
-
if (!isActive || e.key !== 'Tab') return;
|
|
55
|
-
|
|
56
|
-
const focusable = getFocusableElements();
|
|
57
|
-
if (focusable.length === 0) {
|
|
58
|
-
e.preventDefault();
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Always prevent default Tab and manually manage focus.
|
|
63
|
-
// This ensures focus never escapes to elements outside the trap,
|
|
64
|
-
// even when the modal is embedded in a page with other focusable elements.
|
|
65
|
-
e.preventDefault();
|
|
66
|
-
|
|
67
|
-
const currentIndex = focusable.indexOf(document.activeElement as HTMLElement);
|
|
68
|
-
|
|
69
|
-
if (e.shiftKey) {
|
|
70
|
-
const prevIndex = currentIndex <= 0 ? focusable.length - 1 : currentIndex - 1;
|
|
71
|
-
focusable[prevIndex].focus();
|
|
72
|
-
} else {
|
|
73
|
-
const nextIndex = currentIndex >= focusable.length - 1 ? 0 : currentIndex + 1;
|
|
74
|
-
focusable[nextIndex].focus();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function activate() {
|
|
79
|
-
if (isActive) return;
|
|
80
|
-
isActive = true;
|
|
81
|
-
previouslyFocused = document.activeElement;
|
|
82
|
-
|
|
83
|
-
element.setAttribute('aria-modal', 'true');
|
|
84
|
-
|
|
85
|
-
// Focus initialFocus element or first focusable
|
|
86
|
-
const initialEl = resolveElement(element, params.initialFocus);
|
|
87
|
-
if (initialEl) {
|
|
88
|
-
initialEl.focus();
|
|
89
|
-
} else {
|
|
90
|
-
const focusable = getFocusableElements();
|
|
91
|
-
if (focusable.length > 0) {
|
|
92
|
-
focusable[0].focus();
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
element.dispatchEvent(new CustomEvent('focustrap:activated', { bubbles: true }));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function deactivate() {
|
|
100
|
-
if (!isActive) return;
|
|
101
|
-
isActive = false;
|
|
102
|
-
|
|
103
|
-
element.removeAttribute('aria-modal');
|
|
104
|
-
|
|
105
|
-
if (returnFocus && previouslyFocused instanceof HTMLElement) {
|
|
106
|
-
previouslyFocused.focus();
|
|
107
|
-
}
|
|
108
|
-
previouslyFocused = null;
|
|
109
|
-
|
|
110
|
-
element.dispatchEvent(new CustomEvent('focustrap:deactivated', { bubbles: true }));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Keydown handler for Tab trapping
|
|
114
|
-
element.addEventListener('keydown', onKeyDown);
|
|
115
|
-
|
|
116
|
-
// Programmatic activation/deactivation via custom events
|
|
117
|
-
element.addEventListener('focustrap:activate', () => activate());
|
|
118
|
-
element.addEventListener('focustrap:deactivate', () => deactivate());
|
|
119
|
-
|
|
120
|
-
// Activate immediately if requested
|
|
121
|
-
if (params.active !== false) {
|
|
122
|
-
activate();
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Register the FocusTrap behavior with LokaScript.
|
|
23
|
+
* Register the FocusTrap behavior with LokaScript by compiling its
|
|
24
|
+
* hyperscript source and executing the resulting behavior definition.
|
|
128
25
|
*/
|
|
129
26
|
export async function registerFocusTrap(hyperfixi?: LokaScriptInstance): Promise<void> {
|
|
130
27
|
const hf = hyperfixi || resolveRuntime();
|
|
@@ -135,15 +32,14 @@ export async function registerFocusTrap(hyperfixi?: LokaScriptInstance): Promise
|
|
|
135
32
|
);
|
|
136
33
|
}
|
|
137
34
|
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
};
|
|
35
|
+
const result = hf.compileSync(focusTrapSchema.source, { traditional: true });
|
|
36
|
+
|
|
37
|
+
if (!result.ok) {
|
|
38
|
+
throw new Error(`Failed to compile FocusTrap behavior: ${JSON.stringify(result.errors)}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
145
41
|
const ctx = hf.createContext ? hf.createContext() : { locals: new Map(), globals: new Map() };
|
|
146
|
-
await hf.execute(
|
|
42
|
+
await hf.execute(result.ast, ctx);
|
|
147
43
|
}
|
|
148
44
|
|
|
149
45
|
// Auto-register when loaded as a script tag
|
|
@@ -21,23 +21,24 @@ function createMockInstance(overrides: Partial<LokaScriptInstance> = {}): LokaSc
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
// Source-compiled behaviors (use compileSync + execute)
|
|
24
|
+
// Source-compiled behaviors (use compileSync + execute).
|
|
25
|
+
// EVERY behavior compiles its hyperscript `source` — one runtime path, identical
|
|
26
|
+
// browser/npm, no imperative JS installer (the "no imperative JS" rule). The optional
|
|
27
|
+
// three (FocusTrap/ScrollReveal/Tabs) carry their web-API logic in an `init`-block
|
|
28
|
+
// `js()` body, and the experimental three (Draggable/Sortable/Resizable) run their
|
|
29
|
+
// pointer-drag loops via `repeat until event` — all through the single compile path.
|
|
25
30
|
const compiledBehaviors = [
|
|
26
31
|
{ name: 'Removable', register: registerRemovable, source: removableSource },
|
|
27
|
-
] as const;
|
|
28
|
-
|
|
29
|
-
// Imperative behaviors (use synthetic node with imperativeInstaller)
|
|
30
|
-
const imperativeBehaviors = [
|
|
31
|
-
{ name: 'Draggable', register: registerDraggable, source: draggableSource },
|
|
32
32
|
{ name: 'Toggleable', register: registerToggleable, source: toggleableSource },
|
|
33
|
-
{ name: 'Sortable', register: registerSortable, source: sortableSource },
|
|
34
|
-
{ name: 'Resizable', register: registerResizable, source: resizableSource },
|
|
35
33
|
{ name: 'Clipboard', register: registerClipboard, source: clipboardSource },
|
|
36
34
|
{ name: 'AutoDismiss', register: registerAutoDismiss, source: autoDismissSource },
|
|
37
35
|
{ name: 'ClickOutside', register: registerClickOutside, source: clickOutsideSource },
|
|
38
36
|
{ name: 'FocusTrap', register: registerFocusTrap, source: focusTrapSource },
|
|
39
37
|
{ name: 'ScrollReveal', register: registerScrollReveal, source: scrollRevealSource },
|
|
40
38
|
{ name: 'Tabs', register: registerTabs, source: tabsSource },
|
|
39
|
+
{ name: 'Draggable', register: registerDraggable, source: draggableSource },
|
|
40
|
+
{ name: 'Sortable', register: registerSortable, source: sortableSource },
|
|
41
|
+
{ name: 'Resizable', register: registerResizable, source: resizableSource },
|
|
41
42
|
] as const;
|
|
42
43
|
|
|
43
44
|
describe('behavior registration integration', () => {
|
|
@@ -96,60 +97,4 @@ describe('behavior registration integration', () => {
|
|
|
96
97
|
});
|
|
97
98
|
}
|
|
98
99
|
});
|
|
99
|
-
|
|
100
|
-
describe('imperative behaviors', () => {
|
|
101
|
-
for (const { name, register } of imperativeBehaviors) {
|
|
102
|
-
describe(name, () => {
|
|
103
|
-
it('should register via synthetic node with imperativeInstaller', async () => {
|
|
104
|
-
const mock = createMockInstance();
|
|
105
|
-
await register(mock);
|
|
106
|
-
|
|
107
|
-
// Imperative behaviors do NOT call compileSync
|
|
108
|
-
expect(mock.compileSync).not.toHaveBeenCalled();
|
|
109
|
-
|
|
110
|
-
// They call execute with a synthetic node containing imperativeInstaller
|
|
111
|
-
expect(mock.execute).toHaveBeenCalledWith(
|
|
112
|
-
expect.objectContaining({
|
|
113
|
-
type: 'behavior',
|
|
114
|
-
name,
|
|
115
|
-
imperativeInstaller: expect.any(Function),
|
|
116
|
-
}),
|
|
117
|
-
expect.objectContaining({
|
|
118
|
-
locals: expect.any(Map),
|
|
119
|
-
globals: expect.any(Map),
|
|
120
|
-
})
|
|
121
|
-
);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('should create context', async () => {
|
|
125
|
-
const mock = createMockInstance();
|
|
126
|
-
await register(mock);
|
|
127
|
-
expect(mock.createContext).toHaveBeenCalled();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should throw when lokascript is not available', async () => {
|
|
131
|
-
await expect(register(undefined)).rejects.toThrowError(/LokaScript not found/);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should fall back to manual context when createContext is missing', async () => {
|
|
135
|
-
const mock = createMockInstance();
|
|
136
|
-
delete (mock as Partial<LokaScriptInstance>).createContext;
|
|
137
|
-
|
|
138
|
-
await register(mock);
|
|
139
|
-
|
|
140
|
-
expect(mock.execute).toHaveBeenCalledWith(
|
|
141
|
-
expect.objectContaining({
|
|
142
|
-
type: 'behavior',
|
|
143
|
-
name,
|
|
144
|
-
imperativeInstaller: expect.any(Function),
|
|
145
|
-
}),
|
|
146
|
-
expect.objectContaining({
|
|
147
|
-
locals: expect.any(Map),
|
|
148
|
-
globals: expect.any(Map),
|
|
149
|
-
})
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
100
|
});
|