@adobe/data-lit 0.10.31 → 0.10.33

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.
Files changed (76) hide show
  1. package/README.md +12 -14
  2. package/dist/elements/application-host.d.ts +6 -2
  3. package/dist/elements/application-host.js +17 -1
  4. package/dist/elements/application-host.js.map +1 -1
  5. package/dist/functions/iterate-self-and-ancestors.d.ts +1 -0
  6. package/dist/functions/iterate-self-and-ancestors.js +2 -2
  7. package/dist/functions/iterate-self-and-ancestors.js.map +1 -1
  8. package/dist/hooks/component/hooks-controller.js +16 -5
  9. package/dist/hooks/component/hooks-controller.js.map +1 -1
  10. package/dist/hooks/use-drag-observe.js +2 -1
  11. package/dist/hooks/use-drag-observe.js.map +1 -1
  12. package/dist/hooks/use-drag-transaction.d.ts +5 -0
  13. package/dist/hooks/use-drag-transaction.js +42 -22
  14. package/dist/hooks/use-drag-transaction.js.map +1 -1
  15. package/dist/hooks/use-draggable.js +70 -48
  16. package/dist/hooks/use-draggable.js.map +1 -1
  17. package/dist/hooks/use-effect.js +6 -2
  18. package/dist/hooks/use-effect.js.map +1 -1
  19. package/dist/hooks/use-element.d.ts +4 -2
  20. package/dist/hooks/use-element.js +10 -51
  21. package/dist/hooks/use-element.js.map +1 -1
  22. package/dist/hooks/use-memo.js +11 -9
  23. package/dist/hooks/use-memo.js.map +1 -1
  24. package/dist/hooks/use-observable.d.ts +4 -0
  25. package/dist/hooks/use-observable.js +13 -8
  26. package/dist/hooks/use-observable.js.map +1 -1
  27. package/dist/hooks/use-ref.js +4 -3
  28. package/dist/hooks/use-ref.js.map +1 -1
  29. package/dist/hooks/use-window-event.js +1 -1
  30. package/dist/hooks/use-window-event.js.map +1 -1
  31. package/package.json +3 -3
  32. package/src/decorators/apply-decorator.test.ts +37 -0
  33. package/src/decorators/apply-service-decorators.test.ts +53 -0
  34. package/src/decorators/require-service.test.ts +31 -0
  35. package/src/elements/application-element.test.ts +80 -0
  36. package/src/elements/application-host.test.ts +57 -0
  37. package/src/elements/application-host.ts +16 -3
  38. package/src/elements/database-element.test.ts +111 -0
  39. package/src/functions/iterate-self-and-ancestors.test.ts +29 -0
  40. package/src/functions/iterate-self-and-ancestors.ts +2 -2
  41. package/src/hooks/attach-decorator.test.ts +45 -0
  42. package/src/hooks/component/hooks-controller.cleanup.test.ts +65 -0
  43. package/src/hooks/component/hooks-controller.ts +15 -5
  44. package/src/hooks/component/stack.test.ts +37 -0
  45. package/src/hooks/test-host.test-utils.ts +43 -0
  46. package/src/hooks/use-connected.test.ts +88 -0
  47. package/src/hooks/use-debounce.test.ts +69 -0
  48. package/src/hooks/use-drag-generator.test.ts +67 -0
  49. package/src/hooks/use-drag-observe.test.ts +74 -0
  50. package/src/hooks/use-drag-observe.ts +2 -1
  51. package/src/hooks/use-drag-transaction.test.ts +109 -0
  52. package/src/hooks/use-drag-transaction.ts +48 -27
  53. package/src/hooks/use-draggable.test.ts +148 -0
  54. package/src/hooks/use-draggable.ts +73 -48
  55. package/src/hooks/use-effect.test.ts +51 -0
  56. package/src/hooks/use-effect.ts +5 -2
  57. package/src/hooks/use-element.test.ts +82 -0
  58. package/src/hooks/use-element.ts +17 -60
  59. package/src/hooks/use-memo.test.ts +66 -0
  60. package/src/hooks/use-memo.ts +16 -11
  61. package/src/hooks/use-observable-values.test.ts +53 -0
  62. package/src/hooks/use-observable.test.ts +63 -0
  63. package/src/hooks/use-observable.ts +13 -8
  64. package/src/hooks/use-pointer-observe.test.ts +59 -0
  65. package/src/hooks/use-ref.test.ts +50 -0
  66. package/src/hooks/use-ref.ts +4 -3
  67. package/src/hooks/use-updated.test.ts +98 -0
  68. package/src/hooks/use-window-event.test.ts +61 -0
  69. package/src/hooks/use-window-event.ts +1 -1
  70. package/src/hooks/with-hooks.test.ts +45 -0
  71. package/src/index.test.ts +26 -0
  72. package/src/public-types.type-test.ts +17 -0
  73. package/tsconfig.json +1 -0
  74. package/tsconfig.test.json +9 -0
  75. package/tsconfig.tsbuildinfo +1 -1
  76. package/vitest.config.ts +7 -0
@@ -0,0 +1,66 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ // @vitest-environment happy-dom
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { useMemo } from "./use-memo.js";
5
+ import { createHost, render, unmount } from "./test-host.test-utils.js";
6
+
7
+ describe("useMemo", () => {
8
+ it("returns the cached value while dependencies are equal", () => {
9
+ const host = createHost();
10
+ const calculate = vi.fn(() => ({}));
11
+ const first = render(host, () => useMemo(calculate, [1, "a"]));
12
+ expect(render(host, () => useMemo(calculate, [1, "a"]))).toBe(first);
13
+ expect(calculate).toHaveBeenCalledTimes(1);
14
+ });
15
+
16
+ it("recalculates when a dependency changes", () => {
17
+ const host = createHost();
18
+ render(host, () => useMemo(() => 1, [1]));
19
+ expect(render(host, () => useMemo(() => 2, [2]))).toBe(2);
20
+ });
21
+
22
+ it("calculates once with the default dependencies", () => {
23
+ const host = createHost();
24
+ const calculate = vi.fn(() => 1);
25
+ render(host, () => useMemo(calculate));
26
+ render(host, () => useMemo(calculate));
27
+ expect(calculate).toHaveBeenCalledTimes(1);
28
+ });
29
+
30
+ it.each([undefined, null, 0, false])("caches the falsy result %s", result => {
31
+ const host = createHost();
32
+ const calculate = vi.fn(() => result);
33
+ render(host, () => useMemo(calculate, []));
34
+ expect(render(host, () => useMemo(calculate, []))).toBe(result);
35
+ expect(calculate).toHaveBeenCalledTimes(1);
36
+ });
37
+
38
+ it("retries a calculation that threw", () => {
39
+ const host = createHost();
40
+ expect(() => render(host, () => useMemo(() => { throw new Error("failed"); }, []))).toThrow("failed");
41
+ expect(render(host, () => useMemo(() => 1, []))).toBe(1);
42
+ });
43
+
44
+ it("never requests a render", () => {
45
+ const host = createHost();
46
+ render(host, () => useMemo(() => 1, [1]));
47
+ render(host, () => useMemo(() => 2, [2]));
48
+ expect(host.requestUpdate).not.toHaveBeenCalled();
49
+ });
50
+
51
+ it("keeps independent memos per slot", () => {
52
+ const host = createHost();
53
+ const read = (n: number) => [useMemo(() => `a${n}`, [n]), useMemo(() => "b", [])];
54
+ render(host, () => read(1));
55
+ expect(render(host, () => read(2))).toEqual(["a2", "b"]);
56
+ });
57
+
58
+ it("recalculates after an unmount", () => {
59
+ const host = createHost();
60
+ const calculate = vi.fn(() => 1);
61
+ render(host, () => useMemo(calculate, []));
62
+ unmount(host);
63
+ render(host, () => useMemo(calculate, []));
64
+ expect(calculate).toHaveBeenCalledTimes(2);
65
+ });
66
+ });
@@ -1,16 +1,21 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
 
3
- import { useEffect } from "./use-effect.js";
4
- import { useState } from "./use-state.js";
3
+ import { equalsShallow } from "@adobe/data/equals-shallow";
4
+ import { Component_stack } from "./component/stack.js";
5
5
 
6
- export function useMemo<T>(calculateValue: () => T, dependencies: unknown[] = []): T {
7
- const [state, setState] = useState<T | undefined>(undefined);
8
- let currentValue = state;
9
-
10
- useEffect(() => {
11
- currentValue = calculateValue();
12
- setState(currentValue);
13
- }, dependencies);
6
+ interface Memo<T> {
7
+ value: T;
8
+ dependencies: unknown[];
9
+ }
14
10
 
15
- return currentValue!;
11
+ export function useMemo<T>(calculateValue: () => T, dependencies: unknown[] = []): T {
12
+ const component = Component_stack.active();
13
+ const hookIndex = component.hookIndex++;
14
+ const memo = component.hooks[hookIndex] as Memo<T> | undefined;
15
+ if (memo && equalsShallow(dependencies, memo.dependencies)) {
16
+ return memo.value;
17
+ }
18
+ const value = calculateValue();
19
+ component.hooks[hookIndex] = { value, dependencies } satisfies Memo<T>;
20
+ return value;
16
21
  }
@@ -0,0 +1,53 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ // @vitest-environment happy-dom
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { Observe } from "@adobe/data/observe";
5
+ import { useObservableValues } from "./use-observable-values.js";
6
+ import { createHost, render, unmount } from "./test-host.test-utils.js";
7
+
8
+ describe("useObservableValues", () => {
9
+ it("returns undefined until every source has emitted, then all latest values", () => {
10
+ const host = createHost();
11
+ const [name, setName] = Observe.createEvent<string>();
12
+ const [count, setCount] = Observe.createEvent<number>();
13
+ const factory = () => ({ name, count });
14
+
15
+ expect(render(host, () => useObservableValues(factory))).toBeUndefined();
16
+ setName("a");
17
+ expect(render(host, () => useObservableValues(factory))).toBeUndefined();
18
+ setCount(1);
19
+ expect(render(host, () => useObservableValues(factory))).toEqual({ name: "a", count: 1 });
20
+ setName("b");
21
+ expect(render(host, () => useObservableValues(factory))).toEqual({ name: "b", count: 1 });
22
+ unmount(host);
23
+ });
24
+
25
+ it("combines sources that emit synchronously in the first render", () => {
26
+ const host = createHost();
27
+ const factory = () => ({ a: Observe.fromConstant(1), b: Observe.fromConstant("x") });
28
+ expect(render(host, () => useObservableValues(factory))).toEqual({ a: 1, b: "x" });
29
+ unmount(host);
30
+ });
31
+
32
+ it("calls the factory again only when dependencies change", () => {
33
+ const host = createHost();
34
+ const factory = vi.fn(() => ({ a: Observe.fromConstant(1) }));
35
+ render(host, () => useObservableValues(factory, [1]));
36
+ render(host, () => useObservableValues(factory, [1]));
37
+ render(host, () => useObservableValues(factory, [2]));
38
+ expect(factory).toHaveBeenCalledTimes(2);
39
+ unmount(host);
40
+ });
41
+
42
+ it("unsubscribes from every source on unmount", () => {
43
+ const host = createHost();
44
+ const unsubscribed = vi.fn();
45
+ const tracked = <T,>(value: T): Observe<T> => observer => {
46
+ observer(value);
47
+ return unsubscribed;
48
+ };
49
+ render(host, () => useObservableValues(() => ({ a: tracked(1), b: tracked(2) })));
50
+ unmount(host);
51
+ expect(unsubscribed).toHaveBeenCalledTimes(2);
52
+ });
53
+ });
@@ -0,0 +1,63 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ // @vitest-environment happy-dom
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { Observe } from "@adobe/data/observe";
5
+ import { useObservable } from "./use-observable.js";
6
+ import { createHost, render, unmount } from "./test-host.test-utils.js";
7
+
8
+ function source<T>() {
9
+ const [observe, notify] = Observe.createEvent<T>();
10
+ const subscribe = vi.fn(observe);
11
+ return { observable: subscribe as Observe<T>, notify, subscribe };
12
+ }
13
+
14
+ describe("useObservable", () => {
15
+ it("returns undefined until the source emits, then the latest value", () => {
16
+ const host = createHost();
17
+ const { observable, notify } = source<number>();
18
+ expect(render(host, () => useObservable(observable))).toBeUndefined();
19
+ notify(1);
20
+ notify(2);
21
+ expect(render(host, () => useObservable(observable))).toBe(2);
22
+ });
23
+
24
+ it("returns a value emitted synchronously on subscribe in the same render", () => {
25
+ const host = createHost();
26
+ const observable: Observe<number> = observer => { observer(7); return () => {}; };
27
+ expect(render(host, () => useObservable(observable))).toBe(7);
28
+ });
29
+
30
+ it("requests a render for every notification, including a repeated reference", () => {
31
+ const host = createHost();
32
+ const { observable, notify } = source<{ count: number }>();
33
+ const value = { count: 0 };
34
+ render(host, () => useObservable(observable));
35
+ notify(value);
36
+ value.count = 1;
37
+ notify(value);
38
+ expect(host.requestUpdate).toHaveBeenCalledTimes(2);
39
+ expect(render(host, () => useObservable(observable))).toBe(value);
40
+ });
41
+
42
+ it("subscribes once per source and resubscribes when the source changes", () => {
43
+ const host = createHost();
44
+ const a = source<number>();
45
+ const b = source<number>();
46
+ render(host, () => useObservable(a.observable));
47
+ render(host, () => useObservable(a.observable));
48
+ render(host, () => useObservable(b.observable));
49
+ a.notify(1);
50
+ expect(a.subscribe).toHaveBeenCalledTimes(1);
51
+ expect(b.subscribe).toHaveBeenCalledTimes(1);
52
+ expect(host.requestUpdate).not.toHaveBeenCalled();
53
+ });
54
+
55
+ it("unsubscribes on unmount", () => {
56
+ const host = createHost();
57
+ const { observable, notify } = source<number>();
58
+ render(host, () => useObservable(observable));
59
+ unmount(host);
60
+ notify(1);
61
+ expect(host.requestUpdate).not.toHaveBeenCalled();
62
+ });
63
+ });
@@ -1,15 +1,20 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
 
3
- import { useState } from "./use-state.js";
3
+ import { Component_stack } from "./component/stack.js";
4
4
  import { useEffect } from "./use-effect.js";
5
+ import { useRef } from "./use-ref.js";
5
6
  import { Observe } from "@adobe/data/observe";
6
7
 
8
+ /**
9
+ * Subscribes to `observable` and returns its latest value. Every notification
10
+ * requests a render, including one that repeats the previous value or reference.
11
+ */
7
12
  export function useObservable<T>(observable: Observe<T>): T | undefined {
8
- let [value, setValue] = useState<T | undefined>(undefined);
9
- useEffect(() => {
10
- return observable(newValue => {
11
- setValue(value = newValue);
12
- });
13
- }, [observable]);
14
- return value;
13
+ const component = Component_stack.active();
14
+ const latest = useRef<T | undefined>(undefined);
15
+ useEffect(() => observable(value => {
16
+ latest.current = value;
17
+ component.requestUpdate();
18
+ }), [observable]);
19
+ return latest.current;
15
20
  }
@@ -0,0 +1,59 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ // @vitest-environment happy-dom
3
+ import { describe, expect, it } from "vitest";
4
+ import type { Vec2 } from "@adobe/data/math";
5
+ import { usePointerObserve } from "./use-pointer-observe.js";
6
+ import { createHost, render, unmount, type TestHost } from "./test-host.test-utils.js";
7
+
8
+ function placeAt(host: TestHost, left: number, top: number) {
9
+ host.getBoundingClientRect = () => ({ left, top, x: left, y: top, width: 100, height: 100, right: left + 100, bottom: top + 100, toJSON() {} });
10
+ }
11
+
12
+ const pointerMoveOver = (host: TestHost, clientX: number, clientY: number) =>
13
+ host.dispatchEvent(new PointerEvent("pointermove", { clientX, clientY }));
14
+
15
+ describe("usePointerObserve", () => {
16
+ it("emits pointer positions relative to the host", () => {
17
+ const host = createHost();
18
+ placeAt(host, 10, 20);
19
+ const positions: Vec2[] = [];
20
+ render(host, () => usePointerObserve([]))(position => positions.push(position));
21
+
22
+ pointerMoveOver(host, 15, 30);
23
+ pointerMoveOver(host, 50, 60);
24
+ expect(positions).toEqual([[5, 10], [40, 40]]);
25
+ unmount(host);
26
+ });
27
+
28
+ it("returns the same stream while dependencies are equal", () => {
29
+ const host = createHost();
30
+ const first = render(host, () => usePointerObserve([1]));
31
+ expect(render(host, () => usePointerObserve([1]))).toBe(first);
32
+ expect(render(host, () => usePointerObserve([2]))).not.toBe(first);
33
+ unmount(host);
34
+ });
35
+
36
+ it("delivers to every subscriber", () => {
37
+ const host = createHost();
38
+ placeAt(host, 0, 0);
39
+ const observe = render(host, () => usePointerObserve([]));
40
+ const a: Vec2[] = [];
41
+ const b: Vec2[] = [];
42
+ observe(position => a.push(position));
43
+ observe(position => b.push(position));
44
+ pointerMoveOver(host, 1, 2);
45
+ expect(a).toEqual([[1, 2]]);
46
+ expect(b).toEqual([[1, 2]]);
47
+ unmount(host);
48
+ });
49
+
50
+ it("stops listening on unmount", () => {
51
+ const host = createHost();
52
+ placeAt(host, 0, 0);
53
+ const positions: Vec2[] = [];
54
+ render(host, () => usePointerObserve([]))(position => positions.push(position));
55
+ unmount(host);
56
+ pointerMoveOver(host, 1, 1);
57
+ expect(positions).toEqual([]);
58
+ });
59
+ });
@@ -0,0 +1,50 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ // @vitest-environment happy-dom
3
+ import { describe, expect, it } from "vitest";
4
+ import { useRef } from "./use-ref.js";
5
+ import { createHost, render, unmount } from "./test-host.test-utils.js";
6
+
7
+ describe("useRef", () => {
8
+ it("returns the same ref across renders and keeps its mutations", () => {
9
+ const host = createHost();
10
+ const ref = render(host, () => useRef(1));
11
+ ref.current = 2;
12
+
13
+ const next = render(host, () => useRef(1));
14
+ expect(next).toBe(ref);
15
+ expect(next.current).toBe(2);
16
+ });
17
+
18
+ it("ignores the initial value after the first render", () => {
19
+ const host = createHost();
20
+ render(host, () => useRef("first"));
21
+ expect(render(host, () => useRef("second")).current).toBe("first");
22
+ });
23
+
24
+ it("stores a function as a value", () => {
25
+ const host = createHost();
26
+ const callback = () => 1;
27
+ expect(render(host, () => useRef(callback)).current).toBe(callback);
28
+ });
29
+
30
+ it("does not request a render when current changes", () => {
31
+ const host = createHost();
32
+ render(host, () => useRef(0)).current = 1;
33
+ expect(host.requestUpdate).not.toHaveBeenCalled();
34
+ });
35
+
36
+ it("keeps independent refs per slot and per host", () => {
37
+ const host = createHost();
38
+ const other = createHost();
39
+ const [a, b] = render(host, () => [useRef(0), useRef(0)]);
40
+ const c = render(other, () => useRef(0));
41
+ expect(new Set([a, b, c]).size).toBe(3);
42
+ });
43
+
44
+ it("starts from the initial value after an unmount", () => {
45
+ const host = createHost();
46
+ render(host, () => useRef(0)).current = 5;
47
+ unmount(host);
48
+ expect(render(host, () => useRef(0)).current).toBe(0);
49
+ });
50
+ });
@@ -1,8 +1,9 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
 
3
- import { useState } from "./use-state.js";
3
+ import { Component_stack } from "./component/stack.js";
4
4
 
5
5
  export function useRef<T>(initialValue: T): { current: T } {
6
- const [state] = useState({ current: initialValue });
7
- return state;
6
+ const component = Component_stack.active();
7
+ const hookIndex = component.hookIndex++;
8
+ return component.hooks[hookIndex] ??= { current: initialValue };
8
9
  }
@@ -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
+ });
@@ -12,5 +12,5 @@ export function useWindowEvent<K extends keyof WindowEventMap>(
12
12
  useEffect(() => {
13
13
  window.addEventListener(event, callback);
14
14
  return () => window.removeEventListener(event, callback);
15
- }, [callback]);
15
+ }, [event, callback]);
16
16
  }
@@ -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
@@ -7,5 +7,6 @@
7
7
  "types": []
8
8
  },
9
9
  "include": ["src/**/*"],
10
+ "exclude": ["src/**/*.test.ts", "src/**/*.test-utils.ts", "src/**/*.type-test.ts"],
10
11
  "references": []
11
12
  }
@@ -0,0 +1,9 @@
1
+ {
2
+ // Type-checks all of src, including the runtime and compile-time tests the build excludes.
3
+ "extends": "./tsconfig.json",
4
+ "compilerOptions": {
5
+ "composite": false,
6
+ "noEmit": true
7
+ },
8
+ "exclude": []
9
+ }