@supermousejs/core 2.4.1 → 2.4.2
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 +6 -0
- package/README.md +334 -334
- package/dist/index.mjs +1 -1
- package/dist/index.umd.js +1 -1
- package/package.json +7 -1
- package/src/Supermouse.ts +0 -960
- package/src/__tests__/core.spec.ts +0 -291
- package/src/__tests__/cursor-mode.spec.ts +0 -109
- package/src/__tests__/input.spec.ts +0 -261
- package/src/__tests__/integration.spec.ts +0 -121
- package/src/__tests__/lifecycle.spec.ts +0 -199
- package/src/__tests__/nested-instance.spec.ts +0 -58
- package/src/__tests__/plugin.spec.ts +0 -130
- package/src/__tests__/setup.ts +0 -21
- package/src/__tests__/stage.spec.ts +0 -235
- package/src/__tests__/suspend-resume.spec.ts +0 -50
- package/src/index.ts +0 -2
- package/src/types.ts +0 -165
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
- package/vite.config.ts +0 -32
- package/vitest.config.ts +0 -10
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
|
-
import { Supermouse } from "../Supermouse";
|
|
3
|
-
import { DEFAULT_HOVER_SELECTORS } from "../Supermouse";
|
|
4
|
-
|
|
5
|
-
describe("Supermouse core", () => {
|
|
6
|
-
let app: Supermouse;
|
|
7
|
-
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
app?.destroy();
|
|
10
|
-
document.body.innerHTML = "";
|
|
11
|
-
document.head.innerHTML = "";
|
|
12
|
-
vi.restoreAllMocks();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("registers and looks up plugins by name", () => {
|
|
16
|
-
const app = new Supermouse();
|
|
17
|
-
|
|
18
|
-
const plugin = {
|
|
19
|
-
name: "test-plugin",
|
|
20
|
-
install() {},
|
|
21
|
-
update() {}
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
app.use(plugin);
|
|
25
|
-
|
|
26
|
-
expect(app.getPlugin("test-plugin")).toBe(plugin);
|
|
27
|
-
expect(app.getPlugin("missing-plugin")).toBeUndefined();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("applies default options", () => {
|
|
31
|
-
app = new Supermouse({ autoStart: false });
|
|
32
|
-
expect(app.options.smoothness).toBe(0.15);
|
|
33
|
-
expect(app.options.enableTouch).toBe(false);
|
|
34
|
-
expect(app.options.autoDisableOnMobile).toBe(true);
|
|
35
|
-
expect(app.options.cursor).toBe("auto");
|
|
36
|
-
expect(app.options.hideOnLeave).toBe(true);
|
|
37
|
-
expect(app.options.autoStart).toBe(false);
|
|
38
|
-
expect(app.options.container).toBe(document.body);
|
|
39
|
-
expect(app.options.dataPrefix).toBe("supermouse");
|
|
40
|
-
expect(app.options.zIndex).toBe(9999);
|
|
41
|
-
expect((app.options as any).ignoreOnNative).toBeUndefined();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("merges user options over defaults", () => {
|
|
45
|
-
const container = document.createElement("div");
|
|
46
|
-
document.body.appendChild(container);
|
|
47
|
-
|
|
48
|
-
app = new Supermouse({
|
|
49
|
-
smoothness: 0.3,
|
|
50
|
-
enableTouch: true,
|
|
51
|
-
autoDisableOnMobile: false,
|
|
52
|
-
cursor: "custom",
|
|
53
|
-
hideOnLeave: false,
|
|
54
|
-
autoStart: false,
|
|
55
|
-
container,
|
|
56
|
-
dataPrefix: "custom",
|
|
57
|
-
zIndex: 1234
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
expect(app.options.smoothness).toBe(0.3);
|
|
61
|
-
expect(app.options.enableTouch).toBe(true);
|
|
62
|
-
expect(app.options.autoDisableOnMobile).toBe(false);
|
|
63
|
-
expect(app.options.cursor).toBe("custom");
|
|
64
|
-
expect(app.options.hideOnLeave).toBe(false);
|
|
65
|
-
expect(app.options.autoStart).toBe(false);
|
|
66
|
-
expect(app.options.container).toBe(container);
|
|
67
|
-
expect(app.options.dataPrefix).toBe("custom");
|
|
68
|
-
expect(app.options.zIndex).toBe(1234);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("initializes state with correct shape", () => {
|
|
72
|
-
app = new Supermouse({ autoStart: false });
|
|
73
|
-
const { state } = app;
|
|
74
|
-
expect(state.pointer).toEqual({ x: -100, y: -100 });
|
|
75
|
-
expect(state.target).toEqual({ x: -100, y: -100 });
|
|
76
|
-
expect(state.smooth).toEqual({ x: -100, y: -100 });
|
|
77
|
-
expect(state.velocity).toEqual({ x: 0, y: 0 });
|
|
78
|
-
expect(state.displacement).toEqual({ x: 0, y: 0 });
|
|
79
|
-
expect(state.angle).toBe(0);
|
|
80
|
-
expect(state.isDown).toBe(false);
|
|
81
|
-
expect(state.isHover).toBe(false);
|
|
82
|
-
expect(state.isNative).toBe(false);
|
|
83
|
-
expect(state.cursorMode).toBe("auto");
|
|
84
|
-
expect(state.hoverTarget).toBeNull();
|
|
85
|
-
expect(state.reducedMotion).toBe(false);
|
|
86
|
-
expect(state.hasReceivedInput).toBe(false);
|
|
87
|
-
expect(state.shape).toBeNull();
|
|
88
|
-
expect(state.interaction).toEqual({});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("appends stage element to container (default body)", () => {
|
|
92
|
-
app = new Supermouse({ autoStart: false });
|
|
93
|
-
const stage = app.stage;
|
|
94
|
-
expect(stage).toBeInstanceOf(HTMLDivElement);
|
|
95
|
-
expect(document.body.contains(stage)).toBe(true);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("appends stage element to custom container", () => {
|
|
99
|
-
const container = document.createElement("div");
|
|
100
|
-
document.body.appendChild(container);
|
|
101
|
-
app = new Supermouse({ container, autoStart: false });
|
|
102
|
-
expect(container.contains(app.stage)).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("exposes version", () => {
|
|
106
|
-
app = new Supermouse({ autoStart: false });
|
|
107
|
-
expect(app.version).toBeDefined();
|
|
108
|
-
expect(typeof app.version).toBe("string");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("sets default hover selectors", () => {
|
|
112
|
-
app = new Supermouse({ autoStart: false });
|
|
113
|
-
const selectors = Array.from((app as any).hoverSelectors);
|
|
114
|
-
expect(selectors).toEqual(expect.arrayContaining(DEFAULT_HOVER_SELECTORS));
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("overrides hover selectors if provided", () => {
|
|
118
|
-
app = new Supermouse({
|
|
119
|
-
autoStart: false,
|
|
120
|
-
hoverSelectors: [".my-custom-hover", "a"]
|
|
121
|
-
});
|
|
122
|
-
const selectors = Array.from((app as any).hoverSelectors);
|
|
123
|
-
expect(selectors).toEqual([".my-custom-hover", "a"]);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("auto-starts the loop if autoStart is true", () => {
|
|
127
|
-
const spy = vi.spyOn(Supermouse.prototype as any, "startLoop");
|
|
128
|
-
app = new Supermouse({ autoStart: true });
|
|
129
|
-
expect(spy).toHaveBeenCalled();
|
|
130
|
-
spy.mockRestore();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("setCursor updates cursorMode", () => {
|
|
134
|
-
app = new Supermouse({ autoStart: false });
|
|
135
|
-
app.setCursor("native");
|
|
136
|
-
expect(app.state.cursorMode).toBe("native");
|
|
137
|
-
app.setCursor("custom");
|
|
138
|
-
expect(app.state.cursorMode).toBe("custom");
|
|
139
|
-
app.setCursor("auto");
|
|
140
|
-
expect(app.state.cursorMode).toBe("auto");
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("cursor option sets initial cursorMode", () => {
|
|
144
|
-
app = new Supermouse({ autoStart: false, cursor: "native" });
|
|
145
|
-
expect(app.state.cursorMode).toBe("native");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("reads data attributes into interaction on hover", () => {
|
|
149
|
-
app = new Supermouse({ autoStart: false });
|
|
150
|
-
const div = document.createElement("div");
|
|
151
|
-
div.setAttribute("data-supermouse-test", "hello");
|
|
152
|
-
div.setAttribute("data-supermouse-flag", "");
|
|
153
|
-
div.setAttribute("data-cursor", "");
|
|
154
|
-
document.body.appendChild(div);
|
|
155
|
-
|
|
156
|
-
div.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
|
157
|
-
|
|
158
|
-
expect(app.state.isHover).toBe(true);
|
|
159
|
-
expect(app.state.hoverTarget).toBe(div);
|
|
160
|
-
expect(app.state.interaction).toEqual({ test: "hello", flag: true });
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("evaluates rules on hover", () => {
|
|
164
|
-
app = new Supermouse({
|
|
165
|
-
autoStart: false,
|
|
166
|
-
rules: {
|
|
167
|
-
"[data-cursor]": { custom: "rule-value" }
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
const div = document.createElement("div");
|
|
171
|
-
div.setAttribute("data-cursor", "");
|
|
172
|
-
document.body.appendChild(div);
|
|
173
|
-
|
|
174
|
-
div.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
|
175
|
-
|
|
176
|
-
expect(app.state.interaction).toEqual({ custom: "rule-value" });
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("caches selector matching for rules", () => {
|
|
180
|
-
app = new Supermouse({
|
|
181
|
-
autoStart: false,
|
|
182
|
-
rules: {
|
|
183
|
-
"[data-cursor]": { test: "value" }
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
const div = document.createElement("div");
|
|
187
|
-
div.setAttribute("data-cursor", "");
|
|
188
|
-
document.body.appendChild(div);
|
|
189
|
-
div.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
|
190
|
-
|
|
191
|
-
const input = (app as any).input;
|
|
192
|
-
const spy = vi.spyOn(input, "matchesSelector");
|
|
193
|
-
|
|
194
|
-
// Same element again – selector matching should be cached
|
|
195
|
-
input.parseDOMInteraction(div);
|
|
196
|
-
expect(spy).not.toHaveBeenCalled();
|
|
197
|
-
|
|
198
|
-
// Different element – should trigger selector matching
|
|
199
|
-
const div2 = document.createElement("div");
|
|
200
|
-
div2.setAttribute("data-cursor", "");
|
|
201
|
-
document.body.appendChild(div2);
|
|
202
|
-
input.parseDOMInteraction(div2);
|
|
203
|
-
expect(spy).toHaveBeenCalled();
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("re-enable restores original cursor mode after disable", () => {
|
|
207
|
-
app = new Supermouse({ cursor: "custom", autoStart: false });
|
|
208
|
-
expect(app.state.cursorMode).toBe("custom");
|
|
209
|
-
|
|
210
|
-
app.disable();
|
|
211
|
-
expect(app.state.cursorMode).toBe("custom");
|
|
212
|
-
|
|
213
|
-
app.enable();
|
|
214
|
-
expect(app.state.cursorMode).toBe("custom");
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("suspend/resume does not change cursor mode", () => {
|
|
218
|
-
app = new Supermouse({ cursor: "custom", autoStart: false });
|
|
219
|
-
app.start();
|
|
220
|
-
app.suspend();
|
|
221
|
-
expect(app.state.cursorMode).toBe("custom");
|
|
222
|
-
app.resume();
|
|
223
|
-
expect(app.state.cursorMode).toBe("custom");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("hideOnLeave resets pointer and hasReceivedInput", () => {
|
|
227
|
-
app = new Supermouse({ hideOnLeave: true, autoStart: false });
|
|
228
|
-
window.dispatchEvent(
|
|
229
|
-
new PointerEvent("pointermove", { clientX: 50, clientY: 60, pointerType: "mouse" })
|
|
230
|
-
);
|
|
231
|
-
expect(app.state.hasReceivedInput).toBe(true);
|
|
232
|
-
|
|
233
|
-
document.dispatchEvent(new MouseEvent("mouseout", { relatedTarget: null }));
|
|
234
|
-
expect(app.state.hasReceivedInput).toBe(false);
|
|
235
|
-
expect(app.state.pointer).toEqual({ x: -100, y: -100 });
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it("scoped instances maintain independent cursor modes", () => {
|
|
239
|
-
const container1 = document.createElement("div");
|
|
240
|
-
const container2 = document.createElement("div");
|
|
241
|
-
document.body.appendChild(container1);
|
|
242
|
-
document.body.appendChild(container2);
|
|
243
|
-
|
|
244
|
-
const app1 = new Supermouse({ container: container1, cursor: "custom", autoStart: false });
|
|
245
|
-
const app2 = new Supermouse({ container: container2, cursor: "native", autoStart: false });
|
|
246
|
-
|
|
247
|
-
expect(app1.state.cursorMode).toBe("custom");
|
|
248
|
-
expect(app2.state.cursorMode).toBe("native");
|
|
249
|
-
|
|
250
|
-
app1.disable();
|
|
251
|
-
expect(app1.state.cursorMode).toBe("custom");
|
|
252
|
-
expect(app2.state.cursorMode).toBe("native"); // unaffected
|
|
253
|
-
|
|
254
|
-
app1.destroy();
|
|
255
|
-
app2.destroy();
|
|
256
|
-
container1.remove();
|
|
257
|
-
container2.remove();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it("matches simple selectors directly", () => {
|
|
261
|
-
app = new Supermouse({
|
|
262
|
-
autoStart: false,
|
|
263
|
-
rules: {
|
|
264
|
-
".simple": { test: "ok" }
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
const div = document.createElement("div");
|
|
268
|
-
div.className = "simple";
|
|
269
|
-
document.body.appendChild(div);
|
|
270
|
-
div.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
|
271
|
-
expect(app.state.interaction).toEqual({ test: "ok" });
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it("matches complex selectors with ancestor", () => {
|
|
275
|
-
app = new Supermouse({
|
|
276
|
-
autoStart: false,
|
|
277
|
-
rules: {
|
|
278
|
-
".ancestor .descendant": { custom: "value" }
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
const ancestor = document.createElement("div");
|
|
282
|
-
ancestor.className = "ancestor";
|
|
283
|
-
const child = document.createElement("span");
|
|
284
|
-
child.className = "descendant";
|
|
285
|
-
ancestor.appendChild(child);
|
|
286
|
-
document.body.appendChild(ancestor);
|
|
287
|
-
|
|
288
|
-
child.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
|
289
|
-
expect(app.state.interaction).toEqual({ custom: "value" });
|
|
290
|
-
});
|
|
291
|
-
});
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
|
-
import { Supermouse } from "../Supermouse";
|
|
3
|
-
|
|
4
|
-
describe("Supermouse cursor modes", () => {
|
|
5
|
-
let app: Supermouse;
|
|
6
|
-
let container: HTMLElement;
|
|
7
|
-
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
app?.destroy();
|
|
10
|
-
document.body.innerHTML = "";
|
|
11
|
-
document.head.innerHTML = "";
|
|
12
|
-
vi.restoreAllMocks();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const hasClassPrefix = (el: HTMLElement, prefix: string) =>
|
|
16
|
-
Array.from(el.classList).some((c) => c.startsWith(prefix));
|
|
17
|
-
|
|
18
|
-
function createApp(cursor: "auto" | "custom" | "native" | "both") {
|
|
19
|
-
container = document.createElement("div");
|
|
20
|
-
document.body.appendChild(container);
|
|
21
|
-
app = new Supermouse({ container, cursor, autoStart: false });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function hoverElement(el: HTMLElement) {
|
|
25
|
-
el.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
|
26
|
-
// Trigger an update cycle so core applies cursor decisions
|
|
27
|
-
app.step(performance.now() + 16);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function movePointer(x: number, y: number) {
|
|
31
|
-
window.dispatchEvent(
|
|
32
|
-
new PointerEvent("pointermove", { clientX: x, clientY: y, pointerType: "mouse" })
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
it("auto mode marks native inputs as isNative and hides custom stage", () => {
|
|
37
|
-
createApp("auto");
|
|
38
|
-
const input = document.createElement("input");
|
|
39
|
-
container.appendChild(input);
|
|
40
|
-
movePointer(50, 50);
|
|
41
|
-
hoverElement(input);
|
|
42
|
-
|
|
43
|
-
expect(app.state.isNative).toBe(true);
|
|
44
|
-
expect(app.stage.style.opacity).toBe("0");
|
|
45
|
-
expect(container.style.cursor).toBe("");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("custom mode ignores native detection and shows custom cursor over inputs", () => {
|
|
49
|
-
createApp("custom");
|
|
50
|
-
const input = document.createElement("input");
|
|
51
|
-
container.appendChild(input);
|
|
52
|
-
movePointer(50, 50);
|
|
53
|
-
hoverElement(input);
|
|
54
|
-
|
|
55
|
-
expect(app.state.isNative).toBe(false);
|
|
56
|
-
expect(app.stage.style.opacity).toBe("1");
|
|
57
|
-
expect(container.style.cursor).toBe("none");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("both mode shows custom cursor and keeps native cursor visible over inputs", () => {
|
|
61
|
-
createApp("both");
|
|
62
|
-
const input = document.createElement("input");
|
|
63
|
-
container.appendChild(input);
|
|
64
|
-
movePointer(50, 50);
|
|
65
|
-
hoverElement(input);
|
|
66
|
-
|
|
67
|
-
expect(app.state.isNative).toBe(false);
|
|
68
|
-
expect(app.stage.style.opacity).toBe("1");
|
|
69
|
-
expect(container.style.cursor).toBe("");
|
|
70
|
-
expect(hasClassPrefix(container, "supermouse-hide-")).toBe(false);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("native mode hides custom stage and keeps native cursor over inputs", () => {
|
|
74
|
-
createApp("native");
|
|
75
|
-
const input = document.createElement("input");
|
|
76
|
-
container.appendChild(input);
|
|
77
|
-
movePointer(50, 50);
|
|
78
|
-
hoverElement(input);
|
|
79
|
-
|
|
80
|
-
expect(app.state.isNative).toBe(false);
|
|
81
|
-
expect(app.stage.style.opacity).toBe("0");
|
|
82
|
-
expect(container.style.cursor).toBe("");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("auto mode respects data-supermouse-ignore attribute (still native)", () => {
|
|
86
|
-
createApp("auto");
|
|
87
|
-
const div = document.createElement("div");
|
|
88
|
-
div.setAttribute("data-supermouse-ignore", "");
|
|
89
|
-
container.appendChild(div);
|
|
90
|
-
movePointer(50, 50);
|
|
91
|
-
hoverElement(div);
|
|
92
|
-
|
|
93
|
-
expect(app.state.isNative).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("custom mode ignores data-supermouse-ignore and keeps custom", () => {
|
|
97
|
-
createApp("custom");
|
|
98
|
-
const div = document.createElement("div");
|
|
99
|
-
div.setAttribute("data-supermouse-ignore", "");
|
|
100
|
-
container.appendChild(div);
|
|
101
|
-
movePointer(50, 50);
|
|
102
|
-
hoverElement(div);
|
|
103
|
-
|
|
104
|
-
expect(app.state.isNative).toBe(false);
|
|
105
|
-
expect(app.stage.style.opacity).toBe("1");
|
|
106
|
-
expect(container.style.cursor).toBe("none");
|
|
107
|
-
expect(hasClassPrefix(container, "supermouse-hide-")).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach, vi, beforeEach } from "vitest";
|
|
2
|
-
import { Supermouse } from "../Supermouse";
|
|
3
|
-
|
|
4
|
-
describe("Supermouse input handling", () => {
|
|
5
|
-
let app: Supermouse;
|
|
6
|
-
let container: HTMLElement;
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
container = document.createElement("div");
|
|
10
|
-
document.body.appendChild(container);
|
|
11
|
-
// Ensure the container is not body for relative coordinate tests
|
|
12
|
-
app = new Supermouse({ container, autoStart: false });
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
app?.destroy();
|
|
17
|
-
document.body.innerHTML = "";
|
|
18
|
-
document.head.innerHTML = "";
|
|
19
|
-
vi.restoreAllMocks();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
function dispatchPointerMove(x: number, y: number, pointerType = "mouse") {
|
|
23
|
-
const event = new PointerEvent("pointermove", {
|
|
24
|
-
clientX: x,
|
|
25
|
-
clientY: y,
|
|
26
|
-
pointerType,
|
|
27
|
-
bubbles: true
|
|
28
|
-
});
|
|
29
|
-
window.dispatchEvent(event);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function dispatchPointerDown() {
|
|
33
|
-
window.dispatchEvent(new PointerEvent("pointerdown", { pointerType: "mouse", bubbles: true }));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function dispatchPointerUp() {
|
|
37
|
-
window.dispatchEvent(new PointerEvent("pointerup", { pointerType: "mouse", bubbles: true }));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function dispatchMouseOver(target: HTMLElement) {
|
|
41
|
-
target.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function dispatchMouseOut(target: HTMLElement, relatedTarget?: Node) {
|
|
45
|
-
const event = new MouseEvent("mouseout", {
|
|
46
|
-
bubbles: true,
|
|
47
|
-
relatedTarget: relatedTarget ?? null
|
|
48
|
-
});
|
|
49
|
-
target.dispatchEvent(event);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
it("updates pointer and sets hasReceivedInput on first move", () => {
|
|
53
|
-
dispatchPointerMove(100, 150);
|
|
54
|
-
expect(app.state.pointer).toEqual({ x: 100, y: 150 });
|
|
55
|
-
expect(app.state.hasReceivedInput).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("continues updating pointer on subsequent moves", () => {
|
|
59
|
-
dispatchPointerMove(100, 150);
|
|
60
|
-
dispatchPointerMove(200, 250);
|
|
61
|
-
expect(app.state.pointer).toEqual({ x: 200, y: 250 });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("ignores touch events when autoDisableOnMobile is true and enableTouch is false", () => {
|
|
65
|
-
// Simulate a coarse pointer device by mocking matchMedia
|
|
66
|
-
vi.spyOn(window, "matchMedia").mockImplementation(
|
|
67
|
-
(query) =>
|
|
68
|
-
({
|
|
69
|
-
matches: query === "(pointer: fine)" ? false : false,
|
|
70
|
-
media: query,
|
|
71
|
-
onchange: null,
|
|
72
|
-
addListener: () => {},
|
|
73
|
-
removeListener: () => {},
|
|
74
|
-
addEventListener: () => {},
|
|
75
|
-
removeEventListener: () => {},
|
|
76
|
-
dispatchEvent: () => false
|
|
77
|
-
}) as MediaQueryList
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
// Recreate app with the mock (need to destroy previous)
|
|
81
|
-
app.destroy();
|
|
82
|
-
app = new Supermouse({ container, autoStart: false });
|
|
83
|
-
|
|
84
|
-
dispatchPointerMove(50, 60, "touch");
|
|
85
|
-
// Touch should be ignored, so pointer remains offscreen
|
|
86
|
-
expect(app.state.pointer).toEqual({ x: -100, y: -100 });
|
|
87
|
-
expect(app.state.hasReceivedInput).toBe(false);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("sets isDown on pointerdown and clears on pointerup", () => {
|
|
91
|
-
dispatchPointerDown();
|
|
92
|
-
expect(app.state.isDown).toBe(true);
|
|
93
|
-
dispatchPointerUp();
|
|
94
|
-
expect(app.state.isDown).toBe(false);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("detects hover on registered selector and parses data attributes", () => {
|
|
98
|
-
const link = document.createElement("a");
|
|
99
|
-
link.setAttribute("data-supermouse-text", "Hello");
|
|
100
|
-
link.setAttribute("data-supermouse-icon", "pointer");
|
|
101
|
-
link.setAttribute("data-supermouse-magnetic", ""); // empty becomes true
|
|
102
|
-
container.appendChild(link);
|
|
103
|
-
|
|
104
|
-
dispatchMouseOver(link);
|
|
105
|
-
|
|
106
|
-
expect(app.state.isHover).toBe(true);
|
|
107
|
-
expect(app.state.hoverTarget).toBe(link);
|
|
108
|
-
expect(app.state.interaction).toEqual({
|
|
109
|
-
text: "Hello",
|
|
110
|
-
icon: "pointer",
|
|
111
|
-
magnetic: true
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("clears hover state on mouseout", () => {
|
|
116
|
-
const link = document.createElement("a");
|
|
117
|
-
container.appendChild(link);
|
|
118
|
-
|
|
119
|
-
dispatchMouseOver(link);
|
|
120
|
-
expect(app.state.isHover).toBe(true);
|
|
121
|
-
|
|
122
|
-
dispatchMouseOut(link);
|
|
123
|
-
expect(app.state.isHover).toBe(false);
|
|
124
|
-
expect(app.state.hoverTarget).toBeNull();
|
|
125
|
-
expect(app.state.interaction).toEqual({});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("sets isNative for input elements when ignoreOnNative is auto", () => {
|
|
129
|
-
const input = document.createElement("input");
|
|
130
|
-
container.appendChild(input);
|
|
131
|
-
|
|
132
|
-
dispatchMouseOver(input);
|
|
133
|
-
|
|
134
|
-
expect(app.state.isNative).toBe(true);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("sets isNative for elements with custom CSS cursor", () => {
|
|
138
|
-
const div = document.createElement("div");
|
|
139
|
-
div.style.cursor = "text"; // not in SUPERMOUSE_CURSORS
|
|
140
|
-
container.appendChild(div);
|
|
141
|
-
|
|
142
|
-
dispatchMouseOver(div);
|
|
143
|
-
|
|
144
|
-
expect(app.state.isNative).toBe(true);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("does not set isNative for elements with default cursor", () => {
|
|
148
|
-
const div = document.createElement("div");
|
|
149
|
-
// default cursor is auto/default, which is in SUPERMOUSE_CURSORS
|
|
150
|
-
container.appendChild(div);
|
|
151
|
-
|
|
152
|
-
dispatchMouseOver(div);
|
|
153
|
-
|
|
154
|
-
expect(app.state.isNative).toBe(false);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("sets isNative when element has ignore attribute", () => {
|
|
158
|
-
const div = document.createElement("div");
|
|
159
|
-
div.setAttribute("data-supermouse-ignore", "");
|
|
160
|
-
container.appendChild(div);
|
|
161
|
-
|
|
162
|
-
dispatchMouseOver(div);
|
|
163
|
-
|
|
164
|
-
expect(app.state.isNative).toBe(true);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("does not set isHover when element does not match hover selector", () => {
|
|
168
|
-
const span = document.createElement("span");
|
|
169
|
-
container.appendChild(span);
|
|
170
|
-
|
|
171
|
-
dispatchMouseOver(span);
|
|
172
|
-
|
|
173
|
-
expect(app.state.isHover).toBe(false);
|
|
174
|
-
expect(app.state.hoverTarget).toBeNull();
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("resets pointer to offscreen on document mouseleave when hideOnLeave is true", () => {
|
|
178
|
-
dispatchPointerMove(100, 100);
|
|
179
|
-
expect(app.state.pointer).toEqual({ x: 100, y: 100 });
|
|
180
|
-
|
|
181
|
-
const leaveEvent = new MouseEvent("mouseout", { relatedTarget: null });
|
|
182
|
-
document.dispatchEvent(leaveEvent);
|
|
183
|
-
|
|
184
|
-
expect(app.state.hasReceivedInput).toBe(false);
|
|
185
|
-
expect(app.state.pointer).toEqual({ x: -100, y: -100 });
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("does not reset pointer on mouseleave if hideOnLeave is false", () => {
|
|
189
|
-
app.destroy();
|
|
190
|
-
app = new Supermouse({ container, autoStart: false, hideOnLeave: false });
|
|
191
|
-
|
|
192
|
-
dispatchPointerMove(100, 100);
|
|
193
|
-
const leaveEvent = new MouseEvent("mouseout", { relatedTarget: null });
|
|
194
|
-
document.dispatchEvent(leaveEvent);
|
|
195
|
-
|
|
196
|
-
expect(app.state.hasReceivedInput).toBe(true);
|
|
197
|
-
expect(app.state.pointer).toEqual({ x: 100, y: 100 });
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("makes coordinates relative to container", () => {
|
|
201
|
-
const rect = {
|
|
202
|
-
left: 200,
|
|
203
|
-
top: 300,
|
|
204
|
-
right: 500,
|
|
205
|
-
bottom: 600,
|
|
206
|
-
width: 300,
|
|
207
|
-
height: 300,
|
|
208
|
-
x: 200,
|
|
209
|
-
y: 300,
|
|
210
|
-
toJSON: () => {}
|
|
211
|
-
};
|
|
212
|
-
vi.spyOn(container, "getBoundingClientRect").mockReturnValue(rect as DOMRect);
|
|
213
|
-
|
|
214
|
-
window.dispatchEvent(new Event("resize"));
|
|
215
|
-
|
|
216
|
-
dispatchPointerMove(250, 350);
|
|
217
|
-
|
|
218
|
-
expect(app.state.pointer).toEqual({ x: 50, y: 50 });
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("ignores all custom behavior inside data-supermouse-ignore area", () => {
|
|
222
|
-
app = new Supermouse({ container, autoStart: false });
|
|
223
|
-
|
|
224
|
-
// Create ignored wrapper
|
|
225
|
-
const ignored = document.createElement("div");
|
|
226
|
-
ignored.setAttribute("data-supermouse-ignore", "");
|
|
227
|
-
const innerButton = document.createElement("button");
|
|
228
|
-
innerButton.setAttribute("data-supermouse-text", "Hello");
|
|
229
|
-
ignored.appendChild(innerButton);
|
|
230
|
-
container.appendChild(ignored);
|
|
231
|
-
|
|
232
|
-
// Spy on parseDOMInteraction to ensure it's not called
|
|
233
|
-
const input = (app as any).input;
|
|
234
|
-
const parseSpy = vi.spyOn(input, "parseDOMInteraction");
|
|
235
|
-
|
|
236
|
-
// Hover the inner button (inside ignored area)
|
|
237
|
-
innerButton.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
|
238
|
-
|
|
239
|
-
// Expect no interaction parsing
|
|
240
|
-
expect(parseSpy).not.toHaveBeenCalled();
|
|
241
|
-
// Native cursor shown
|
|
242
|
-
expect(app.state.isNative).toBe(true);
|
|
243
|
-
// Hover state cleared
|
|
244
|
-
expect(app.state.isHover).toBe(false);
|
|
245
|
-
expect(app.state.hoverTarget).toBeNull();
|
|
246
|
-
// Interaction data empty
|
|
247
|
-
expect(app.state.interaction).toEqual({});
|
|
248
|
-
|
|
249
|
-
// Now move to a normal element outside ignored area
|
|
250
|
-
const normalButton = document.createElement("button");
|
|
251
|
-
container.appendChild(normalButton);
|
|
252
|
-
normalButton.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
|
253
|
-
|
|
254
|
-
// Normal behavior should resume
|
|
255
|
-
expect(app.state.isNative).toBe(false);
|
|
256
|
-
expect(app.state.isHover).toBe(true);
|
|
257
|
-
expect(app.state.hoverTarget).toBe(normalButton);
|
|
258
|
-
// Interaction parsing should happen again
|
|
259
|
-
expect(parseSpy).toHaveBeenCalled();
|
|
260
|
-
});
|
|
261
|
-
});
|