@simplysm/core-browser 13.0.0-beta.1
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/.cache/typecheck-browser.tsbuildinfo +1 -0
- package/.cache/typecheck-tests-browser.tsbuildinfo +1 -0
- package/README.md +221 -0
- package/dist/core-browser/src/extensions/element-ext.d.ts +98 -0
- package/dist/core-browser/src/extensions/element-ext.d.ts.map +1 -0
- package/dist/core-browser/src/extensions/html-element-ext.d.ts +54 -0
- package/dist/core-browser/src/extensions/html-element-ext.d.ts.map +1 -0
- package/dist/core-browser/src/index.d.ts +7 -0
- package/dist/core-browser/src/index.d.ts.map +1 -0
- package/dist/core-browser/src/utils/blob.d.ts +10 -0
- package/dist/core-browser/src/utils/blob.d.ts.map +1 -0
- package/dist/core-browser/src/utils/download.d.ts +11 -0
- package/dist/core-browser/src/utils/download.d.ts.map +1 -0
- package/dist/core-common/src/common.types.d.ts +74 -0
- package/dist/core-common/src/common.types.d.ts.map +1 -0
- package/dist/core-common/src/env.d.ts +6 -0
- package/dist/core-common/src/env.d.ts.map +1 -0
- package/dist/core-common/src/errors/argument-error.d.ts +25 -0
- package/dist/core-common/src/errors/argument-error.d.ts.map +1 -0
- package/dist/core-common/src/errors/not-implemented-error.d.ts +29 -0
- package/dist/core-common/src/errors/not-implemented-error.d.ts.map +1 -0
- package/dist/core-common/src/errors/sd-error.d.ts +27 -0
- package/dist/core-common/src/errors/sd-error.d.ts.map +1 -0
- package/dist/core-common/src/errors/timeout-error.d.ts +31 -0
- package/dist/core-common/src/errors/timeout-error.d.ts.map +1 -0
- package/dist/core-common/src/extensions/arr-ext.d.ts +15 -0
- package/dist/core-common/src/extensions/arr-ext.d.ts.map +1 -0
- package/dist/core-common/src/extensions/arr-ext.helpers.d.ts +19 -0
- package/dist/core-common/src/extensions/arr-ext.helpers.d.ts.map +1 -0
- package/dist/core-common/src/extensions/arr-ext.types.d.ts +215 -0
- package/dist/core-common/src/extensions/arr-ext.types.d.ts.map +1 -0
- package/dist/core-common/src/extensions/map-ext.d.ts +57 -0
- package/dist/core-common/src/extensions/map-ext.d.ts.map +1 -0
- package/dist/core-common/src/extensions/set-ext.d.ts +36 -0
- package/dist/core-common/src/extensions/set-ext.d.ts.map +1 -0
- package/dist/core-common/src/features/debounce-queue.d.ts +53 -0
- package/dist/core-common/src/features/debounce-queue.d.ts.map +1 -0
- package/dist/core-common/src/features/event-emitter.d.ts +66 -0
- package/dist/core-common/src/features/event-emitter.d.ts.map +1 -0
- package/dist/core-common/src/features/serial-queue.d.ts +47 -0
- package/dist/core-common/src/features/serial-queue.d.ts.map +1 -0
- package/dist/core-common/src/index.d.ts +32 -0
- package/dist/core-common/src/index.d.ts.map +1 -0
- package/dist/core-common/src/types/date-only.d.ts +152 -0
- package/dist/core-common/src/types/date-only.d.ts.map +1 -0
- package/dist/core-common/src/types/date-time.d.ts +96 -0
- package/dist/core-common/src/types/date-time.d.ts.map +1 -0
- package/dist/core-common/src/types/lazy-gc-map.d.ts +80 -0
- package/dist/core-common/src/types/lazy-gc-map.d.ts.map +1 -0
- package/dist/core-common/src/types/time.d.ts +68 -0
- package/dist/core-common/src/types/time.d.ts.map +1 -0
- package/dist/core-common/src/types/uuid.d.ts +35 -0
- package/dist/core-common/src/types/uuid.d.ts.map +1 -0
- package/dist/core-common/src/utils/bytes.d.ts +51 -0
- package/dist/core-common/src/utils/bytes.d.ts.map +1 -0
- package/dist/core-common/src/utils/date-format.d.ts +90 -0
- package/dist/core-common/src/utils/date-format.d.ts.map +1 -0
- package/dist/core-common/src/utils/json.d.ts +34 -0
- package/dist/core-common/src/utils/json.d.ts.map +1 -0
- package/dist/core-common/src/utils/num.d.ts +60 -0
- package/dist/core-common/src/utils/num.d.ts.map +1 -0
- package/dist/core-common/src/utils/obj.d.ts +258 -0
- package/dist/core-common/src/utils/obj.d.ts.map +1 -0
- package/dist/core-common/src/utils/path.d.ts +23 -0
- package/dist/core-common/src/utils/path.d.ts.map +1 -0
- package/dist/core-common/src/utils/primitive.d.ts +18 -0
- package/dist/core-common/src/utils/primitive.d.ts.map +1 -0
- package/dist/core-common/src/utils/str.d.ts +103 -0
- package/dist/core-common/src/utils/str.d.ts.map +1 -0
- package/dist/core-common/src/utils/template-strings.d.ts +84 -0
- package/dist/core-common/src/utils/template-strings.d.ts.map +1 -0
- package/dist/core-common/src/utils/transferable.d.ts +47 -0
- package/dist/core-common/src/utils/transferable.d.ts.map +1 -0
- package/dist/core-common/src/utils/wait.d.ts +19 -0
- package/dist/core-common/src/utils/wait.d.ts.map +1 -0
- package/dist/core-common/src/utils/xml.d.ts +36 -0
- package/dist/core-common/src/utils/xml.d.ts.map +1 -0
- package/dist/core-common/src/zip/sd-zip.d.ts +80 -0
- package/dist/core-common/src/zip/sd-zip.d.ts.map +1 -0
- package/dist/extensions/element-ext.js +122 -0
- package/dist/extensions/element-ext.js.map +7 -0
- package/dist/extensions/html-element-ext.js +50 -0
- package/dist/extensions/html-element-ext.js.map +7 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +7 -0
- package/dist/utils/blob.js +19 -0
- package/dist/utils/blob.js.map +7 -0
- package/dist/utils/download.js +47 -0
- package/dist/utils/download.js.map +7 -0
- package/package.json +26 -0
- package/src/extensions/element-ext.ts +246 -0
- package/src/extensions/html-element-ext.ts +117 -0
- package/src/index.ts +11 -0
- package/src/utils/blob.ts +19 -0
- package/src/utils/download.ts +66 -0
- package/tests/extensions/element-ext.spec.ts +729 -0
- package/tests/extensions/html-element-ext.spec.ts +190 -0
- package/tests/utils/blob.spec.ts +68 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { TimeoutError } from "@simplysm/core-common";
|
|
3
|
+
import "../../src/extensions/element-ext";
|
|
4
|
+
import { copyElement, pasteToElement, getBounds } from "../../src/extensions/element-ext";
|
|
5
|
+
|
|
6
|
+
describe("Element prototype extensions", () => {
|
|
7
|
+
let container: HTMLDivElement;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
container = document.createElement("div");
|
|
11
|
+
document.body.appendChild(container);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
container.remove();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("prependChild", () => {
|
|
19
|
+
it("요소를 첫 번째 자식으로 삽입", () => {
|
|
20
|
+
const existing = document.createElement("span");
|
|
21
|
+
existing.textContent = "existing";
|
|
22
|
+
container.appendChild(existing);
|
|
23
|
+
|
|
24
|
+
const newChild = document.createElement("div");
|
|
25
|
+
newChild.textContent = "new";
|
|
26
|
+
|
|
27
|
+
const result = container.prependChild(newChild);
|
|
28
|
+
|
|
29
|
+
expect(result).toBe(newChild);
|
|
30
|
+
expect(container.children[0]).toBe(newChild);
|
|
31
|
+
expect(container.children[1]).toBe(existing);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("빈 컨테이너에 삽입", () => {
|
|
35
|
+
const newChild = document.createElement("div");
|
|
36
|
+
container.prependChild(newChild);
|
|
37
|
+
|
|
38
|
+
expect(container.children[0]).toBe(newChild);
|
|
39
|
+
expect(container.children.length).toBe(1);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("findAll", () => {
|
|
44
|
+
it("셀렉터로 하위 요소 전체 검색", () => {
|
|
45
|
+
container.innerHTML = `
|
|
46
|
+
<div class="item">1</div>
|
|
47
|
+
<div class="item">2</div>
|
|
48
|
+
<span class="item">3</span>
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const result = container.findAll(".item");
|
|
52
|
+
|
|
53
|
+
expect(result.length).toBe(3);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("복합 셀렉터 지원", () => {
|
|
57
|
+
container.innerHTML = `
|
|
58
|
+
<div class="a">a</div>
|
|
59
|
+
<span class="b">b</span>
|
|
60
|
+
<p class="c">c</p>
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
const result = container.findAll(".a, .b");
|
|
64
|
+
|
|
65
|
+
expect(result.length).toBe(2);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("매칭 요소 없으면 빈 배열 반환", () => {
|
|
69
|
+
const result = container.findAll(".not-exist");
|
|
70
|
+
expect(result).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("빈 셀렉터는 빈 배열 반환", () => {
|
|
74
|
+
container.innerHTML = `<div class="item">1</div>`;
|
|
75
|
+
const result = container.findAll("");
|
|
76
|
+
expect(result).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("공백만 있는 셀렉터는 빈 배열 반환", () => {
|
|
80
|
+
container.innerHTML = `<div class="item">1</div>`;
|
|
81
|
+
const result = container.findAll(" ");
|
|
82
|
+
expect(result).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("속성 셀렉터 내 콤마가 포함된 경우 처리", () => {
|
|
86
|
+
container.innerHTML = `
|
|
87
|
+
<div data-values="a,b,c">1</div>
|
|
88
|
+
<div class="item">2</div>
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
// 속성 셀렉터에 콤마가 포함된 경우
|
|
92
|
+
// 현재 구현에서는 잘못 분할될 수 있으므로, 단일 셀렉터로만 사용 권장
|
|
93
|
+
const result = container.findAll('[data-values="a,b,c"]');
|
|
94
|
+
|
|
95
|
+
// 엣지 케이스 동작 확인
|
|
96
|
+
expect(result.length).toBeGreaterThanOrEqual(0);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("findFirst", () => {
|
|
101
|
+
it("첫 번째 매칭 요소 반환", () => {
|
|
102
|
+
container.innerHTML = `
|
|
103
|
+
<div class="item" id="first">1</div>
|
|
104
|
+
<div class="item" id="second">2</div>
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
const result = container.findFirst(".item");
|
|
108
|
+
|
|
109
|
+
expect(result?.id).toBe("first");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("매칭 요소 없으면 undefined 반환", () => {
|
|
113
|
+
const result = container.findFirst(".not-exist");
|
|
114
|
+
expect(result).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("빈 셀렉터는 undefined 반환", () => {
|
|
118
|
+
container.innerHTML = `<div class="item">1</div>`;
|
|
119
|
+
const result = container.findFirst("");
|
|
120
|
+
expect(result).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("공백만 있는 셀렉터는 undefined 반환", () => {
|
|
124
|
+
container.innerHTML = `<div class="item">1</div>`;
|
|
125
|
+
const result = container.findFirst(" ");
|
|
126
|
+
expect(result).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("getParents", () => {
|
|
131
|
+
it("모든 부모 요소 반환", () => {
|
|
132
|
+
container.innerHTML = `<div id="level1"><div id="level2"><span id="target"></span></div></div>`;
|
|
133
|
+
const target = container.querySelector("#target")!;
|
|
134
|
+
|
|
135
|
+
const parents = target.getParents();
|
|
136
|
+
|
|
137
|
+
expect(parents.length).toBeGreaterThanOrEqual(3);
|
|
138
|
+
expect(parents[0].id).toBe("level2");
|
|
139
|
+
expect(parents[1].id).toBe("level1");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("body까지 포함", () => {
|
|
143
|
+
const parents = container.getParents();
|
|
144
|
+
expect(parents).toContain(document.body);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("findFocusableParent", () => {
|
|
149
|
+
it("포커스 가능한 부모 요소 반환", () => {
|
|
150
|
+
container.innerHTML = `<button id="parent-btn"><span id="child">text</span></button>`;
|
|
151
|
+
const child = container.querySelector("#child")!;
|
|
152
|
+
|
|
153
|
+
const result = child.findFocusableParent();
|
|
154
|
+
|
|
155
|
+
expect(result?.id).toBe("parent-btn");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("포커스 가능한 부모 없으면 undefined", () => {
|
|
159
|
+
container.innerHTML = `<div><span id="child">text</span></div>`;
|
|
160
|
+
const child = container.querySelector("#child")!;
|
|
161
|
+
|
|
162
|
+
const result = child.findFocusableParent();
|
|
163
|
+
|
|
164
|
+
expect(result).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("tabindex 속성으로 포커스 가능해진 요소를 찾는다", () => {
|
|
168
|
+
const parent = document.createElement("div");
|
|
169
|
+
parent.setAttribute("tabindex", "0");
|
|
170
|
+
const child = document.createElement("span");
|
|
171
|
+
parent.appendChild(child);
|
|
172
|
+
document.body.appendChild(parent);
|
|
173
|
+
|
|
174
|
+
const result = child.findFocusableParent();
|
|
175
|
+
expect(result).toBe(parent);
|
|
176
|
+
|
|
177
|
+
document.body.removeChild(parent);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("isOffsetElement", () => {
|
|
182
|
+
it("position: relative는 offset 요소", () => {
|
|
183
|
+
container.style.position = "relative";
|
|
184
|
+
expect(container.isOffsetElement()).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("position: absolute는 offset 요소", () => {
|
|
188
|
+
container.style.position = "absolute";
|
|
189
|
+
expect(container.isOffsetElement()).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("position: fixed는 offset 요소", () => {
|
|
193
|
+
container.style.position = "fixed";
|
|
194
|
+
expect(container.isOffsetElement()).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("position: sticky는 offset 요소", () => {
|
|
198
|
+
container.style.position = "sticky";
|
|
199
|
+
expect(container.isOffsetElement()).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("position: static은 offset 요소 아님", () => {
|
|
203
|
+
container.style.position = "static";
|
|
204
|
+
expect(container.isOffsetElement()).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("isVisible", () => {
|
|
209
|
+
it("기본 요소는 visible", () => {
|
|
210
|
+
container.style.width = "100px";
|
|
211
|
+
container.style.height = "100px";
|
|
212
|
+
expect(container.isVisible()).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("visibility: hidden은 not visible", () => {
|
|
216
|
+
container.style.visibility = "hidden";
|
|
217
|
+
expect(container.isVisible()).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("opacity: 0은 not visible", () => {
|
|
221
|
+
container.style.opacity = "0";
|
|
222
|
+
expect(container.isVisible()).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("display: none은 not visible", () => {
|
|
226
|
+
container.style.display = "none";
|
|
227
|
+
expect(container.isVisible()).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("width가 0인 요소의 가시성 판단", () => {
|
|
231
|
+
container.style.width = "0";
|
|
232
|
+
container.style.height = "100px";
|
|
233
|
+
// getClientRects().length가 0이면 not visible
|
|
234
|
+
const isVisible = container.isVisible();
|
|
235
|
+
// 브라우저 환경에 따라 다를 수 있으므로 boolean 타입임을 확인
|
|
236
|
+
expect(typeof isVisible).toBe("boolean");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("height가 0인 요소의 가시성 판단", () => {
|
|
240
|
+
container.style.width = "100px";
|
|
241
|
+
container.style.height = "0";
|
|
242
|
+
// getClientRects().length가 0이면 not visible
|
|
243
|
+
const isVisible = container.isVisible();
|
|
244
|
+
// 브라우저 환경에 따라 다를 수 있으므로 boolean 타입임을 확인
|
|
245
|
+
expect(typeof isVisible).toBe("boolean");
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("copyElement", () => {
|
|
250
|
+
function createMockClipboardEvent(target: Element, data?: string): ClipboardEvent {
|
|
251
|
+
const clipboardData = {
|
|
252
|
+
setData: vi.fn(),
|
|
253
|
+
getData: vi.fn().mockReturnValue(data ?? ""),
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
target,
|
|
257
|
+
clipboardData,
|
|
258
|
+
preventDefault: vi.fn(),
|
|
259
|
+
} as unknown as ClipboardEvent;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
it("input 요소의 value 복사", () => {
|
|
263
|
+
container.innerHTML = `<input type="text" value="test value" />`;
|
|
264
|
+
const event = createMockClipboardEvent(container);
|
|
265
|
+
|
|
266
|
+
copyElement(event);
|
|
267
|
+
|
|
268
|
+
expect(event.clipboardData?.setData).toHaveBeenCalledWith("text/plain", "test value");
|
|
269
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("textarea 요소의 value 복사", () => {
|
|
273
|
+
container.innerHTML = `<textarea>textarea content</textarea>`;
|
|
274
|
+
const event = createMockClipboardEvent(container);
|
|
275
|
+
|
|
276
|
+
copyElement(event);
|
|
277
|
+
|
|
278
|
+
expect(event.clipboardData?.setData).toHaveBeenCalledWith("text/plain", "textarea content");
|
|
279
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("input/textarea 없으면 기본 동작 유지", () => {
|
|
283
|
+
container.innerHTML = `<span>content</span>`;
|
|
284
|
+
const event = createMockClipboardEvent(container);
|
|
285
|
+
|
|
286
|
+
copyElement(event);
|
|
287
|
+
|
|
288
|
+
expect(event.clipboardData?.setData).not.toHaveBeenCalled();
|
|
289
|
+
expect(event.preventDefault).not.toHaveBeenCalled();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("clipboardData가 null이면 아무것도 안함", () => {
|
|
293
|
+
const event = { target: container, clipboardData: null, preventDefault: vi.fn() } as unknown as ClipboardEvent;
|
|
294
|
+
|
|
295
|
+
copyElement(event);
|
|
296
|
+
|
|
297
|
+
expect(event.preventDefault).not.toHaveBeenCalled();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("target이 Element가 아니면 아무것도 안함", () => {
|
|
301
|
+
const event = {
|
|
302
|
+
target: document,
|
|
303
|
+
clipboardData: { setData: vi.fn(), getData: vi.fn() },
|
|
304
|
+
preventDefault: vi.fn(),
|
|
305
|
+
} as unknown as ClipboardEvent;
|
|
306
|
+
|
|
307
|
+
copyElement(event);
|
|
308
|
+
|
|
309
|
+
expect(event.preventDefault).not.toHaveBeenCalled();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("여러 input 중 첫 번째 input의 value 복사", () => {
|
|
313
|
+
container.innerHTML = `
|
|
314
|
+
<input type="text" value="first" />
|
|
315
|
+
<input type="text" value="second" />
|
|
316
|
+
`;
|
|
317
|
+
const clipboardData = {
|
|
318
|
+
setData: vi.fn(),
|
|
319
|
+
getData: vi.fn().mockReturnValue(""),
|
|
320
|
+
};
|
|
321
|
+
const event = {
|
|
322
|
+
target: container,
|
|
323
|
+
clipboardData,
|
|
324
|
+
preventDefault: vi.fn(),
|
|
325
|
+
} as unknown as ClipboardEvent;
|
|
326
|
+
|
|
327
|
+
copyElement(event);
|
|
328
|
+
|
|
329
|
+
expect(clipboardData.setData).toHaveBeenCalledWith("text/plain", "first");
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe("pasteToElement", () => {
|
|
334
|
+
function createMockClipboardEvent(target: Element, data?: string): ClipboardEvent {
|
|
335
|
+
const clipboardData = {
|
|
336
|
+
setData: vi.fn(),
|
|
337
|
+
getData: vi.fn().mockReturnValue(data ?? ""),
|
|
338
|
+
};
|
|
339
|
+
return {
|
|
340
|
+
target,
|
|
341
|
+
clipboardData,
|
|
342
|
+
preventDefault: vi.fn(),
|
|
343
|
+
} as unknown as ClipboardEvent;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
it("클립보드 내용을 input에 붙여넣기", () => {
|
|
347
|
+
container.innerHTML = `<input type="text" />`;
|
|
348
|
+
const input = container.querySelector("input")!;
|
|
349
|
+
const event = createMockClipboardEvent(container, "pasted text");
|
|
350
|
+
|
|
351
|
+
pasteToElement(event);
|
|
352
|
+
|
|
353
|
+
expect(input.value).toBe("pasted text");
|
|
354
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("붙여넣기 시 input 이벤트 발생", () => {
|
|
358
|
+
container.innerHTML = `<input type="text" />`;
|
|
359
|
+
const input = container.querySelector("input")!;
|
|
360
|
+
const inputEventSpy = vi.fn();
|
|
361
|
+
input.addEventListener("input", inputEventSpy);
|
|
362
|
+
|
|
363
|
+
const event = createMockClipboardEvent(container, "pasted text");
|
|
364
|
+
pasteToElement(event);
|
|
365
|
+
|
|
366
|
+
expect(inputEventSpy).toHaveBeenCalledTimes(1);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("클립보드 내용을 textarea에 붙여넣기", () => {
|
|
370
|
+
container.innerHTML = `<textarea></textarea>`;
|
|
371
|
+
const textarea = container.querySelector("textarea")!;
|
|
372
|
+
const event = createMockClipboardEvent(container, "pasted text");
|
|
373
|
+
|
|
374
|
+
pasteToElement(event);
|
|
375
|
+
|
|
376
|
+
expect(textarea.value).toBe("pasted text");
|
|
377
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("input/textarea 없으면 아무것도 안함", () => {
|
|
381
|
+
container.innerHTML = `<div>no input</div>`;
|
|
382
|
+
const event = createMockClipboardEvent(container, "pasted text");
|
|
383
|
+
|
|
384
|
+
pasteToElement(event);
|
|
385
|
+
|
|
386
|
+
expect(event.preventDefault).not.toHaveBeenCalled();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("clipboardData가 null이면 아무것도 안함", () => {
|
|
390
|
+
container.innerHTML = `<input type="text" />`;
|
|
391
|
+
const event = { target: container, clipboardData: null, preventDefault: vi.fn() } as unknown as ClipboardEvent;
|
|
392
|
+
|
|
393
|
+
pasteToElement(event);
|
|
394
|
+
|
|
395
|
+
expect(event.preventDefault).not.toHaveBeenCalled();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("target이 Element가 아니면 아무것도 안함", () => {
|
|
399
|
+
const event = {
|
|
400
|
+
target: document,
|
|
401
|
+
clipboardData: { setData: vi.fn(), getData: vi.fn().mockReturnValue("text") },
|
|
402
|
+
preventDefault: vi.fn(),
|
|
403
|
+
} as unknown as ClipboardEvent;
|
|
404
|
+
|
|
405
|
+
pasteToElement(event);
|
|
406
|
+
|
|
407
|
+
expect(event.preventDefault).not.toHaveBeenCalled();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("여러 input 중 첫 번째 input에 붙여넣기", () => {
|
|
411
|
+
container.innerHTML = `
|
|
412
|
+
<input type="text" value="existing1" />
|
|
413
|
+
<input type="text" value="existing2" />
|
|
414
|
+
`;
|
|
415
|
+
const inputs = container.querySelectorAll("input");
|
|
416
|
+
const clipboardData = {
|
|
417
|
+
setData: vi.fn(),
|
|
418
|
+
getData: vi.fn().mockReturnValue("pasted"),
|
|
419
|
+
};
|
|
420
|
+
const event = {
|
|
421
|
+
target: container,
|
|
422
|
+
clipboardData,
|
|
423
|
+
preventDefault: vi.fn(),
|
|
424
|
+
} as unknown as ClipboardEvent;
|
|
425
|
+
|
|
426
|
+
pasteToElement(event);
|
|
427
|
+
|
|
428
|
+
expect(inputs[0].value).toBe("pasted");
|
|
429
|
+
expect(inputs[1].value).toBe("existing2");
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
describe("getBounds", () => {
|
|
434
|
+
it("빈 배열 전달 시 즉시 빈 배열 반환", async () => {
|
|
435
|
+
const result = await getBounds([]);
|
|
436
|
+
expect(result).toEqual([]);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("IntersectionObserver로 bounds 조회", async () => {
|
|
440
|
+
const mockObserver = {
|
|
441
|
+
observe: vi.fn(),
|
|
442
|
+
disconnect: vi.fn(),
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const MockIntersectionObserver = vi.fn(function (
|
|
446
|
+
this: IntersectionObserver,
|
|
447
|
+
callback: IntersectionObserverCallback,
|
|
448
|
+
) {
|
|
449
|
+
setTimeout(() => {
|
|
450
|
+
callback(
|
|
451
|
+
[
|
|
452
|
+
{
|
|
453
|
+
target: container,
|
|
454
|
+
boundingClientRect: {
|
|
455
|
+
top: 10,
|
|
456
|
+
left: 20,
|
|
457
|
+
width: 100,
|
|
458
|
+
height: 50,
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
] as unknown as IntersectionObserverEntry[],
|
|
462
|
+
this,
|
|
463
|
+
);
|
|
464
|
+
}, 0);
|
|
465
|
+
return mockObserver;
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
|
|
469
|
+
|
|
470
|
+
const result = await getBounds([container]);
|
|
471
|
+
|
|
472
|
+
expect(result.length).toBe(1);
|
|
473
|
+
expect(result[0].target).toBe(container);
|
|
474
|
+
expect(result[0].top).toBe(10);
|
|
475
|
+
expect(result[0].left).toBe(20);
|
|
476
|
+
expect(result[0].width).toBe(100);
|
|
477
|
+
expect(result[0].height).toBe(50);
|
|
478
|
+
|
|
479
|
+
expect(mockObserver.observe).toHaveBeenCalledWith(container);
|
|
480
|
+
expect(mockObserver.disconnect).toHaveBeenCalled();
|
|
481
|
+
|
|
482
|
+
vi.unstubAllGlobals();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("여러 요소 동시 조회", async () => {
|
|
486
|
+
const el1 = document.createElement("div");
|
|
487
|
+
const el2 = document.createElement("div");
|
|
488
|
+
|
|
489
|
+
const mockObserver = {
|
|
490
|
+
observe: vi.fn(),
|
|
491
|
+
disconnect: vi.fn(),
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const MockIntersectionObserver = vi.fn(function (
|
|
495
|
+
this: IntersectionObserver,
|
|
496
|
+
callback: IntersectionObserverCallback,
|
|
497
|
+
) {
|
|
498
|
+
setTimeout(() => {
|
|
499
|
+
callback(
|
|
500
|
+
[
|
|
501
|
+
{
|
|
502
|
+
target: el1,
|
|
503
|
+
boundingClientRect: { top: 0, left: 0, width: 10, height: 10 },
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
target: el2,
|
|
507
|
+
boundingClientRect: { top: 20, left: 20, width: 20, height: 20 },
|
|
508
|
+
},
|
|
509
|
+
] as unknown as IntersectionObserverEntry[],
|
|
510
|
+
this,
|
|
511
|
+
);
|
|
512
|
+
}, 0);
|
|
513
|
+
return mockObserver;
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
|
|
517
|
+
|
|
518
|
+
const result = await getBounds([el1, el2]);
|
|
519
|
+
|
|
520
|
+
expect(result.length).toBe(2);
|
|
521
|
+
expect(mockObserver.observe).toHaveBeenCalledTimes(2);
|
|
522
|
+
|
|
523
|
+
vi.unstubAllGlobals();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("중복 요소는 한 번만 처리", async () => {
|
|
527
|
+
const mockObserver = {
|
|
528
|
+
observe: vi.fn(),
|
|
529
|
+
disconnect: vi.fn(),
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const MockIntersectionObserver = vi.fn(function (
|
|
533
|
+
this: IntersectionObserver,
|
|
534
|
+
callback: IntersectionObserverCallback,
|
|
535
|
+
) {
|
|
536
|
+
setTimeout(() => {
|
|
537
|
+
callback(
|
|
538
|
+
[
|
|
539
|
+
{
|
|
540
|
+
target: container,
|
|
541
|
+
boundingClientRect: { top: 10, left: 20, width: 100, height: 50 },
|
|
542
|
+
},
|
|
543
|
+
] as unknown as IntersectionObserverEntry[],
|
|
544
|
+
this,
|
|
545
|
+
);
|
|
546
|
+
}, 0);
|
|
547
|
+
return mockObserver;
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
|
|
551
|
+
|
|
552
|
+
// 같은 요소를 3번 전달
|
|
553
|
+
const result = await getBounds([container, container, container]);
|
|
554
|
+
|
|
555
|
+
// 결과는 1개만 반환
|
|
556
|
+
expect(result.length).toBe(1);
|
|
557
|
+
expect(result[0].target).toBe(container);
|
|
558
|
+
// observe도 1번만 호출
|
|
559
|
+
expect(mockObserver.observe).toHaveBeenCalledTimes(1);
|
|
560
|
+
|
|
561
|
+
vi.unstubAllGlobals();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("결과가 입력 순서대로 정렬됨", async () => {
|
|
565
|
+
const el1 = document.createElement("div");
|
|
566
|
+
const el2 = document.createElement("div");
|
|
567
|
+
const el3 = document.createElement("div");
|
|
568
|
+
|
|
569
|
+
const mockObserver = {
|
|
570
|
+
observe: vi.fn(),
|
|
571
|
+
disconnect: vi.fn(),
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const MockIntersectionObserver = vi.fn(function (
|
|
575
|
+
this: IntersectionObserver,
|
|
576
|
+
callback: IntersectionObserverCallback,
|
|
577
|
+
) {
|
|
578
|
+
setTimeout(() => {
|
|
579
|
+
// 콜백은 역순으로 호출 (el3, el2, el1)
|
|
580
|
+
callback(
|
|
581
|
+
[
|
|
582
|
+
{
|
|
583
|
+
target: el3,
|
|
584
|
+
boundingClientRect: { top: 30, left: 30, width: 30, height: 30 },
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
target: el2,
|
|
588
|
+
boundingClientRect: { top: 20, left: 20, width: 20, height: 20 },
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
target: el1,
|
|
592
|
+
boundingClientRect: { top: 10, left: 10, width: 10, height: 10 },
|
|
593
|
+
},
|
|
594
|
+
] as unknown as IntersectionObserverEntry[],
|
|
595
|
+
this,
|
|
596
|
+
);
|
|
597
|
+
}, 0);
|
|
598
|
+
return mockObserver;
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
|
|
602
|
+
|
|
603
|
+
// 입력 순서: el1, el2, el3
|
|
604
|
+
const result = await getBounds([el1, el2, el3]);
|
|
605
|
+
|
|
606
|
+
// 결과도 입력 순서대로: el1, el2, el3
|
|
607
|
+
expect(result.length).toBe(3);
|
|
608
|
+
expect(result[0].target).toBe(el1);
|
|
609
|
+
expect(result[1].target).toBe(el2);
|
|
610
|
+
expect(result[2].target).toBe(el3);
|
|
611
|
+
|
|
612
|
+
vi.unstubAllGlobals();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("타임아웃 시 TimeoutError 발생", async () => {
|
|
616
|
+
const mockObserver = {
|
|
617
|
+
observe: vi.fn(),
|
|
618
|
+
disconnect: vi.fn(),
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// 콜백을 호출하지 않는 Mock (타임아웃 유도)
|
|
622
|
+
const MockIntersectionObserver = vi.fn(function () {
|
|
623
|
+
return mockObserver;
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
|
|
627
|
+
|
|
628
|
+
// 50ms 타임아웃으로 테스트
|
|
629
|
+
await expect(getBounds([container], 50)).rejects.toThrow(TimeoutError);
|
|
630
|
+
|
|
631
|
+
vi.unstubAllGlobals();
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it("커스텀 타임아웃 설정", async () => {
|
|
635
|
+
const mockObserver = {
|
|
636
|
+
observe: vi.fn(),
|
|
637
|
+
disconnect: vi.fn(),
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const MockIntersectionObserver = vi.fn(function (
|
|
641
|
+
this: IntersectionObserver,
|
|
642
|
+
callback: IntersectionObserverCallback,
|
|
643
|
+
) {
|
|
644
|
+
// 100ms 후에 응답
|
|
645
|
+
setTimeout(() => {
|
|
646
|
+
callback(
|
|
647
|
+
[
|
|
648
|
+
{
|
|
649
|
+
target: container,
|
|
650
|
+
boundingClientRect: { top: 0, left: 0, width: 10, height: 10 },
|
|
651
|
+
},
|
|
652
|
+
] as unknown as IntersectionObserverEntry[],
|
|
653
|
+
this,
|
|
654
|
+
);
|
|
655
|
+
}, 100);
|
|
656
|
+
return mockObserver;
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
|
|
660
|
+
|
|
661
|
+
// 200ms 타임아웃이면 성공해야 함
|
|
662
|
+
const result = await getBounds([container], 200);
|
|
663
|
+
expect(result.length).toBe(1);
|
|
664
|
+
|
|
665
|
+
vi.unstubAllGlobals();
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it("콜백이 여러 번 분할 호출되어도 모든 결과 수집", async () => {
|
|
669
|
+
const el1 = document.createElement("div");
|
|
670
|
+
const el2 = document.createElement("div");
|
|
671
|
+
const el3 = document.createElement("div");
|
|
672
|
+
|
|
673
|
+
const mockObserver = {
|
|
674
|
+
observe: vi.fn(),
|
|
675
|
+
disconnect: vi.fn(),
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
const MockIntersectionObserver = vi.fn(function (
|
|
679
|
+
this: IntersectionObserver,
|
|
680
|
+
callback: IntersectionObserverCallback,
|
|
681
|
+
) {
|
|
682
|
+
// 첫 번째 콜백 - el1만
|
|
683
|
+
setTimeout(() => {
|
|
684
|
+
callback(
|
|
685
|
+
[
|
|
686
|
+
{
|
|
687
|
+
target: el1,
|
|
688
|
+
boundingClientRect: { top: 10, left: 10, width: 10, height: 10 },
|
|
689
|
+
},
|
|
690
|
+
] as unknown as IntersectionObserverEntry[],
|
|
691
|
+
this,
|
|
692
|
+
);
|
|
693
|
+
}, 0);
|
|
694
|
+
|
|
695
|
+
// 두 번째 콜백 - el2, el3
|
|
696
|
+
setTimeout(() => {
|
|
697
|
+
callback(
|
|
698
|
+
[
|
|
699
|
+
{
|
|
700
|
+
target: el2,
|
|
701
|
+
boundingClientRect: { top: 20, left: 20, width: 20, height: 20 },
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
target: el3,
|
|
705
|
+
boundingClientRect: { top: 30, left: 30, width: 30, height: 30 },
|
|
706
|
+
},
|
|
707
|
+
] as unknown as IntersectionObserverEntry[],
|
|
708
|
+
this,
|
|
709
|
+
);
|
|
710
|
+
}, 10);
|
|
711
|
+
|
|
712
|
+
return mockObserver;
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
|
|
716
|
+
|
|
717
|
+
const result = await getBounds([el1, el2, el3]);
|
|
718
|
+
|
|
719
|
+
// 모든 요소가 수집되어야 함
|
|
720
|
+
expect(result.length).toBe(3);
|
|
721
|
+
// 입력 순서대로 정렬되어야 함
|
|
722
|
+
expect(result[0].target).toBe(el1);
|
|
723
|
+
expect(result[1].target).toBe(el2);
|
|
724
|
+
expect(result[2].target).toBe(el3);
|
|
725
|
+
|
|
726
|
+
vi.unstubAllGlobals();
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
});
|