@adobe/data-lit 0.10.30 → 0.10.32
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 +22 -12
- package/dist/elements/application-host.d.ts +6 -2
- package/dist/elements/application-host.js +17 -1
- package/dist/elements/application-host.js.map +1 -1
- package/dist/functions/iterate-self-and-ancestors.d.ts +1 -0
- package/dist/functions/iterate-self-and-ancestors.js +2 -2
- package/dist/functions/iterate-self-and-ancestors.js.map +1 -1
- package/dist/hooks/component/hooks-controller.js +16 -5
- package/dist/hooks/component/hooks-controller.js.map +1 -1
- package/dist/hooks/use-drag-observe.js +2 -1
- package/dist/hooks/use-drag-observe.js.map +1 -1
- package/dist/hooks/use-drag-transaction.d.ts +5 -0
- package/dist/hooks/use-drag-transaction.js +42 -22
- package/dist/hooks/use-drag-transaction.js.map +1 -1
- package/dist/hooks/use-draggable.js +70 -48
- package/dist/hooks/use-draggable.js.map +1 -1
- package/dist/hooks/use-effect.js +6 -2
- package/dist/hooks/use-effect.js.map +1 -1
- package/dist/hooks/use-element.d.ts +4 -2
- package/dist/hooks/use-element.js +10 -51
- package/dist/hooks/use-element.js.map +1 -1
- package/dist/hooks/use-memo.js +11 -9
- package/dist/hooks/use-memo.js.map +1 -1
- package/dist/hooks/use-observable.d.ts +4 -0
- package/dist/hooks/use-observable.js +13 -8
- package/dist/hooks/use-observable.js.map +1 -1
- package/dist/hooks/use-ref.js +4 -3
- package/dist/hooks/use-ref.js.map +1 -1
- package/dist/hooks/use-resize-observer.js +5 -2
- package/dist/hooks/use-resize-observer.js.map +1 -1
- package/dist/hooks/use-resize-observer.test.d.ts +1 -0
- package/dist/hooks/use-resize-observer.test.js +102 -0
- package/dist/hooks/use-resize-observer.test.js.map +1 -0
- package/dist/hooks/use-state.js +9 -7
- package/dist/hooks/use-state.js.map +1 -1
- package/dist/hooks/use-state.test.d.ts +1 -0
- package/dist/hooks/use-state.test.js +83 -0
- package/dist/hooks/use-state.test.js.map +1 -0
- package/dist/hooks/use-window-event.js +1 -1
- package/dist/hooks/use-window-event.js.map +1 -1
- package/package.json +3 -3
- package/src/decorators/apply-decorator.test.ts +37 -0
- package/src/decorators/apply-service-decorators.test.ts +53 -0
- package/src/decorators/require-service.test.ts +31 -0
- package/src/elements/application-element.test.ts +80 -0
- package/src/elements/application-host.test.ts +57 -0
- package/src/elements/application-host.ts +16 -3
- package/src/elements/database-element.test.ts +111 -0
- package/src/functions/iterate-self-and-ancestors.test.ts +29 -0
- package/src/functions/iterate-self-and-ancestors.ts +2 -2
- package/src/hooks/attach-decorator.test.ts +45 -0
- package/src/hooks/component/hooks-controller.cleanup.test.ts +65 -0
- package/src/hooks/component/hooks-controller.ts +15 -5
- package/src/hooks/component/stack.test.ts +37 -0
- package/src/hooks/test-host.test-utils.ts +43 -0
- package/src/hooks/use-connected.test.ts +88 -0
- package/src/hooks/use-debounce.test.ts +69 -0
- package/src/hooks/use-drag-generator.test.ts +67 -0
- package/src/hooks/use-drag-observe.test.ts +74 -0
- package/src/hooks/use-drag-observe.ts +2 -1
- package/src/hooks/use-drag-transaction.test.ts +109 -0
- package/src/hooks/use-drag-transaction.ts +48 -27
- package/src/hooks/use-draggable.test.ts +148 -0
- package/src/hooks/use-draggable.ts +73 -48
- package/src/hooks/use-effect.test.ts +51 -0
- package/src/hooks/use-effect.ts +5 -2
- package/src/hooks/use-element.test.ts +82 -0
- package/src/hooks/use-element.ts +17 -60
- package/src/hooks/use-memo.test.ts +66 -0
- package/src/hooks/use-memo.ts +16 -11
- package/src/hooks/use-observable-values.test.ts +53 -0
- package/src/hooks/use-observable.test.ts +63 -0
- package/src/hooks/use-observable.ts +13 -8
- package/src/hooks/use-pointer-observe.test.ts +59 -0
- package/src/hooks/use-ref.test.ts +50 -0
- package/src/hooks/use-ref.ts +4 -3
- package/src/hooks/use-resize-observer.test.ts +114 -0
- package/src/hooks/use-resize-observer.ts +5 -2
- package/src/hooks/use-state.test.ts +108 -0
- package/src/hooks/use-state.ts +14 -9
- package/src/hooks/use-updated.test.ts +98 -0
- package/src/hooks/use-window-event.test.ts +61 -0
- package/src/hooks/use-window-event.ts +1 -1
- package/src/hooks/with-hooks.test.ts +45 -0
- package/src/index.test.ts +26 -0
- package/src/public-types.type-test.ts +17 -0
- package/tsconfig.json +1 -0
- package/tsconfig.test.json +9 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, describe, it, expect, vi } from "vitest";
|
|
3
|
+
import { LitElement, html } from "lit";
|
|
4
|
+
import { attachDecorator } from "./attach-decorator.js";
|
|
5
|
+
import { withHooks } from "./with-hooks.js";
|
|
6
|
+
import { useMemo } from "./use-memo.js";
|
|
7
|
+
import { useResizeObserver } from "./use-resize-observer.js";
|
|
8
|
+
import { useState } from "./use-state.js";
|
|
9
|
+
|
|
10
|
+
class ResizeProbeElement extends LitElement {
|
|
11
|
+
renders = 0;
|
|
12
|
+
offset = 0;
|
|
13
|
+
widths: number[] = [];
|
|
14
|
+
calculateCallback = vi.fn((setWidth: (value: number) => void) => setWidth);
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
attachDecorator(this, "render", withHooks);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
render() {
|
|
22
|
+
this.renders++;
|
|
23
|
+
const [width, setWidth] = useState(0);
|
|
24
|
+
const memoizedSetter = useMemo(() => this.calculateCallback(setWidth), [setWidth]);
|
|
25
|
+
const offset = this.offset;
|
|
26
|
+
useResizeObserver(info => {
|
|
27
|
+
this.widths.push(info.width + offset);
|
|
28
|
+
memoizedSetter(Math.floor(info.width + offset));
|
|
29
|
+
});
|
|
30
|
+
return html`<span>${width}</span>`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
customElements.define("resize-state-probe", ResizeProbeElement);
|
|
34
|
+
|
|
35
|
+
function mockResizeObserver() {
|
|
36
|
+
const observers: { emit: (width: number) => void; observe: ReturnType<typeof vi.fn>; disconnect: ReturnType<typeof vi.fn> }[] = [];
|
|
37
|
+
vi.stubGlobal("ResizeObserver", class {
|
|
38
|
+
observe = vi.fn();
|
|
39
|
+
disconnect = vi.fn();
|
|
40
|
+
|
|
41
|
+
constructor(callback: ResizeObserverCallback) {
|
|
42
|
+
observers.push({
|
|
43
|
+
emit: width => callback([
|
|
44
|
+
{ contentRect: { width, height: 20 } } as ResizeObserverEntry,
|
|
45
|
+
], this as unknown as ResizeObserver),
|
|
46
|
+
observe: this.observe,
|
|
47
|
+
disconnect: this.disconnect,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return observers;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
afterEach(async () => {
|
|
55
|
+
document.body.replaceChildren();
|
|
56
|
+
await new Promise<void>(resolve => queueMicrotask(resolve));
|
|
57
|
+
vi.unstubAllGlobals();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("useResizeObserver with state", () => {
|
|
61
|
+
it("keeps one observer, uses the latest inline callback, and skips unchanged widths", async () => {
|
|
62
|
+
const observers = mockResizeObserver();
|
|
63
|
+
const element = document.createElement("resize-state-probe") as ResizeProbeElement;
|
|
64
|
+
document.body.appendChild(element);
|
|
65
|
+
await element.updateComplete;
|
|
66
|
+
const observer = observers[0];
|
|
67
|
+
expect(observer.observe).toHaveBeenCalledExactlyOnceWith(element);
|
|
68
|
+
|
|
69
|
+
observer.emit(100.2);
|
|
70
|
+
await element.updateComplete;
|
|
71
|
+
expect(element.shadowRoot?.textContent).toBe("100");
|
|
72
|
+
expect(element.renders).toBe(2);
|
|
73
|
+
expect(observers).toHaveLength(1);
|
|
74
|
+
expect(observer.disconnect).not.toHaveBeenCalled();
|
|
75
|
+
expect(element.calculateCallback).toHaveBeenCalledTimes(1);
|
|
76
|
+
|
|
77
|
+
const requestUpdate = vi.spyOn(element, "requestUpdate");
|
|
78
|
+
observer.emit(100.8);
|
|
79
|
+
await element.updateComplete;
|
|
80
|
+
expect(requestUpdate).not.toHaveBeenCalled();
|
|
81
|
+
expect(element.renders).toBe(2);
|
|
82
|
+
|
|
83
|
+
element.offset = 10;
|
|
84
|
+
element.requestUpdate();
|
|
85
|
+
await element.updateComplete;
|
|
86
|
+
observer.emit(200);
|
|
87
|
+
await element.updateComplete;
|
|
88
|
+
expect(element.widths).toEqual([100.2, 100.8, 210]);
|
|
89
|
+
expect(element.shadowRoot?.textContent).toBe("210");
|
|
90
|
+
expect(observers).toHaveLength(1);
|
|
91
|
+
expect(observer.disconnect).not.toHaveBeenCalled();
|
|
92
|
+
expect(element.calculateCallback).toHaveBeenCalledTimes(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("disconnects on unmount and creates fresh state and observer on remount", async () => {
|
|
96
|
+
const observers = mockResizeObserver();
|
|
97
|
+
const element = document.createElement("resize-state-probe") as ResizeProbeElement;
|
|
98
|
+
document.body.appendChild(element);
|
|
99
|
+
await element.updateComplete;
|
|
100
|
+
observers[0].emit(100);
|
|
101
|
+
await element.updateComplete;
|
|
102
|
+
|
|
103
|
+
element.remove();
|
|
104
|
+
await new Promise<void>(resolve => queueMicrotask(resolve));
|
|
105
|
+
expect(observers[0].disconnect).toHaveBeenCalledTimes(1);
|
|
106
|
+
|
|
107
|
+
document.body.appendChild(element);
|
|
108
|
+
await element.updateComplete;
|
|
109
|
+
expect(element.shadowRoot?.textContent).toBe("0");
|
|
110
|
+
expect(observers).toHaveLength(2);
|
|
111
|
+
expect(observers[1].observe).toHaveBeenCalledExactlyOnceWith(element);
|
|
112
|
+
expect(element.calculateCallback).toHaveBeenCalledTimes(2);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect } from "./use-effect.js";
|
|
4
4
|
import { useElement } from "./use-element.js";
|
|
5
|
+
import { useRef } from "./use-ref.js";
|
|
5
6
|
|
|
6
7
|
export interface ResizeInfo {
|
|
7
8
|
width: number;
|
|
@@ -17,15 +18,17 @@ export interface ResizeInfo {
|
|
|
17
18
|
*/
|
|
18
19
|
export function useResizeObserver(onResize: (info: ResizeInfo) => void) {
|
|
19
20
|
const element = useElement();
|
|
21
|
+
const latestCallback = useRef(onResize);
|
|
22
|
+
latestCallback.current = onResize;
|
|
20
23
|
|
|
21
24
|
useEffect(() => {
|
|
22
25
|
const observer = new ResizeObserver(entries => {
|
|
23
26
|
for (const entry of entries) {
|
|
24
27
|
const { width, height } = entry.contentRect;
|
|
25
|
-
|
|
28
|
+
latestCallback.current({ width, height, entry });
|
|
26
29
|
}
|
|
27
30
|
});
|
|
28
31
|
observer.observe(element);
|
|
29
32
|
return () => observer.disconnect();
|
|
30
|
-
}, [element
|
|
33
|
+
}, [element]);
|
|
31
34
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import type { Component } from "./component/component.js";
|
|
3
|
+
import { Component_stack } from "./component/stack.js";
|
|
4
|
+
import { useState } from "./use-state.js";
|
|
5
|
+
|
|
6
|
+
function createHost(): Component {
|
|
7
|
+
return Object.assign(new EventTarget(), {
|
|
8
|
+
isConnected: true,
|
|
9
|
+
hookIndex: 0,
|
|
10
|
+
hooks: [],
|
|
11
|
+
updatedListeners: new Set<() => void>(),
|
|
12
|
+
requestUpdate: vi.fn(),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function render<T>(host: Component, callback: () => T): T {
|
|
17
|
+
Component_stack.push(host);
|
|
18
|
+
try {
|
|
19
|
+
return callback();
|
|
20
|
+
} finally {
|
|
21
|
+
Component_stack.pop();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("useState", () => {
|
|
26
|
+
it("keeps the setter stable across renders and updates", () => {
|
|
27
|
+
const host = createHost();
|
|
28
|
+
const [, setValue] = render(host, () => useState(0));
|
|
29
|
+
expect(render(host, () => useState(0))[1]).toBe(setValue);
|
|
30
|
+
|
|
31
|
+
setValue(1);
|
|
32
|
+
|
|
33
|
+
const [value, nextSetter] = render(host, () => useState(0));
|
|
34
|
+
expect(value).toBe(1);
|
|
35
|
+
expect(nextSetter).toBe(setValue);
|
|
36
|
+
expect(host.requestUpdate).toHaveBeenCalledTimes(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it.each([0, NaN, null, undefined, "value", false, {}, []])(
|
|
40
|
+
"does not request an update for an Object.is-equal value: %s",
|
|
41
|
+
initial => {
|
|
42
|
+
const host = createHost();
|
|
43
|
+
const [, setValue] = render(host, () => useState(initial));
|
|
44
|
+
|
|
45
|
+
setValue(initial);
|
|
46
|
+
setValue(initial);
|
|
47
|
+
|
|
48
|
+
expect(host.requestUpdate).not.toHaveBeenCalled();
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
it("compares against the latest value before another render", () => {
|
|
53
|
+
const host = createHost();
|
|
54
|
+
const [, setValue] = render(host, () => useState(0));
|
|
55
|
+
|
|
56
|
+
setValue(1);
|
|
57
|
+
setValue(1);
|
|
58
|
+
setValue(2);
|
|
59
|
+
|
|
60
|
+
expect(host.requestUpdate).toHaveBeenCalledTimes(2);
|
|
61
|
+
expect(render(host, () => useState(0))[0]).toBe(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("distinguishes positive and negative zero", () => {
|
|
65
|
+
const host = createHost();
|
|
66
|
+
const [, setValue] = render(host, () => useState(0));
|
|
67
|
+
|
|
68
|
+
setValue(-0);
|
|
69
|
+
|
|
70
|
+
expect(host.requestUpdate).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(render(host, () => useState(0))[0]).toBe(-0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it.each([null, undefined])("initializes a nullish value only once: %s", initial => {
|
|
75
|
+
const host = createHost();
|
|
76
|
+
const initialize = vi.fn(() => initial);
|
|
77
|
+
|
|
78
|
+
render(host, () => useState(initialize));
|
|
79
|
+
render(host, () => useState(initialize));
|
|
80
|
+
|
|
81
|
+
expect(initialize).toHaveBeenCalledTimes(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it.each([null, undefined])("preserves a nullish update: %s", nextValue => {
|
|
85
|
+
const host = createHost();
|
|
86
|
+
const [, setValue] = render(host, () => useState<number | null | undefined>(1));
|
|
87
|
+
|
|
88
|
+
setValue(nextValue);
|
|
89
|
+
|
|
90
|
+
expect(render(host, () => useState(1))[0]).toBe(nextValue);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("keeps state slots and hosts independent", () => {
|
|
94
|
+
const host = createHost();
|
|
95
|
+
const otherHost = createHost();
|
|
96
|
+
const read = () => [useState(0), useState(10)] as const;
|
|
97
|
+
const [first, second] = render(host, read);
|
|
98
|
+
const [other] = render(otherHost, read);
|
|
99
|
+
|
|
100
|
+
first[1](1);
|
|
101
|
+
|
|
102
|
+
expect(render(host, read).map(([value]) => value)).toEqual([1, 10]);
|
|
103
|
+
expect(render(otherHost, read).map(([value]) => value)).toEqual([0, 10]);
|
|
104
|
+
expect(first[1]).not.toBe(second[1]);
|
|
105
|
+
expect(first[1]).not.toBe(other[1]);
|
|
106
|
+
expect(otherHost.requestUpdate).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
});
|
package/src/hooks/use-state.ts
CHANGED
|
@@ -2,18 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
import { Component_stack } from "./component/stack.js";
|
|
4
4
|
|
|
5
|
+
interface State<T> {
|
|
6
|
+
value: T;
|
|
7
|
+
setValue: (value: T) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
export function useState<T>(initializer: () => T): [T, (value: T) => void]
|
|
6
11
|
export function useState<T>(initialValue: T): [T, (value: T) => void]
|
|
7
|
-
export function useState<T>(initial: () => T) {
|
|
12
|
+
export function useState<T>(initial: T | (() => T)): [T, (value: T) => void] {
|
|
8
13
|
const component = Component_stack.active();
|
|
9
14
|
const hookIndex = component.hookIndex++;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
const state: State<T> = component.hooks[hookIndex] ??= {
|
|
16
|
+
value: typeof initial === "function" ? (initial as () => T)() : initial,
|
|
17
|
+
setValue: (newValue: T) => {
|
|
18
|
+
if (Object.is(state.value, newValue)) return;
|
|
19
|
+
state.value = newValue;
|
|
15
20
|
component.requestUpdate();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
return [state.value, state.setValue];
|
|
19
24
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
// @vitest-environment happy-dom
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { LitElement, html } from "lit";
|
|
5
|
+
import { attachDecorator } from "./attach-decorator.js";
|
|
6
|
+
import { withHooks } from "./with-hooks.js";
|
|
7
|
+
import { useUpdated, type UpdateEventHost } from "./use-updated.js";
|
|
8
|
+
import { createHost, render } from "./test-host.test-utils.js";
|
|
9
|
+
|
|
10
|
+
class UpdatedProbe extends LitElement {
|
|
11
|
+
listeners: (() => void)[] = [];
|
|
12
|
+
dependency = 0;
|
|
13
|
+
originalUpdated = vi.fn();
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
attachDecorator(this, "render", withHooks);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override updated() {
|
|
21
|
+
this.originalUpdated();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
render() {
|
|
25
|
+
for (const listener of this.listeners) {
|
|
26
|
+
useUpdated(listener, [this.dependency]);
|
|
27
|
+
}
|
|
28
|
+
return html``;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
customElements.define("use-updated-probe", UpdatedProbe);
|
|
32
|
+
|
|
33
|
+
async function mount(...listeners: (() => void)[]) {
|
|
34
|
+
const element = document.createElement("use-updated-probe") as UpdatedProbe;
|
|
35
|
+
element.listeners = listeners;
|
|
36
|
+
document.body.append(element);
|
|
37
|
+
await element.updateComplete;
|
|
38
|
+
return element;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
afterEach(async () => {
|
|
42
|
+
document.body.replaceChildren();
|
|
43
|
+
await Promise.resolve();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("useUpdated", () => {
|
|
47
|
+
it("calls the listener after every update, after the element's own updated", async () => {
|
|
48
|
+
const calls: string[] = [];
|
|
49
|
+
const element = await mount(() => calls.push("listener"));
|
|
50
|
+
expect(calls).toEqual(["listener"]);
|
|
51
|
+
element.originalUpdated.mockImplementation(() => calls.push("updated"));
|
|
52
|
+
calls.length = 0;
|
|
53
|
+
|
|
54
|
+
element.requestUpdate();
|
|
55
|
+
await element.updateComplete;
|
|
56
|
+
expect(calls).toEqual(["updated", "listener"]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("calls multiple listeners in registration order through one wrapper", async () => {
|
|
60
|
+
const calls: string[] = [];
|
|
61
|
+
const element = await mount(() => calls.push("a"), () => calls.push("b"));
|
|
62
|
+
calls.length = 0;
|
|
63
|
+
element.requestUpdate();
|
|
64
|
+
await element.updateComplete;
|
|
65
|
+
expect(calls).toEqual(["a", "b"]);
|
|
66
|
+
expect(element.originalUpdated).toHaveBeenCalledTimes(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("replaces the listener when dependencies change", async () => {
|
|
70
|
+
const first = vi.fn();
|
|
71
|
+
const second = vi.fn();
|
|
72
|
+
const element = await mount(first);
|
|
73
|
+
element.listeners = [second];
|
|
74
|
+
element.dependency = 1;
|
|
75
|
+
element.requestUpdate();
|
|
76
|
+
await element.updateComplete;
|
|
77
|
+
first.mockClear();
|
|
78
|
+
second.mockClear();
|
|
79
|
+
|
|
80
|
+
element.requestUpdate();
|
|
81
|
+
await element.updateComplete;
|
|
82
|
+
expect(first).not.toHaveBeenCalled();
|
|
83
|
+
expect(second).toHaveBeenCalledTimes(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("removes the listener on unmount", async () => {
|
|
87
|
+
const element = await mount(() => {});
|
|
88
|
+
const listeners = (element as unknown as UpdateEventHost).updateListeners;
|
|
89
|
+
expect(listeners.size).toBe(1);
|
|
90
|
+
element.remove();
|
|
91
|
+
await Promise.resolve();
|
|
92
|
+
expect(listeners.size).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("throws for a host without an updated method", () => {
|
|
96
|
+
expect(() => render(createHost(), () => useUpdated(() => {}))).toThrow("does not have an updated method");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
// @vitest-environment happy-dom
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { useWindowEvent } from "./use-window-event.js";
|
|
5
|
+
import { createHost, render, unmount } from "./test-host.test-utils.js";
|
|
6
|
+
|
|
7
|
+
describe("useWindowEvent", () => {
|
|
8
|
+
it("listens on window with the callback from the factory", () => {
|
|
9
|
+
const host = createHost();
|
|
10
|
+
const listener = vi.fn();
|
|
11
|
+
render(host, () => useWindowEvent("resize", () => listener, []));
|
|
12
|
+
window.dispatchEvent(new Event("resize"));
|
|
13
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
14
|
+
unmount(host);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("keeps the first callback while factory dependencies are equal", () => {
|
|
18
|
+
const host = createHost();
|
|
19
|
+
const first = vi.fn();
|
|
20
|
+
const second = vi.fn();
|
|
21
|
+
render(host, () => useWindowEvent("resize", () => first, [1]));
|
|
22
|
+
render(host, () => useWindowEvent("resize", () => second, [1]));
|
|
23
|
+
window.dispatchEvent(new Event("resize"));
|
|
24
|
+
expect(first).toHaveBeenCalledTimes(1);
|
|
25
|
+
expect(second).not.toHaveBeenCalled();
|
|
26
|
+
unmount(host);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("replaces the callback when factory dependencies change", () => {
|
|
30
|
+
const host = createHost();
|
|
31
|
+
const first = vi.fn();
|
|
32
|
+
const second = vi.fn();
|
|
33
|
+
render(host, () => useWindowEvent("resize", () => first, [1]));
|
|
34
|
+
render(host, () => useWindowEvent("resize", () => second, [2]));
|
|
35
|
+
window.dispatchEvent(new Event("resize"));
|
|
36
|
+
expect(first).not.toHaveBeenCalled();
|
|
37
|
+
expect(second).toHaveBeenCalledTimes(1);
|
|
38
|
+
unmount(host);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("moves its listener when the event name changes", () => {
|
|
42
|
+
const host = createHost();
|
|
43
|
+
const received: string[] = [];
|
|
44
|
+
const listener = (event: Event) => received.push(event.type);
|
|
45
|
+
render(host, () => useWindowEvent("resize", () => listener, []));
|
|
46
|
+
render(host, () => useWindowEvent("scroll", () => listener, []));
|
|
47
|
+
window.dispatchEvent(new Event("resize"));
|
|
48
|
+
window.dispatchEvent(new Event("scroll"));
|
|
49
|
+
expect(received).toEqual(["scroll"]);
|
|
50
|
+
unmount(host);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("removes its listener on unmount", () => {
|
|
54
|
+
const host = createHost();
|
|
55
|
+
const listener = vi.fn();
|
|
56
|
+
render(host, () => useWindowEvent("resize", () => listener, []));
|
|
57
|
+
unmount(host);
|
|
58
|
+
window.dispatchEvent(new Event("resize"));
|
|
59
|
+
expect(listener).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
// @vitest-environment happy-dom
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { ReactiveController } from "lit";
|
|
5
|
+
import { Component_stack } from "./component/stack.js";
|
|
6
|
+
import { withHooks } from "./with-hooks.js";
|
|
7
|
+
import { createHost } from "./test-host.test-utils.js";
|
|
8
|
+
|
|
9
|
+
function wrap<Args extends unknown[], Return>(method: (...args: Args) => Return) {
|
|
10
|
+
return withHooks({}, "render", { value: method }).value!;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("withHooks", () => {
|
|
14
|
+
it("makes the host active during the call and forwards arguments and return value", () => {
|
|
15
|
+
const host = createHost();
|
|
16
|
+
const render = wrap(function (this: unknown, a: number, b: number) {
|
|
17
|
+
expect(Component_stack.active()).toBe(this);
|
|
18
|
+
return a + b;
|
|
19
|
+
});
|
|
20
|
+
expect(render.call(host, 1, 2)).toBe(3);
|
|
21
|
+
expect(Component_stack.active()).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("resets the hook cursor before each call", () => {
|
|
25
|
+
const host = createHost();
|
|
26
|
+
host.hookIndex = 4;
|
|
27
|
+
const render = wrap(() => Component_stack.active().hookIndex);
|
|
28
|
+
expect(render.call(host)).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("restores the stack when the method throws", () => {
|
|
32
|
+
const host = createHost();
|
|
33
|
+
const render = wrap(() => { throw new Error("render failed"); });
|
|
34
|
+
expect(() => render.call(host)).toThrow("render failed");
|
|
35
|
+
expect(Component_stack.active()).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("installs the lifecycle controller once on a reactive host", () => {
|
|
39
|
+
const host = Object.assign(createHost(), { addController: vi.fn((_: ReactiveController) => {}) });
|
|
40
|
+
const render = wrap(() => {});
|
|
41
|
+
render.call(host);
|
|
42
|
+
render.call(host);
|
|
43
|
+
expect(host.addController).toHaveBeenCalledTimes(1);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
// @vitest-environment happy-dom
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import * as dataLit from "./index.js";
|
|
5
|
+
|
|
6
|
+
describe("@adobe/data-lit public exports", () => {
|
|
7
|
+
it.each([
|
|
8
|
+
"useState", "useEffect", "useConnected", "useMemo", "useRef", "useObservable", "useObservableValues",
|
|
9
|
+
"useWindowEvent", "useResizeObserver", "useDebounce", "useElement", "useUpdated", "usePointerObserve",
|
|
10
|
+
"useDraggable", "useDragObserve", "useDragGenerator", "useDragTransaction",
|
|
11
|
+
"withHooks", "attachDecorator", "installHooksController",
|
|
12
|
+
"ApplicationHost", "ApplicationElement", "DatabaseElement",
|
|
13
|
+
"applyDecorator", "applyServiceDecorators", "requireService",
|
|
14
|
+
])("exports the function %s", name => {
|
|
15
|
+
expect(typeof (dataLit as Record<string, unknown>)[name]).toBe("function");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("exports the component stack and the Template helpers", () => {
|
|
19
|
+
expect(typeof dataLit.Component_stack.active).toBe("function");
|
|
20
|
+
expect(typeof dataLit.Template.from).toBe("function");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("registers the application-host element", () => {
|
|
24
|
+
expect(customElements.get("application-host")).toBe(dataLit.ApplicationHost);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compile-time tests for public element and hook declarations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
8
|
+
|
|
9
|
+
import { ApplicationHost } from "./elements/application-host.js";
|
|
10
|
+
import { useElement } from "./hooks/use-element.js";
|
|
11
|
+
|
|
12
|
+
const _hostTag = (): ApplicationHost => document.createElement("application-host");
|
|
13
|
+
|
|
14
|
+
const _tagSelector = (): HTMLButtonElement | null => useElement("button");
|
|
15
|
+
const _cssSelector = (): HTMLElement | null => useElement(".target");
|
|
16
|
+
const _typedCssSelector = (): HTMLInputElement | null => useElement<HTMLInputElement>("input.name");
|
|
17
|
+
const _component = (): HTMLElement => useElement();
|
package/tsconfig.json
CHANGED