@simplysm/core-browser 13.0.69 → 13.0.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -224
- package/dist/extensions/element-ext.d.ts +36 -36
- package/dist/extensions/element-ext.d.ts.map +1 -1
- package/dist/extensions/element-ext.js +1 -1
- package/dist/extensions/element-ext.js.map +1 -1
- package/dist/extensions/html-element-ext.d.ts +22 -22
- package/dist/utils/download.d.ts +3 -3
- package/dist/utils/fetch.d.ts +1 -1
- package/dist/utils/file-dialog.d.ts +1 -1
- package/package.json +6 -5
- package/src/extensions/element-ext.ts +41 -41
- package/src/extensions/html-element-ext.ts +24 -24
- package/src/index.ts +1 -1
- package/src/utils/download.ts +3 -3
- package/src/utils/fetch.ts +4 -4
- package/src/utils/file-dialog.ts +1 -1
- package/tests/extensions/element-ext.spec.ts +737 -0
- package/tests/extensions/html-element-ext.spec.ts +190 -0
- package/tests/utils/download.spec.ts +66 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { ArgumentError } from "@simplysm/core-common";
|
|
3
|
+
import "../../src/extensions/html-element-ext";
|
|
4
|
+
|
|
5
|
+
describe("HTMLElement prototype extensions", () => {
|
|
6
|
+
let container: HTMLDivElement;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
container = document.createElement("div");
|
|
10
|
+
document.body.appendChild(container);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
container.remove();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("repaint", () => {
|
|
18
|
+
it("triggers reflow by accessing offsetHeight", () => {
|
|
19
|
+
const el = document.createElement("div");
|
|
20
|
+
const offsetHeightSpy = vi.spyOn(el, "offsetHeight", "get").mockReturnValue(100);
|
|
21
|
+
|
|
22
|
+
el.repaint();
|
|
23
|
+
|
|
24
|
+
expect(offsetHeightSpy).toHaveBeenCalled();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("getRelativeOffset", () => {
|
|
29
|
+
it("calculates relative position based on parent element", () => {
|
|
30
|
+
container.style.position = "relative";
|
|
31
|
+
container.innerHTML = `<div id="child" style="position: absolute; top: 50px; left: 30px;"></div>`;
|
|
32
|
+
|
|
33
|
+
const child = container.querySelector<HTMLElement>("#child")!;
|
|
34
|
+
|
|
35
|
+
const result = child.getRelativeOffset(container);
|
|
36
|
+
expect(result).toHaveProperty("top");
|
|
37
|
+
expect(result).toHaveProperty("left");
|
|
38
|
+
expect(typeof result.top).toBe("number");
|
|
39
|
+
expect(typeof result.left).toBe("number");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("finds parent by selector", () => {
|
|
43
|
+
container.id = "parent";
|
|
44
|
+
container.innerHTML = `<div><span id="deep-child"></span></div>`;
|
|
45
|
+
|
|
46
|
+
const deepChild = container.querySelector<HTMLElement>("#deep-child")!;
|
|
47
|
+
|
|
48
|
+
const result = deepChild.getRelativeOffset("#parent");
|
|
49
|
+
expect(result).toHaveProperty("top");
|
|
50
|
+
expect(result).toHaveProperty("left");
|
|
51
|
+
expect(typeof result.top).toBe("number");
|
|
52
|
+
expect(typeof result.left).toBe("number");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("throws error when parent is not found", () => {
|
|
56
|
+
const child = document.createElement("div");
|
|
57
|
+
container.appendChild(child);
|
|
58
|
+
|
|
59
|
+
expect(() => child.getRelativeOffset(".not-exist")).toThrow(ArgumentError);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("handles elements with transforms", () => {
|
|
63
|
+
container.style.position = "relative";
|
|
64
|
+
container.style.transform = "scale(2)";
|
|
65
|
+
container.innerHTML = `<div id="child" style="transform: rotate(45deg);"></div>`;
|
|
66
|
+
|
|
67
|
+
const child = container.querySelector<HTMLElement>("#child")!;
|
|
68
|
+
|
|
69
|
+
// Should return result without error even with transforms
|
|
70
|
+
const result = child.getRelativeOffset(container);
|
|
71
|
+
expect(result).toHaveProperty("top");
|
|
72
|
+
expect(result).toHaveProperty("left");
|
|
73
|
+
expect(typeof result.top).toBe("number");
|
|
74
|
+
expect(typeof result.left).toBe("number");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("accumulates border width of intermediate elements", () => {
|
|
78
|
+
container.style.position = "relative";
|
|
79
|
+
container.innerHTML = `
|
|
80
|
+
<div id="middle" style="border: 10px solid black;">
|
|
81
|
+
<div id="child"></div>
|
|
82
|
+
</div>
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const child = container.querySelector<HTMLElement>("#child")!;
|
|
86
|
+
const result = child.getRelativeOffset(container);
|
|
87
|
+
|
|
88
|
+
// borderTopWidth(10px) and borderLeftWidth(10px) should be reflected in result
|
|
89
|
+
expect(result.top).toBeGreaterThanOrEqual(10);
|
|
90
|
+
expect(result.left).toBeGreaterThanOrEqual(10);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("calculates correct position when parent element is scrolled", () => {
|
|
94
|
+
container.style.position = "relative";
|
|
95
|
+
container.style.overflow = "auto";
|
|
96
|
+
container.style.height = "100px";
|
|
97
|
+
container.style.width = "100px";
|
|
98
|
+
|
|
99
|
+
const inner = document.createElement("div");
|
|
100
|
+
inner.style.height = "500px";
|
|
101
|
+
inner.style.width = "500px";
|
|
102
|
+
inner.innerHTML = `<div id="child" style="position: absolute; top: 200px; left: 150px;"></div>`;
|
|
103
|
+
container.appendChild(inner);
|
|
104
|
+
|
|
105
|
+
// Scroll parent element
|
|
106
|
+
container.scrollTop = 50;
|
|
107
|
+
container.scrollLeft = 30;
|
|
108
|
+
|
|
109
|
+
const child = container.querySelector<HTMLElement>("#child")!;
|
|
110
|
+
const result = child.getRelativeOffset(container);
|
|
111
|
+
|
|
112
|
+
// scrollTop/scrollLeft are reflected in result (parentEl.scrollTop + parentEl.scrollLeft added)
|
|
113
|
+
// In test environment, getBoundingClientRect does not reflect scroll
|
|
114
|
+
// Verify that at least scrollTop/scrollLeft values are added
|
|
115
|
+
expect(result.top).toBeGreaterThanOrEqual(200);
|
|
116
|
+
expect(result.left).toBeGreaterThanOrEqual(150);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("scrollIntoViewIfNeeded", () => {
|
|
121
|
+
it("scrolls when target is above offset", () => {
|
|
122
|
+
container.style.overflow = "auto";
|
|
123
|
+
container.style.height = "100px";
|
|
124
|
+
// Add scrollable content
|
|
125
|
+
const inner = document.createElement("div");
|
|
126
|
+
inner.style.height = "500px";
|
|
127
|
+
container.appendChild(inner);
|
|
128
|
+
container.scrollTop = 100;
|
|
129
|
+
|
|
130
|
+
container.scrollIntoViewIfNeeded({ top: 50, left: 0 }, { top: 10, left: 0 });
|
|
131
|
+
|
|
132
|
+
expect(container.scrollTop).toBe(40);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("does not scroll if target is sufficiently visible", () => {
|
|
136
|
+
container.style.overflow = "auto";
|
|
137
|
+
container.style.height = "100px";
|
|
138
|
+
const inner = document.createElement("div");
|
|
139
|
+
inner.style.height = "500px";
|
|
140
|
+
container.appendChild(inner);
|
|
141
|
+
container.scrollTop = 0;
|
|
142
|
+
|
|
143
|
+
container.scrollIntoViewIfNeeded({ top: 50, left: 0 }, { top: 10, left: 0 });
|
|
144
|
+
|
|
145
|
+
expect(container.scrollTop).toBe(0);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("defaults to 0 offset", () => {
|
|
149
|
+
container.style.overflow = "auto";
|
|
150
|
+
container.style.height = "100px";
|
|
151
|
+
const inner = document.createElement("div");
|
|
152
|
+
inner.style.height = "500px";
|
|
153
|
+
container.appendChild(inner);
|
|
154
|
+
container.scrollTop = 100;
|
|
155
|
+
|
|
156
|
+
container.scrollIntoViewIfNeeded({ top: 50, left: 0 });
|
|
157
|
+
|
|
158
|
+
expect(container.scrollTop).toBe(50);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("scrolls horizontally when target is to the left of offset", () => {
|
|
162
|
+
container.style.overflow = "auto";
|
|
163
|
+
container.style.width = "100px";
|
|
164
|
+
// Add scrollable content
|
|
165
|
+
const inner = document.createElement("div");
|
|
166
|
+
inner.style.width = "500px";
|
|
167
|
+
inner.style.height = "10px";
|
|
168
|
+
container.appendChild(inner);
|
|
169
|
+
container.scrollLeft = 100;
|
|
170
|
+
|
|
171
|
+
container.scrollIntoViewIfNeeded({ top: 0, left: 50 }, { top: 0, left: 10 });
|
|
172
|
+
|
|
173
|
+
expect(container.scrollLeft).toBe(40);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("does not scroll horizontally if target is sufficiently visible", () => {
|
|
177
|
+
container.style.overflow = "auto";
|
|
178
|
+
container.style.width = "100px";
|
|
179
|
+
const inner = document.createElement("div");
|
|
180
|
+
inner.style.width = "500px";
|
|
181
|
+
inner.style.height = "10px";
|
|
182
|
+
container.appendChild(inner);
|
|
183
|
+
container.scrollLeft = 0;
|
|
184
|
+
|
|
185
|
+
container.scrollIntoViewIfNeeded({ top: 0, left: 50 }, { top: 0, left: 10 });
|
|
186
|
+
|
|
187
|
+
expect(container.scrollLeft).toBe(0);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { downloadBlob } from "../../src/utils/download";
|
|
3
|
+
|
|
4
|
+
describe("downloadBlob", () => {
|
|
5
|
+
let originalCreateObjectURL: typeof URL.createObjectURL;
|
|
6
|
+
let originalRevokeObjectURL: typeof URL.revokeObjectURL;
|
|
7
|
+
let mockLink: HTMLAnchorElement;
|
|
8
|
+
let clickSpy: ReturnType<typeof vi.fn>;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.useFakeTimers();
|
|
12
|
+
|
|
13
|
+
originalCreateObjectURL = URL.createObjectURL;
|
|
14
|
+
originalRevokeObjectURL = URL.revokeObjectURL;
|
|
15
|
+
URL.createObjectURL = vi.fn().mockReturnValue("blob:mock-url");
|
|
16
|
+
URL.revokeObjectURL = vi.fn();
|
|
17
|
+
|
|
18
|
+
clickSpy = vi.fn();
|
|
19
|
+
mockLink = {
|
|
20
|
+
href: "",
|
|
21
|
+
download: "",
|
|
22
|
+
click: clickSpy,
|
|
23
|
+
} as unknown as HTMLAnchorElement;
|
|
24
|
+
|
|
25
|
+
vi.spyOn(document, "createElement").mockReturnValue(mockLink);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.useRealTimers();
|
|
30
|
+
URL.createObjectURL = originalCreateObjectURL;
|
|
31
|
+
URL.revokeObjectURL = originalRevokeObjectURL;
|
|
32
|
+
vi.restoreAllMocks();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("converts Blob to download link and clicks it", () => {
|
|
36
|
+
const blob = new Blob(["test content"], { type: "text/plain" });
|
|
37
|
+
const fileName = "test.txt";
|
|
38
|
+
|
|
39
|
+
downloadBlob(blob, fileName);
|
|
40
|
+
|
|
41
|
+
expect(URL.createObjectURL).toHaveBeenCalledWith(blob);
|
|
42
|
+
expect(mockLink.href).toBe("blob:mock-url");
|
|
43
|
+
expect(mockLink.download).toBe(fileName);
|
|
44
|
+
expect(clickSpy).toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("calls URL.revokeObjectURL after download to prevent memory leak", () => {
|
|
48
|
+
const blob = new Blob(["test"], { type: "text/plain" });
|
|
49
|
+
|
|
50
|
+
downloadBlob(blob, "test.txt");
|
|
51
|
+
vi.runAllTimers();
|
|
52
|
+
|
|
53
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:mock-url");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("calls revokeObjectURL even when error occurs", () => {
|
|
57
|
+
const blob = new Blob(["test"], { type: "text/plain" });
|
|
58
|
+
clickSpy.mockImplementation(() => {
|
|
59
|
+
throw new Error("Click failed");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(() => downloadBlob(blob, "test.txt")).toThrow("Click failed");
|
|
63
|
+
vi.runAllTimers();
|
|
64
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:mock-url");
|
|
65
|
+
});
|
|
66
|
+
});
|