@praxisjs/composables 0.1.3 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @praxisjs/composables
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [d11a10a]
8
+ - @praxisjs/core@0.4.2
9
+
10
+ ## 0.1.4
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [fe39901]
15
+ - @praxisjs/core@0.4.1
16
+
3
17
  ## 0.1.3
4
18
 
5
19
  ### Patch Changes
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=browser.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/browser.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,118 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect, vi } from "vitest";
3
+ import { useMediaQuery, useColorScheme, useMouse, useKeyCombo, useIdle } from "../browser";
4
+ // ---------- useMediaQuery ----------
5
+ describe("useMediaQuery", () => {
6
+ it("returns computed that reflects matchMedia result", () => {
7
+ Object.defineProperty(window, "matchMedia", {
8
+ writable: true,
9
+ value: vi.fn((query) => ({
10
+ matches: query === "(prefers-color-scheme: dark)",
11
+ addEventListener: vi.fn(),
12
+ removeEventListener: vi.fn(),
13
+ })),
14
+ });
15
+ const isDark = useMediaQuery("(prefers-color-scheme: dark)");
16
+ expect(isDark()).toBe(true);
17
+ const isLight = useMediaQuery("(prefers-color-scheme: light)");
18
+ expect(isLight()).toBe(false);
19
+ });
20
+ });
21
+ // ---------- useColorScheme ----------
22
+ describe("useColorScheme", () => {
23
+ it("isDark and isLight are complementary", () => {
24
+ Object.defineProperty(window, "matchMedia", {
25
+ writable: true,
26
+ value: vi.fn(() => ({
27
+ matches: true,
28
+ addEventListener: vi.fn(),
29
+ removeEventListener: vi.fn(),
30
+ })),
31
+ });
32
+ const { isDark, isLight } = useColorScheme();
33
+ expect(isDark()).toBe(true);
34
+ expect(isLight()).toBe(false);
35
+ });
36
+ });
37
+ // ---------- useMouse ----------
38
+ describe("useMouse", () => {
39
+ it("starts at (0, 0)", () => {
40
+ const { x, y } = useMouse();
41
+ expect(x()).toBe(0);
42
+ expect(y()).toBe(0);
43
+ });
44
+ it("updates on mousemove event", () => {
45
+ const { x, y } = useMouse();
46
+ window.dispatchEvent(new MouseEvent("mousemove", { clientX: 100, clientY: 200 }));
47
+ expect(x()).toBe(100);
48
+ expect(y()).toBe(200);
49
+ });
50
+ });
51
+ // ---------- useKeyCombo ----------
52
+ describe("useKeyCombo", () => {
53
+ it("starts as false", () => {
54
+ const pressed = useKeyCombo("ctrl+s");
55
+ expect(pressed()).toBe(false);
56
+ });
57
+ it("becomes true on matching keydown", () => {
58
+ const pressed = useKeyCombo("ctrl+s");
59
+ window.dispatchEvent(new KeyboardEvent("keydown", { key: "s", ctrlKey: true }));
60
+ expect(pressed()).toBe(true);
61
+ });
62
+ it("resets to false on keyup", () => {
63
+ const pressed = useKeyCombo("ctrl+s");
64
+ window.dispatchEvent(new KeyboardEvent("keydown", { key: "s", ctrlKey: true }));
65
+ window.dispatchEvent(new KeyboardEvent("keyup"));
66
+ expect(pressed()).toBe(false);
67
+ });
68
+ });
69
+ // ---------- useIdle ----------
70
+ describe("useIdle", () => {
71
+ it("starts as not idle", () => {
72
+ vi.useFakeTimers();
73
+ const idle = useIdle(500);
74
+ expect(idle()).toBe(false);
75
+ vi.useRealTimers();
76
+ });
77
+ it("becomes idle after the timeout elapses", () => {
78
+ vi.useFakeTimers();
79
+ const idle = useIdle(500);
80
+ vi.advanceTimersByTime(500);
81
+ expect(idle()).toBe(true);
82
+ vi.useRealTimers();
83
+ });
84
+ it("resets idle on user activity", () => {
85
+ vi.useFakeTimers();
86
+ const idle = useIdle(500);
87
+ vi.advanceTimersByTime(300);
88
+ window.dispatchEvent(new MouseEvent("mousemove"));
89
+ vi.advanceTimersByTime(300); // not enough since activity reset the timer
90
+ expect(idle()).toBe(false);
91
+ vi.useRealTimers();
92
+ });
93
+ });
94
+ // ---------- useMediaQuery — change event ----------
95
+ describe("useMediaQuery change event", () => {
96
+ it("updates when the media query match state changes", () => {
97
+ let changeHandler = null;
98
+ Object.defineProperty(window, "matchMedia", {
99
+ writable: true,
100
+ value: vi.fn(() => ({
101
+ matches: false,
102
+ addEventListener: vi.fn((_event, handler) => {
103
+ changeHandler = handler;
104
+ }),
105
+ removeEventListener: vi.fn(),
106
+ })),
107
+ });
108
+ const matches = useMediaQuery("(min-width: 768px)");
109
+ expect(matches()).toBe(false);
110
+ // Simulate the media query becoming true
111
+ changeHandler({ matches: true });
112
+ expect(matches()).toBe(true);
113
+ // Simulate it reverting
114
+ changeHandler({ matches: false });
115
+ expect(matches()).toBe(false);
116
+ });
117
+ });
118
+ //# sourceMappingURL=browser.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.test.js","sourceRoot":"","sources":["../../src/__tests__/browser.test.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAC5B,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE3F,sCAAsC;AAEtC,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE;YAC1C,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,KAAa,EAAE,EAAE,CAAC,CAAC;gBAC/B,OAAO,EAAE,KAAK,KAAK,8BAA8B;gBACjD,gBAAgB,EAAE,EAAE,CAAC,EAAE,EAAE;gBACzB,mBAAmB,EAAE,EAAE,CAAC,EAAE,EAAE;aAC7B,CAAC,CAAC;SACJ,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,aAAa,CAAC,8BAA8B,CAAC,CAAC;QAC7D,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAE5B,MAAM,OAAO,GAAG,aAAa,CAAC,+BAA+B,CAAC,CAAC;QAC/D,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,uCAAuC;AAEvC,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE;YAC1C,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;gBAClB,OAAO,EAAE,IAAI;gBACb,gBAAgB,EAAE,EAAE,CAAC,EAAE,EAAE;gBACzB,mBAAmB,EAAE,EAAE,CAAC,EAAE,EAAE;aAC7B,CAAC,CAAC;SACJ,CAAC,CAAC;QAEH,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,cAAc,EAAE,CAAC;QAC7C,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5B,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,iCAAiC;AAEjC,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,EAAE,CAAC,kBAAkB,EAAE,GAAG,EAAE;QAC1B,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC5B,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACtB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC5B,MAAM,CAAC,aAAa,CAClB,IAAI,UAAU,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAC5D,CAAC;QACF,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACtB,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,oCAAoC;AAEpC,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,EAAE,CAAC,iBAAiB,EAAE,GAAG,EAAE;QACzB,MAAM,OAAO,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;QACtC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,OAAO,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;QACtC,MAAM,CAAC,aAAa,CAClB,IAAI,aAAa,CAAC,SAAS,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAC1D,CAAC;QACF,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;QAClC,MAAM,OAAO,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;QACtC,MAAM,CAAC,aAAa,CAClB,IAAI,aAAa,CAAC,SAAS,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAC1D,CAAC;QACF,MAAM,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;QACjD,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,gCAAgC;AAEhC,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE;IACvB,EAAE,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAC5B,EAAE,CAAC,aAAa,EAAE,CAAC;QACnB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC1B,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3B,EAAE,CAAC,aAAa,EAAE,CAAC;IACrB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,EAAE,CAAC,aAAa,EAAE,CAAC;QACnB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC1B,EAAE,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC;QAC5B,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1B,EAAE,CAAC,aAAa,EAAE,CAAC;IACrB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,EAAE,CAAC,aAAa,EAAE,CAAC;QACnB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC1B,EAAE,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC;QAC5B,MAAM,CAAC,aAAa,CAAC,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC;QAClD,EAAE,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC,4CAA4C;QACzE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3B,EAAE,CAAC,aAAa,EAAE,CAAC;IACrB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,qDAAqD;AAErD,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC1C,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,IAAI,aAAa,GAA8C,IAAI,CAAC;QACpE,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE;YAC1C,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;gBAClB,OAAO,EAAE,KAAK;gBACd,gBAAgB,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,MAAc,EAAE,OAAyC,EAAE,EAAE;oBACpF,aAAa,GAAG,OAAO,CAAC;gBAC1B,CAAC,CAAC;gBACF,mBAAmB,EAAE,EAAE,CAAC,EAAE,EAAE;aAC7B,CAAC,CAAC;SACJ,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,aAAa,CAAC,oBAAoB,CAAC,CAAC;QACpD,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE9B,yCAAyC;QACzC,aAAc,CAAC,EAAE,OAAO,EAAE,IAAI,EAAyB,CAAC,CAAC;QACzD,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAE7B,wBAAwB;QACxB,aAAc,CAAC,EAAE,OAAO,EAAE,KAAK,EAAyB,CAAC,CAAC;QAC1D,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=dom.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/dom.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,167 @@
1
+ // @vitest-environment jsdom
2
+ import { beforeEach, afterEach, describe, it, expect, vi } from "vitest";
3
+ import { createRef, useWindowSize, useScrollPosition, useFocus, useElementSize, useIntersection } from "../dom";
4
+ // ---------- createRef ----------
5
+ describe("createRef", () => {
6
+ it("starts with current = null", () => {
7
+ const ref = createRef();
8
+ expect(ref.current).toBeNull();
9
+ });
10
+ it("can be assigned an element", () => {
11
+ const ref = createRef();
12
+ const el = document.createElement("div");
13
+ ref.current = el;
14
+ expect(ref.current).toBe(el);
15
+ });
16
+ });
17
+ // ---------- useWindowSize ----------
18
+ describe("useWindowSize", () => {
19
+ it("returns the current window dimensions", () => {
20
+ Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 1024 });
21
+ Object.defineProperty(window, "innerHeight", { writable: true, configurable: true, value: 768 });
22
+ const { width, height } = useWindowSize();
23
+ expect(width()).toBe(1024);
24
+ expect(height()).toBe(768);
25
+ });
26
+ it("updates on resize event", () => {
27
+ Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 800 });
28
+ Object.defineProperty(window, "innerHeight", { writable: true, configurable: true, value: 600 });
29
+ const { width, height } = useWindowSize();
30
+ Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 1280 });
31
+ Object.defineProperty(window, "innerHeight", { writable: true, configurable: true, value: 720 });
32
+ window.dispatchEvent(new Event("resize"));
33
+ expect(width()).toBe(1280);
34
+ expect(height()).toBe(720);
35
+ });
36
+ });
37
+ // ---------- useScrollPosition ----------
38
+ describe("useScrollPosition", () => {
39
+ it("starts at (0, 0)", () => {
40
+ const { x, y } = useScrollPosition(window);
41
+ expect(x()).toBe(0);
42
+ expect(y()).toBe(0);
43
+ });
44
+ it("updates on element scroll", () => {
45
+ const el = document.createElement("div");
46
+ const { x, y } = useScrollPosition(el);
47
+ Object.defineProperty(el, "scrollLeft", { configurable: true, value: 50 });
48
+ Object.defineProperty(el, "scrollTop", { configurable: true, value: 80 });
49
+ el.dispatchEvent(new Event("scroll"));
50
+ expect(x()).toBe(50);
51
+ expect(y()).toBe(80);
52
+ });
53
+ });
54
+ // ---------- useFocus ----------
55
+ describe("useFocus", () => {
56
+ it("starts as not focused", () => {
57
+ const el = document.createElement("input");
58
+ document.body.appendChild(el);
59
+ const ref = { current: el };
60
+ const focused = useFocus(ref);
61
+ expect(focused()).toBe(false);
62
+ document.body.removeChild(el);
63
+ });
64
+ it("becomes true on focus event", () => {
65
+ const el = document.createElement("input");
66
+ document.body.appendChild(el);
67
+ const ref = { current: el };
68
+ const focused = useFocus(ref);
69
+ el.dispatchEvent(new FocusEvent("focus"));
70
+ expect(focused()).toBe(true);
71
+ document.body.removeChild(el);
72
+ });
73
+ it("becomes false on blur event", () => {
74
+ const el = document.createElement("input");
75
+ document.body.appendChild(el);
76
+ const ref = { current: el };
77
+ const focused = useFocus(ref);
78
+ el.dispatchEvent(new FocusEvent("focus"));
79
+ el.dispatchEvent(new FocusEvent("blur"));
80
+ expect(focused()).toBe(false);
81
+ document.body.removeChild(el);
82
+ });
83
+ it("does nothing when ref.current is null", () => {
84
+ const ref = { current: null };
85
+ expect(() => useFocus(ref)).not.toThrow();
86
+ });
87
+ });
88
+ // ---------- useElementSize ----------
89
+ describe("useElementSize", () => {
90
+ let observerCallback = () => { };
91
+ let mockDisconnect;
92
+ beforeEach(() => {
93
+ mockDisconnect = vi.fn();
94
+ observerCallback = () => { };
95
+ // Class-based mock so `new ResizeObserver(cb)` works correctly
96
+ globalThis.ResizeObserver = class {
97
+ constructor(cb) { observerCallback = cb; }
98
+ observe = vi.fn();
99
+ disconnect = mockDisconnect;
100
+ };
101
+ });
102
+ afterEach(() => {
103
+ // @ts-expect-error – removing test stub
104
+ delete globalThis.ResizeObserver;
105
+ });
106
+ it("starts at (0, 0) when ref.current is null", () => {
107
+ const ref = { current: null };
108
+ const { width, height } = useElementSize(ref);
109
+ expect(width()).toBe(0);
110
+ expect(height()).toBe(0);
111
+ });
112
+ it("calls stop() to disconnect the ResizeObserver", () => {
113
+ const el = document.createElement("div");
114
+ const ref = { current: el };
115
+ const { stop } = useElementSize(ref);
116
+ stop();
117
+ expect(mockDisconnect).toHaveBeenCalled();
118
+ });
119
+ it("updates width and height when ResizeObserver fires", () => {
120
+ const el = document.createElement("div");
121
+ const ref = { current: el };
122
+ const { width, height } = useElementSize(ref);
123
+ observerCallback([{ contentRect: { width: 300, height: 200 } }], null);
124
+ expect(width()).toBe(300);
125
+ expect(height()).toBe(200);
126
+ });
127
+ });
128
+ // ---------- useIntersection ----------
129
+ describe("useIntersection", () => {
130
+ let intersectionCallback = () => { };
131
+ let capturedOptions;
132
+ beforeEach(() => {
133
+ intersectionCallback = () => { };
134
+ capturedOptions = undefined;
135
+ globalThis.IntersectionObserver = class {
136
+ constructor(cb, opts) {
137
+ intersectionCallback = cb;
138
+ capturedOptions = opts;
139
+ }
140
+ observe = vi.fn();
141
+ disconnect = vi.fn();
142
+ };
143
+ });
144
+ afterEach(() => {
145
+ // @ts-expect-error – removing test stub
146
+ delete globalThis.IntersectionObserver;
147
+ });
148
+ it("starts as false when ref.current is null", () => {
149
+ const ref = { current: null };
150
+ const visible = useIntersection(ref);
151
+ expect(visible()).toBe(false);
152
+ });
153
+ it("becomes true when IntersectionObserver fires with isIntersecting=true", () => {
154
+ const el = document.createElement("div");
155
+ const ref = { current: el };
156
+ const visible = useIntersection(ref);
157
+ intersectionCallback([{ isIntersecting: true }], null);
158
+ expect(visible()).toBe(true);
159
+ });
160
+ it("passes options to IntersectionObserver", () => {
161
+ const ref = { current: null };
162
+ const options = { threshold: 0.5 };
163
+ useIntersection(ref, options);
164
+ expect(capturedOptions).toEqual(options);
165
+ });
166
+ });
167
+ //# sourceMappingURL=dom.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom.test.js","sourceRoot":"","sources":["../../src/__tests__/dom.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=utilities.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utilities.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/utilities.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,236 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
3
+ import { signal } from "@praxisjs/core/internal";
4
+ import { usePagination, useClipboard, useGeolocation, useTimeAgo } from "../utilities";
5
+ describe("usePagination", () => {
6
+ it("starts on page 1 by default", () => {
7
+ const p = usePagination({ total: 100, pageSize: 10 });
8
+ expect(p.page()).toBe(1);
9
+ });
10
+ it("respects initial page option", () => {
11
+ const p = usePagination({ total: 100, pageSize: 10, initial: 3 });
12
+ expect(p.page()).toBe(3);
13
+ });
14
+ it("computes total pages correctly", () => {
15
+ expect(usePagination({ total: 100, pageSize: 10 }).totalPages()).toBe(10);
16
+ expect(usePagination({ total: 101, pageSize: 10 }).totalPages()).toBe(11);
17
+ expect(usePagination({ total: 5, pageSize: 10 }).totalPages()).toBe(1);
18
+ });
19
+ it("computes offset", () => {
20
+ const p = usePagination({ total: 100, pageSize: 10 });
21
+ expect(p.offset()).toBe(0);
22
+ p.goTo(3);
23
+ expect(p.offset()).toBe(20);
24
+ });
25
+ it("next() advances the page", () => {
26
+ const p = usePagination({ total: 30, pageSize: 10 });
27
+ p.next();
28
+ expect(p.page()).toBe(2);
29
+ });
30
+ it("next() does nothing on last page", () => {
31
+ const p = usePagination({ total: 10, pageSize: 10 });
32
+ p.next();
33
+ expect(p.page()).toBe(1);
34
+ });
35
+ it("prev() goes back", () => {
36
+ const p = usePagination({ total: 30, pageSize: 10, initial: 3 });
37
+ p.prev();
38
+ expect(p.page()).toBe(2);
39
+ });
40
+ it("prev() does nothing on first page", () => {
41
+ const p = usePagination({ total: 30, pageSize: 10 });
42
+ p.prev();
43
+ expect(p.page()).toBe(1);
44
+ });
45
+ it("goTo() clamps to valid range", () => {
46
+ const p = usePagination({ total: 30, pageSize: 10 });
47
+ p.goTo(0);
48
+ expect(p.page()).toBe(1);
49
+ p.goTo(99);
50
+ expect(p.page()).toBe(3);
51
+ });
52
+ it("first() jumps to page 1", () => {
53
+ const p = usePagination({ total: 50, pageSize: 10, initial: 5 });
54
+ p.first();
55
+ expect(p.page()).toBe(1);
56
+ });
57
+ it("last() jumps to last page", () => {
58
+ const p = usePagination({ total: 50, pageSize: 10 });
59
+ p.last();
60
+ expect(p.page()).toBe(5);
61
+ });
62
+ it("hasNext is false on last page", () => {
63
+ const p = usePagination({ total: 10, pageSize: 10 });
64
+ expect(p.hasNext()).toBe(false);
65
+ });
66
+ it("hasPrev is false on first page", () => {
67
+ const p = usePagination({ total: 30, pageSize: 10 });
68
+ expect(p.hasPrev()).toBe(false);
69
+ });
70
+ it("pages() returns array of page numbers", () => {
71
+ const p = usePagination({ total: 30, pageSize: 10 });
72
+ expect(p.pages()).toEqual([1, 2, 3]);
73
+ });
74
+ it("pageSize() reflects the configured size", () => {
75
+ const p = usePagination({ total: 100, pageSize: 25 });
76
+ expect(p.pageSize()).toBe(25);
77
+ });
78
+ it("accepts a factory function for options", () => {
79
+ const p = usePagination(() => ({ total: 50, pageSize: 5 }));
80
+ expect(p.totalPages()).toBe(10);
81
+ });
82
+ });
83
+ // ── useClipboard ──────────────────────────────────────────────────────────────
84
+ describe("useClipboard", () => {
85
+ beforeEach(() => {
86
+ vi.useFakeTimers();
87
+ Object.defineProperty(navigator, "clipboard", {
88
+ value: { writeText: vi.fn().mockResolvedValue(undefined) },
89
+ configurable: true,
90
+ writable: true,
91
+ });
92
+ });
93
+ afterEach(() => {
94
+ vi.clearAllTimers();
95
+ vi.useRealTimers();
96
+ });
97
+ it("copied starts as false", () => {
98
+ const { copied } = useClipboard();
99
+ expect(copied()).toBe(false);
100
+ });
101
+ it("content starts as empty string", () => {
102
+ const { content } = useClipboard();
103
+ expect(content()).toBe("");
104
+ });
105
+ it("sets copied to true and content to the text after copy()", async () => {
106
+ const { copy, copied, content } = useClipboard();
107
+ await copy("hello");
108
+ expect(copied()).toBe(true);
109
+ expect(content()).toBe("hello");
110
+ });
111
+ it("resets copied to false after resetDelay", async () => {
112
+ const { copy, copied } = useClipboard(1000);
113
+ await copy("test");
114
+ expect(copied()).toBe(true);
115
+ vi.advanceTimersByTime(1000);
116
+ expect(copied()).toBe(false);
117
+ });
118
+ it("warns and does not update state on clipboard failure", async () => {
119
+ navigator.clipboard.writeText =
120
+ vi.fn().mockRejectedValue(new Error("denied"));
121
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
122
+ const { copy, copied } = useClipboard();
123
+ await copy("fail");
124
+ expect(copied()).toBe(false);
125
+ expect(warn).toHaveBeenCalled();
126
+ warn.mockRestore();
127
+ });
128
+ });
129
+ // ── useGeolocation ────────────────────────────────────────────────────────────
130
+ describe("useGeolocation", () => {
131
+ it("starts with loading=true and lat/lng null", () => {
132
+ const successCb = [];
133
+ Object.defineProperty(navigator, "geolocation", {
134
+ value: {
135
+ getCurrentPosition: (success) => {
136
+ successCb.push(success);
137
+ },
138
+ },
139
+ configurable: true,
140
+ });
141
+ const geo = useGeolocation();
142
+ expect(geo.loading()).toBe(true);
143
+ expect(geo.lat()).toBeNull();
144
+ expect(geo.lng()).toBeNull();
145
+ });
146
+ it("updates lat/lng/loading on success", () => {
147
+ let successCb = null;
148
+ Object.defineProperty(navigator, "geolocation", {
149
+ value: {
150
+ getCurrentPosition: (success) => {
151
+ successCb = success;
152
+ },
153
+ },
154
+ configurable: true,
155
+ });
156
+ const geo = useGeolocation();
157
+ successCb({
158
+ coords: { latitude: 10.5, longitude: 20.5 },
159
+ });
160
+ expect(geo.lat()).toBe(10.5);
161
+ expect(geo.lng()).toBe(20.5);
162
+ expect(geo.loading()).toBe(false);
163
+ });
164
+ it("sets error and loading=false on failure", () => {
165
+ let errorCb = null;
166
+ Object.defineProperty(navigator, "geolocation", {
167
+ value: {
168
+ getCurrentPosition: (_s, error) => {
169
+ errorCb = error;
170
+ },
171
+ },
172
+ configurable: true,
173
+ });
174
+ const geo = useGeolocation();
175
+ const fakeError = { code: 1, message: "denied" };
176
+ errorCb(fakeError);
177
+ expect(geo.error()).toBe(fakeError);
178
+ expect(geo.loading()).toBe(false);
179
+ });
180
+ });
181
+ // ── useTimeAgo ────────────────────────────────────────────────────────────────
182
+ describe("useTimeAgo", () => {
183
+ beforeEach(() => {
184
+ vi.useFakeTimers();
185
+ });
186
+ afterEach(() => {
187
+ vi.clearAllTimers();
188
+ vi.useRealTimers();
189
+ });
190
+ it("returns a computed string", () => {
191
+ const now = Date.now();
192
+ const t = useTimeAgo(() => now);
193
+ expect(typeof t()).toBe("string");
194
+ });
195
+ it("formats seconds ago", () => {
196
+ const now = Date.now();
197
+ const t = useTimeAgo(() => now - 30_000);
198
+ expect(t()).toMatch(/segundo|second/i);
199
+ });
200
+ it("formats minutes ago", () => {
201
+ const now = Date.now();
202
+ const t = useTimeAgo(() => now - 5 * 60_000);
203
+ expect(t()).toMatch(/minuto|minute/i);
204
+ });
205
+ it("formats hours ago", () => {
206
+ const now = Date.now();
207
+ const t = useTimeAgo(() => now - 2 * 3_600_000);
208
+ expect(t()).toMatch(/hora|hour/i);
209
+ });
210
+ it("formats days ago", () => {
211
+ const now = Date.now();
212
+ const t = useTimeAgo(() => now - 3 * 86_400_000);
213
+ expect(t()).toMatch(/dia|day/i);
214
+ });
215
+ it("formats months ago", () => {
216
+ const now = Date.now();
217
+ const t = useTimeAgo(() => now - 60 * 86_400_000);
218
+ expect(t()).toMatch(/mes|month/i);
219
+ });
220
+ it("accepts a signal source", () => {
221
+ const src = signal(Date.now() - 90_000);
222
+ const t = useTimeAgo(src);
223
+ expect(typeof t()).toBe("string");
224
+ });
225
+ it("ticks after 60 seconds via setInterval", () => {
226
+ const now = Date.now();
227
+ const t = useTimeAgo(() => now - 30_000);
228
+ const before = t();
229
+ vi.advanceTimersByTime(60_000);
230
+ // After ticking, the computed re-evaluates (may or may not change the string)
231
+ expect(typeof t()).toBe("string");
232
+ // The interval ran — just verify no errors
233
+ expect(before).toBeTruthy();
234
+ });
235
+ });
236
+ //# sourceMappingURL=utilities.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utilities.test.js","sourceRoot":"","sources":["../../src/__tests__/utilities.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisjs/composables",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -14,8 +14,8 @@
14
14
  "typescript": "^5.9.3"
15
15
  },
16
16
  "dependencies": {
17
- "@praxisjs/core": "0.4.0",
18
- "@praxisjs/shared": "0.2.0"
17
+ "@praxisjs/shared": "0.2.0",
18
+ "@praxisjs/core": "0.4.2"
19
19
  },
20
20
  "scripts": {
21
21
  "build": "tsc",
@@ -0,0 +1,147 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect, vi } from "vitest";
3
+
4
+ import { useMediaQuery, useColorScheme, useMouse, useKeyCombo, useIdle } from "../browser";
5
+
6
+ // ---------- useMediaQuery ----------
7
+
8
+ describe("useMediaQuery", () => {
9
+ it("returns computed that reflects matchMedia result", () => {
10
+ Object.defineProperty(window, "matchMedia", {
11
+ writable: true,
12
+ value: vi.fn((query: string) => ({
13
+ matches: query === "(prefers-color-scheme: dark)",
14
+ addEventListener: vi.fn(),
15
+ removeEventListener: vi.fn(),
16
+ })),
17
+ });
18
+
19
+ const isDark = useMediaQuery("(prefers-color-scheme: dark)");
20
+ expect(isDark()).toBe(true);
21
+
22
+ const isLight = useMediaQuery("(prefers-color-scheme: light)");
23
+ expect(isLight()).toBe(false);
24
+ });
25
+ });
26
+
27
+ // ---------- useColorScheme ----------
28
+
29
+ describe("useColorScheme", () => {
30
+ it("isDark and isLight are complementary", () => {
31
+ Object.defineProperty(window, "matchMedia", {
32
+ writable: true,
33
+ value: vi.fn(() => ({
34
+ matches: true,
35
+ addEventListener: vi.fn(),
36
+ removeEventListener: vi.fn(),
37
+ })),
38
+ });
39
+
40
+ const { isDark, isLight } = useColorScheme();
41
+ expect(isDark()).toBe(true);
42
+ expect(isLight()).toBe(false);
43
+ });
44
+ });
45
+
46
+ // ---------- useMouse ----------
47
+
48
+ describe("useMouse", () => {
49
+ it("starts at (0, 0)", () => {
50
+ const { x, y } = useMouse();
51
+ expect(x()).toBe(0);
52
+ expect(y()).toBe(0);
53
+ });
54
+
55
+ it("updates on mousemove event", () => {
56
+ const { x, y } = useMouse();
57
+ window.dispatchEvent(
58
+ new MouseEvent("mousemove", { clientX: 100, clientY: 200 }),
59
+ );
60
+ expect(x()).toBe(100);
61
+ expect(y()).toBe(200);
62
+ });
63
+ });
64
+
65
+ // ---------- useKeyCombo ----------
66
+
67
+ describe("useKeyCombo", () => {
68
+ it("starts as false", () => {
69
+ const pressed = useKeyCombo("ctrl+s");
70
+ expect(pressed()).toBe(false);
71
+ });
72
+
73
+ it("becomes true on matching keydown", () => {
74
+ const pressed = useKeyCombo("ctrl+s");
75
+ window.dispatchEvent(
76
+ new KeyboardEvent("keydown", { key: "s", ctrlKey: true }),
77
+ );
78
+ expect(pressed()).toBe(true);
79
+ });
80
+
81
+ it("resets to false on keyup", () => {
82
+ const pressed = useKeyCombo("ctrl+s");
83
+ window.dispatchEvent(
84
+ new KeyboardEvent("keydown", { key: "s", ctrlKey: true }),
85
+ );
86
+ window.dispatchEvent(new KeyboardEvent("keyup"));
87
+ expect(pressed()).toBe(false);
88
+ });
89
+ });
90
+
91
+ // ---------- useIdle ----------
92
+
93
+ describe("useIdle", () => {
94
+ it("starts as not idle", () => {
95
+ vi.useFakeTimers();
96
+ const idle = useIdle(500);
97
+ expect(idle()).toBe(false);
98
+ vi.useRealTimers();
99
+ });
100
+
101
+ it("becomes idle after the timeout elapses", () => {
102
+ vi.useFakeTimers();
103
+ const idle = useIdle(500);
104
+ vi.advanceTimersByTime(500);
105
+ expect(idle()).toBe(true);
106
+ vi.useRealTimers();
107
+ });
108
+
109
+ it("resets idle on user activity", () => {
110
+ vi.useFakeTimers();
111
+ const idle = useIdle(500);
112
+ vi.advanceTimersByTime(300);
113
+ window.dispatchEvent(new MouseEvent("mousemove"));
114
+ vi.advanceTimersByTime(300); // not enough since activity reset the timer
115
+ expect(idle()).toBe(false);
116
+ vi.useRealTimers();
117
+ });
118
+ });
119
+
120
+ // ---------- useMediaQuery — change event ----------
121
+
122
+ describe("useMediaQuery change event", () => {
123
+ it("updates when the media query match state changes", () => {
124
+ let changeHandler: ((e: MediaQueryListEvent) => void) | null = null;
125
+ Object.defineProperty(window, "matchMedia", {
126
+ writable: true,
127
+ value: vi.fn(() => ({
128
+ matches: false,
129
+ addEventListener: vi.fn((_event: string, handler: (e: MediaQueryListEvent) => void) => {
130
+ changeHandler = handler;
131
+ }),
132
+ removeEventListener: vi.fn(),
133
+ })),
134
+ });
135
+
136
+ const matches = useMediaQuery("(min-width: 768px)");
137
+ expect(matches()).toBe(false);
138
+
139
+ // Simulate the media query becoming true
140
+ changeHandler!({ matches: true } as MediaQueryListEvent);
141
+ expect(matches()).toBe(true);
142
+
143
+ // Simulate it reverting
144
+ changeHandler!({ matches: false } as MediaQueryListEvent);
145
+ expect(matches()).toBe(false);
146
+ });
147
+ });
@@ -0,0 +1,212 @@
1
+ // @vitest-environment jsdom
2
+ import { beforeEach, afterEach, describe, it, expect, vi } from "vitest";
3
+
4
+ import { createRef, useWindowSize, useScrollPosition, useFocus, useElementSize, useIntersection } from "../dom";
5
+
6
+ // ---------- createRef ----------
7
+
8
+ describe("createRef", () => {
9
+ it("starts with current = null", () => {
10
+ const ref = createRef();
11
+ expect(ref.current).toBeNull();
12
+ });
13
+
14
+ it("can be assigned an element", () => {
15
+ const ref = createRef();
16
+ const el = document.createElement("div");
17
+ ref.current = el;
18
+ expect(ref.current).toBe(el);
19
+ });
20
+ });
21
+
22
+ // ---------- useWindowSize ----------
23
+
24
+ describe("useWindowSize", () => {
25
+ it("returns the current window dimensions", () => {
26
+ Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 1024 });
27
+ Object.defineProperty(window, "innerHeight", { writable: true, configurable: true, value: 768 });
28
+
29
+ const { width, height } = useWindowSize();
30
+ expect(width()).toBe(1024);
31
+ expect(height()).toBe(768);
32
+ });
33
+
34
+ it("updates on resize event", () => {
35
+ Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 800 });
36
+ Object.defineProperty(window, "innerHeight", { writable: true, configurable: true, value: 600 });
37
+
38
+ const { width, height } = useWindowSize();
39
+
40
+ Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 1280 });
41
+ Object.defineProperty(window, "innerHeight", { writable: true, configurable: true, value: 720 });
42
+ window.dispatchEvent(new Event("resize"));
43
+
44
+ expect(width()).toBe(1280);
45
+ expect(height()).toBe(720);
46
+ });
47
+ });
48
+
49
+ // ---------- useScrollPosition ----------
50
+
51
+ describe("useScrollPosition", () => {
52
+ it("starts at (0, 0)", () => {
53
+ const { x, y } = useScrollPosition(window);
54
+ expect(x()).toBe(0);
55
+ expect(y()).toBe(0);
56
+ });
57
+
58
+ it("updates on element scroll", () => {
59
+ const el = document.createElement("div");
60
+ const { x, y } = useScrollPosition(el);
61
+
62
+ Object.defineProperty(el, "scrollLeft", { configurable: true, value: 50 });
63
+ Object.defineProperty(el, "scrollTop", { configurable: true, value: 80 });
64
+ el.dispatchEvent(new Event("scroll"));
65
+
66
+ expect(x()).toBe(50);
67
+ expect(y()).toBe(80);
68
+ });
69
+ });
70
+
71
+ // ---------- useFocus ----------
72
+
73
+ describe("useFocus", () => {
74
+ it("starts as not focused", () => {
75
+ const el = document.createElement("input");
76
+ document.body.appendChild(el);
77
+ const ref = { current: el };
78
+ const focused = useFocus(ref);
79
+ expect(focused()).toBe(false);
80
+ document.body.removeChild(el);
81
+ });
82
+
83
+ it("becomes true on focus event", () => {
84
+ const el = document.createElement("input");
85
+ document.body.appendChild(el);
86
+ const ref = { current: el };
87
+ const focused = useFocus(ref);
88
+ el.dispatchEvent(new FocusEvent("focus"));
89
+ expect(focused()).toBe(true);
90
+ document.body.removeChild(el);
91
+ });
92
+
93
+ it("becomes false on blur event", () => {
94
+ const el = document.createElement("input");
95
+ document.body.appendChild(el);
96
+ const ref = { current: el };
97
+ const focused = useFocus(ref);
98
+ el.dispatchEvent(new FocusEvent("focus"));
99
+ el.dispatchEvent(new FocusEvent("blur"));
100
+ expect(focused()).toBe(false);
101
+ document.body.removeChild(el);
102
+ });
103
+
104
+ it("does nothing when ref.current is null", () => {
105
+ const ref: { current: HTMLElement | null } = { current: null };
106
+ expect(() => useFocus(ref)).not.toThrow();
107
+ });
108
+ });
109
+
110
+ // ---------- useElementSize ----------
111
+
112
+ describe("useElementSize", () => {
113
+ let observerCallback: ResizeObserverCallback = () => {};
114
+ let mockDisconnect: ReturnType<typeof vi.fn>;
115
+
116
+ beforeEach(() => {
117
+ mockDisconnect = vi.fn();
118
+ observerCallback = () => {};
119
+
120
+ // Class-based mock so `new ResizeObserver(cb)` works correctly
121
+ globalThis.ResizeObserver = class {
122
+ constructor(cb: ResizeObserverCallback) { observerCallback = cb; }
123
+ observe = vi.fn();
124
+ disconnect = mockDisconnect;
125
+ } as unknown as typeof ResizeObserver;
126
+ });
127
+
128
+ afterEach(() => {
129
+ // @ts-expect-error – removing test stub
130
+ delete globalThis.ResizeObserver;
131
+ });
132
+
133
+ it("starts at (0, 0) when ref.current is null", () => {
134
+ const ref: { current: HTMLElement | null } = { current: null };
135
+ const { width, height } = useElementSize(ref);
136
+ expect(width()).toBe(0);
137
+ expect(height()).toBe(0);
138
+ });
139
+
140
+ it("calls stop() to disconnect the ResizeObserver", () => {
141
+ const el = document.createElement("div");
142
+ const ref = { current: el };
143
+ const { stop } = useElementSize(ref);
144
+ stop();
145
+ expect(mockDisconnect).toHaveBeenCalled();
146
+ });
147
+
148
+ it("updates width and height when ResizeObserver fires", () => {
149
+ const el = document.createElement("div");
150
+ const ref = { current: el };
151
+ const { width, height } = useElementSize(ref);
152
+
153
+ observerCallback(
154
+ [{ contentRect: { width: 300, height: 200 } } as ResizeObserverEntry],
155
+ null as unknown as ResizeObserver,
156
+ );
157
+
158
+ expect(width()).toBe(300);
159
+ expect(height()).toBe(200);
160
+ });
161
+ });
162
+
163
+ // ---------- useIntersection ----------
164
+
165
+ describe("useIntersection", () => {
166
+ let intersectionCallback: IntersectionObserverCallback = () => {};
167
+ let capturedOptions: IntersectionObserverInit | undefined;
168
+
169
+ beforeEach(() => {
170
+ intersectionCallback = () => {};
171
+ capturedOptions = undefined;
172
+
173
+ globalThis.IntersectionObserver = class {
174
+ constructor(cb: IntersectionObserverCallback, opts?: IntersectionObserverInit) {
175
+ intersectionCallback = cb;
176
+ capturedOptions = opts;
177
+ }
178
+ observe = vi.fn();
179
+ disconnect = vi.fn();
180
+ } as unknown as typeof IntersectionObserver;
181
+ });
182
+
183
+ afterEach(() => {
184
+ // @ts-expect-error – removing test stub
185
+ delete globalThis.IntersectionObserver;
186
+ });
187
+
188
+ it("starts as false when ref.current is null", () => {
189
+ const ref: { current: HTMLElement | null } = { current: null };
190
+ const visible = useIntersection(ref);
191
+ expect(visible()).toBe(false);
192
+ });
193
+
194
+ it("becomes true when IntersectionObserver fires with isIntersecting=true", () => {
195
+ const el = document.createElement("div");
196
+ const ref = { current: el };
197
+ const visible = useIntersection(ref);
198
+
199
+ intersectionCallback(
200
+ [{ isIntersecting: true } as IntersectionObserverEntry],
201
+ null as unknown as IntersectionObserver,
202
+ );
203
+ expect(visible()).toBe(true);
204
+ });
205
+
206
+ it("passes options to IntersectionObserver", () => {
207
+ const ref: { current: HTMLElement | null } = { current: null };
208
+ const options = { threshold: 0.5 };
209
+ useIntersection(ref, options);
210
+ expect(capturedOptions).toEqual(options);
211
+ });
212
+ });
@@ -0,0 +1,282 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
3
+
4
+ import { signal } from "@praxisjs/core/internal";
5
+
6
+ import { usePagination, useClipboard, useGeolocation, useTimeAgo } from "../utilities";
7
+
8
+ describe("usePagination", () => {
9
+ it("starts on page 1 by default", () => {
10
+ const p = usePagination({ total: 100, pageSize: 10 });
11
+ expect(p.page()).toBe(1);
12
+ });
13
+
14
+ it("respects initial page option", () => {
15
+ const p = usePagination({ total: 100, pageSize: 10, initial: 3 });
16
+ expect(p.page()).toBe(3);
17
+ });
18
+
19
+ it("computes total pages correctly", () => {
20
+ expect(usePagination({ total: 100, pageSize: 10 }).totalPages()).toBe(10);
21
+ expect(usePagination({ total: 101, pageSize: 10 }).totalPages()).toBe(11);
22
+ expect(usePagination({ total: 5, pageSize: 10 }).totalPages()).toBe(1);
23
+ });
24
+
25
+ it("computes offset", () => {
26
+ const p = usePagination({ total: 100, pageSize: 10 });
27
+ expect(p.offset()).toBe(0);
28
+ p.goTo(3);
29
+ expect(p.offset()).toBe(20);
30
+ });
31
+
32
+ it("next() advances the page", () => {
33
+ const p = usePagination({ total: 30, pageSize: 10 });
34
+ p.next();
35
+ expect(p.page()).toBe(2);
36
+ });
37
+
38
+ it("next() does nothing on last page", () => {
39
+ const p = usePagination({ total: 10, pageSize: 10 });
40
+ p.next();
41
+ expect(p.page()).toBe(1);
42
+ });
43
+
44
+ it("prev() goes back", () => {
45
+ const p = usePagination({ total: 30, pageSize: 10, initial: 3 });
46
+ p.prev();
47
+ expect(p.page()).toBe(2);
48
+ });
49
+
50
+ it("prev() does nothing on first page", () => {
51
+ const p = usePagination({ total: 30, pageSize: 10 });
52
+ p.prev();
53
+ expect(p.page()).toBe(1);
54
+ });
55
+
56
+ it("goTo() clamps to valid range", () => {
57
+ const p = usePagination({ total: 30, pageSize: 10 });
58
+ p.goTo(0);
59
+ expect(p.page()).toBe(1);
60
+ p.goTo(99);
61
+ expect(p.page()).toBe(3);
62
+ });
63
+
64
+ it("first() jumps to page 1", () => {
65
+ const p = usePagination({ total: 50, pageSize: 10, initial: 5 });
66
+ p.first();
67
+ expect(p.page()).toBe(1);
68
+ });
69
+
70
+ it("last() jumps to last page", () => {
71
+ const p = usePagination({ total: 50, pageSize: 10 });
72
+ p.last();
73
+ expect(p.page()).toBe(5);
74
+ });
75
+
76
+ it("hasNext is false on last page", () => {
77
+ const p = usePagination({ total: 10, pageSize: 10 });
78
+ expect(p.hasNext()).toBe(false);
79
+ });
80
+
81
+ it("hasPrev is false on first page", () => {
82
+ const p = usePagination({ total: 30, pageSize: 10 });
83
+ expect(p.hasPrev()).toBe(false);
84
+ });
85
+
86
+ it("pages() returns array of page numbers", () => {
87
+ const p = usePagination({ total: 30, pageSize: 10 });
88
+ expect(p.pages()).toEqual([1, 2, 3]);
89
+ });
90
+
91
+ it("pageSize() reflects the configured size", () => {
92
+ const p = usePagination({ total: 100, pageSize: 25 });
93
+ expect(p.pageSize()).toBe(25);
94
+ });
95
+
96
+ it("accepts a factory function for options", () => {
97
+ const p = usePagination(() => ({ total: 50, pageSize: 5 }));
98
+ expect(p.totalPages()).toBe(10);
99
+ });
100
+ });
101
+
102
+ // ── useClipboard ──────────────────────────────────────────────────────────────
103
+
104
+ describe("useClipboard", () => {
105
+ beforeEach(() => {
106
+ vi.useFakeTimers();
107
+ Object.defineProperty(navigator, "clipboard", {
108
+ value: { writeText: vi.fn().mockResolvedValue(undefined) },
109
+ configurable: true,
110
+ writable: true,
111
+ });
112
+ });
113
+
114
+ afterEach(() => {
115
+ vi.clearAllTimers();
116
+ vi.useRealTimers();
117
+ });
118
+
119
+ it("copied starts as false", () => {
120
+ const { copied } = useClipboard();
121
+ expect(copied()).toBe(false);
122
+ });
123
+
124
+ it("content starts as empty string", () => {
125
+ const { content } = useClipboard();
126
+ expect(content()).toBe("");
127
+ });
128
+
129
+ it("sets copied to true and content to the text after copy()", async () => {
130
+ const { copy, copied, content } = useClipboard();
131
+ await copy("hello");
132
+ expect(copied()).toBe(true);
133
+ expect(content()).toBe("hello");
134
+ });
135
+
136
+ it("resets copied to false after resetDelay", async () => {
137
+ const { copy, copied } = useClipboard(1000);
138
+ await copy("test");
139
+ expect(copied()).toBe(true);
140
+ vi.advanceTimersByTime(1000);
141
+ expect(copied()).toBe(false);
142
+ });
143
+
144
+ it("warns and does not update state on clipboard failure", async () => {
145
+ (navigator.clipboard as unknown as { writeText: ReturnType<typeof vi.fn> }).writeText =
146
+ vi.fn().mockRejectedValue(new Error("denied"));
147
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
148
+ const { copy, copied } = useClipboard();
149
+ await copy("fail");
150
+ expect(copied()).toBe(false);
151
+ expect(warn).toHaveBeenCalled();
152
+ warn.mockRestore();
153
+ });
154
+ });
155
+
156
+ // ── useGeolocation ────────────────────────────────────────────────────────────
157
+
158
+ describe("useGeolocation", () => {
159
+ it("starts with loading=true and lat/lng null", () => {
160
+ const successCb: PositionCallback[] = [];
161
+ Object.defineProperty(navigator, "geolocation", {
162
+ value: {
163
+ getCurrentPosition: (success: PositionCallback) => {
164
+ successCb.push(success);
165
+ },
166
+ },
167
+ configurable: true,
168
+ });
169
+
170
+ const geo = useGeolocation();
171
+ expect(geo.loading()).toBe(true);
172
+ expect(geo.lat()).toBeNull();
173
+ expect(geo.lng()).toBeNull();
174
+ });
175
+
176
+ it("updates lat/lng/loading on success", () => {
177
+ let successCb: PositionCallback | null = null;
178
+ Object.defineProperty(navigator, "geolocation", {
179
+ value: {
180
+ getCurrentPosition: (success: PositionCallback) => {
181
+ successCb = success;
182
+ },
183
+ },
184
+ configurable: true,
185
+ });
186
+
187
+ const geo = useGeolocation();
188
+
189
+ successCb!({
190
+ coords: { latitude: 10.5, longitude: 20.5 } as GeolocationCoordinates,
191
+ } as GeolocationPosition);
192
+
193
+ expect(geo.lat()).toBe(10.5);
194
+ expect(geo.lng()).toBe(20.5);
195
+ expect(geo.loading()).toBe(false);
196
+ });
197
+
198
+ it("sets error and loading=false on failure", () => {
199
+ let errorCb: PositionErrorCallback | null = null;
200
+ Object.defineProperty(navigator, "geolocation", {
201
+ value: {
202
+ getCurrentPosition: (_s: PositionCallback, error: PositionErrorCallback) => {
203
+ errorCb = error;
204
+ },
205
+ },
206
+ configurable: true,
207
+ });
208
+
209
+ const geo = useGeolocation();
210
+ const fakeError = { code: 1, message: "denied" } as GeolocationPositionError;
211
+ errorCb!(fakeError);
212
+
213
+ expect(geo.error()).toBe(fakeError);
214
+ expect(geo.loading()).toBe(false);
215
+ });
216
+ });
217
+
218
+ // ── useTimeAgo ────────────────────────────────────────────────────────────────
219
+
220
+ describe("useTimeAgo", () => {
221
+ beforeEach(() => {
222
+ vi.useFakeTimers();
223
+ });
224
+
225
+ afterEach(() => {
226
+ vi.clearAllTimers();
227
+ vi.useRealTimers();
228
+ });
229
+
230
+ it("returns a computed string", () => {
231
+ const now = Date.now();
232
+ const t = useTimeAgo(() => now);
233
+ expect(typeof t()).toBe("string");
234
+ });
235
+
236
+ it("formats seconds ago", () => {
237
+ const now = Date.now();
238
+ const t = useTimeAgo(() => now - 30_000);
239
+ expect(t()).toMatch(/segundo|second/i);
240
+ });
241
+
242
+ it("formats minutes ago", () => {
243
+ const now = Date.now();
244
+ const t = useTimeAgo(() => now - 5 * 60_000);
245
+ expect(t()).toMatch(/minuto|minute/i);
246
+ });
247
+
248
+ it("formats hours ago", () => {
249
+ const now = Date.now();
250
+ const t = useTimeAgo(() => now - 2 * 3_600_000);
251
+ expect(t()).toMatch(/hora|hour/i);
252
+ });
253
+
254
+ it("formats days ago", () => {
255
+ const now = Date.now();
256
+ const t = useTimeAgo(() => now - 3 * 86_400_000);
257
+ expect(t()).toMatch(/dia|day/i);
258
+ });
259
+
260
+ it("formats months ago", () => {
261
+ const now = Date.now();
262
+ const t = useTimeAgo(() => now - 60 * 86_400_000);
263
+ expect(t()).toMatch(/mes|month/i);
264
+ });
265
+
266
+ it("accepts a signal source", () => {
267
+ const src = signal(Date.now() - 90_000);
268
+ const t = useTimeAgo(src);
269
+ expect(typeof t()).toBe("string");
270
+ });
271
+
272
+ it("ticks after 60 seconds via setInterval", () => {
273
+ const now = Date.now();
274
+ const t = useTimeAgo(() => now - 30_000);
275
+ const before = t();
276
+ vi.advanceTimersByTime(60_000);
277
+ // After ticking, the computed re-evaluates (may or may not change the string)
278
+ expect(typeof t()).toBe("string");
279
+ // The interval ran — just verify no errors
280
+ expect(before).toBeTruthy();
281
+ });
282
+ });