@vanilla-bean/components 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/Component/Component.js +598 -0
  2. package/Component/Component.scenarios.js +88 -0
  3. package/Component/Component.test.js +717 -0
  4. package/Component/README.md +455 -0
  5. package/Component/index.js +3 -0
  6. package/Component/observeElementConnection.js +52 -0
  7. package/Component/observeElementConnection.test.js +121 -0
  8. package/Elem/Elem.js +304 -0
  9. package/Elem/Elem.test.js +679 -0
  10. package/Elem/README.md +373 -0
  11. package/Elem/index.js +1 -0
  12. package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
  13. package/LICENSE +21 -0
  14. package/README.md +413 -0
  15. package/components/BottomSheet/BottomSheet.js +192 -0
  16. package/components/BottomSheet/BottomSheet.lld.md +25 -0
  17. package/components/BottomSheet/README.md +66 -0
  18. package/components/BottomSheet/index.js +1 -0
  19. package/components/Button/Button.js +53 -0
  20. package/components/Button/Button.lld.md +21 -0
  21. package/components/Button/index.js +1 -0
  22. package/components/Calendar/Calendar.js +720 -0
  23. package/components/Calendar/Calendar.lld.md +22 -0
  24. package/components/Calendar/CalendarEvent.js +102 -0
  25. package/components/Calendar/Toolbar.js +78 -0
  26. package/components/Calendar/index.js +2 -0
  27. package/components/Calendar/utils.js +56 -0
  28. package/components/Code/Code.js +84 -0
  29. package/components/Code/Code.lld.md +21 -0
  30. package/components/Code/index.js +1 -0
  31. package/components/ColorPicker/ColorPicker.js +445 -0
  32. package/components/ColorPicker/ColorPicker.lld.md +21 -0
  33. package/components/ColorPicker/index.js +1 -0
  34. package/components/ColorPicker/svg.js +5 -0
  35. package/components/Dialog/Dialog.js +278 -0
  36. package/components/Dialog/Dialog.lld.md +20 -0
  37. package/components/Dialog/README.md +96 -0
  38. package/components/Dialog/index.js +1 -0
  39. package/components/Form/Form.js +257 -0
  40. package/components/Form/Form.lld.md +21 -0
  41. package/components/Form/README.md +87 -0
  42. package/components/Form/index.js +1 -0
  43. package/components/Icon/Icon.js +54 -0
  44. package/components/Icon/Icon.lld.md +21 -0
  45. package/components/Icon/index.js +1 -0
  46. package/components/Input/Input.js +173 -0
  47. package/components/Input/Input.lld.md +28 -0
  48. package/components/Input/README.md +97 -0
  49. package/components/Input/index.js +2 -0
  50. package/components/Input/utils.js +122 -0
  51. package/components/Keyboard/Key.js +38 -0
  52. package/components/Keyboard/Keyboard.js +173 -0
  53. package/components/Keyboard/Keyboard.lld.md +21 -0
  54. package/components/Keyboard/index.js +1 -0
  55. package/components/Label/Label.js +214 -0
  56. package/components/Label/Label.lld.md +20 -0
  57. package/components/Label/index.js +1 -0
  58. package/components/Link/Link.js +43 -0
  59. package/components/Link/Link.lld.md +15 -0
  60. package/components/Link/index.js +1 -0
  61. package/components/List/List.js +82 -0
  62. package/components/List/List.lld.md +19 -0
  63. package/components/List/index.js +1 -0
  64. package/components/Menu/Menu.js +93 -0
  65. package/components/Menu/Menu.lld.md +15 -0
  66. package/components/Menu/index.js +1 -0
  67. package/components/Notify/Notify.js +96 -0
  68. package/components/Notify/Notify.lld.md +20 -0
  69. package/components/Notify/index.js +1 -0
  70. package/components/Page/Page.js +67 -0
  71. package/components/Page/Page.lld.md +20 -0
  72. package/components/Page/index.js +1 -0
  73. package/components/Popover/Popover.js +175 -0
  74. package/components/Popover/Popover.lld.md +19 -0
  75. package/components/Popover/index.js +1 -0
  76. package/components/RadioButton/RadioButton.js +108 -0
  77. package/components/RadioButton/RadioButton.lld.md +15 -0
  78. package/components/RadioButton/index.js +1 -0
  79. package/components/Router/README.md +160 -0
  80. package/components/Router/Router.js +150 -0
  81. package/components/Router/Router.lld.md +31 -0
  82. package/components/Router/View.js +15 -0
  83. package/components/Router/index.js +2 -0
  84. package/components/Router/utils.js +17 -0
  85. package/components/Select/README.md +88 -0
  86. package/components/Select/Select.js +74 -0
  87. package/components/Select/Select.lld.md +20 -0
  88. package/components/Select/index.js +1 -0
  89. package/components/Table/README.md +94 -0
  90. package/components/Table/Table.js +171 -0
  91. package/components/Table/Table.lld.md +21 -0
  92. package/components/Table/index.js +1 -0
  93. package/components/TagList/Tag.js +84 -0
  94. package/components/TagList/TagList.js +118 -0
  95. package/components/TagList/TagList.lld.md +30 -0
  96. package/components/TagList/design.excalidraw.png +0 -0
  97. package/components/TagList/index.js +2 -0
  98. package/components/Tooltip/Tooltip.js +139 -0
  99. package/components/Tooltip/Tooltip.lld.md +22 -0
  100. package/components/Tooltip/index.js +1 -0
  101. package/components/TooltipWrapper/TooltipWrapper.js +89 -0
  102. package/components/TooltipWrapper/TooltipWrapper.lld.md +21 -0
  103. package/components/TooltipWrapper/index.js +1 -0
  104. package/components/Whiteboard/Whiteboard.js +198 -0
  105. package/components/Whiteboard/Whiteboard.lld.md +35 -0
  106. package/components/Whiteboard/index.js +1 -0
  107. package/components/index.js +27 -0
  108. package/eslint.config.cjs +118 -0
  109. package/index.d.ts +635 -0
  110. package/index.js +19 -0
  111. package/package.json +123 -0
  112. package/plugins/asText.js +38 -0
  113. package/plugins/loadPlugins.js +5 -0
  114. package/plugins/markdownLoader.js +121 -0
  115. package/prettier.config.cjs +7 -0
  116. package/spellcheck.config.cjs +227 -0
  117. package/styled/README.md +329 -0
  118. package/styled/appendStyles.js +26 -0
  119. package/styled/appendStyles.test.js +45 -0
  120. package/styled/index.js +4 -0
  121. package/styled/shimCSS.js +31 -0
  122. package/styled/shimCSS.test.js +103 -0
  123. package/styled/styled.js +91 -0
  124. package/styled/styled.test.js +586 -0
  125. package/styled/themeStyles.js +36 -0
  126. package/styled/themeStyles.test.js +135 -0
  127. package/test-setup.js +123 -0
  128. package/theme/.test.js +69 -0
  129. package/theme/README.md +607 -0
  130. package/theme/button.js +100 -0
  131. package/theme/code.js +123 -0
  132. package/theme/colors.js +42 -0
  133. package/theme/fonts.js +42 -0
  134. package/theme/index.js +33 -0
  135. package/theme/input.js +64 -0
  136. package/theme/page.js +208 -0
  137. package/theme/scrollbar.js +24 -0
  138. package/theme/table.js +53 -0
  139. package/utils/README.md +176 -0
  140. package/utils/browser.js +92 -0
  141. package/utils/class.js +30 -0
  142. package/utils/color.js +81 -0
  143. package/utils/data.js +164 -0
  144. package/utils/element.js +55 -0
  145. package/utils/index.js +7 -0
  146. package/utils/rand.js +12 -0
  147. package/utils/string.js +72 -0
@@ -0,0 +1,679 @@
1
+ import { screen, queryByText, findByRole, within } from '@testing-library/dom';
2
+ import userEvent from '@testing-library/user-event';
3
+
4
+ import Elem from './Elem';
5
+
6
+ const user = userEvent.setup();
7
+
8
+ describe('Elem', () => {
9
+ let elem;
10
+
11
+ afterEach(() => {
12
+ elem = null;
13
+ document.body.replaceChildren();
14
+ });
15
+
16
+ describe('construction', () => {
17
+ test('creates default div element', () => {
18
+ elem = new Elem();
19
+ expect(elem.tag).toBe('div');
20
+ expect(elem.elem.tagName).toBe('DIV');
21
+ expect(elem.elem._elem).toBe(elem);
22
+ });
23
+
24
+ test('creates element with specified tag', () => {
25
+ elem = new Elem({ tag: 'button' });
26
+ expect(elem.tag).toBe('button');
27
+ expect(elem.elem.tagName).toBe('BUTTON');
28
+ });
29
+
30
+ test('applies basic properties during construction', () => {
31
+ elem = new Elem({
32
+ tag: 'input',
33
+ id: 'test-input',
34
+ className: 'form-control',
35
+ disabled: true,
36
+ });
37
+ document.body.appendChild(elem.elem);
38
+
39
+ const input = screen.getByRole('textbox');
40
+ expect(input).toHaveAttribute('id', 'test-input');
41
+ expect(input).toHaveClass('form-control');
42
+ expect(input).toBeDisabled();
43
+ });
44
+
45
+ test('applies text content during construction', () => {
46
+ elem = new Elem({
47
+ tag: 'button',
48
+ textContent: 'Click Me',
49
+ });
50
+ document.body.appendChild(elem.elem);
51
+
52
+ expect(screen.getByRole('button')).toHaveTextContent('Click Me');
53
+ });
54
+
55
+ test('handles children in constructor', () => {
56
+ const child1 = new Elem({ tag: 'span', textContent: 'Child 1' });
57
+ const child2 = new Elem({ tag: 'span', textContent: 'Child 2' });
58
+
59
+ elem = new Elem(
60
+ {
61
+ tag: 'div',
62
+ attributes: { 'data-testid': 'parent' },
63
+ },
64
+ child1,
65
+ child2,
66
+ );
67
+ document.body.appendChild(elem.elem);
68
+
69
+ expect(screen.getByText('Child 1')).toBeInTheDocument();
70
+ expect(screen.getByText('Child 2')).toBeInTheDocument();
71
+
72
+ const parent = screen.getByTestId('parent');
73
+ const spans = within(parent).getAllByRole('generic');
74
+ expect(spans[0]).toHaveTextContent('Child 1');
75
+ expect(spans[1]).toHaveTextContent('Child 2');
76
+ });
77
+
78
+ test('combines append option with constructor children', () => {
79
+ const child1 = new Elem({ tag: 'span', textContent: 'Option child' });
80
+ const child2 = new Elem({ tag: 'span', textContent: 'Constructor child' });
81
+
82
+ elem = new Elem(
83
+ {
84
+ tag: 'div',
85
+ attributes: { 'data-testid': 'parent' },
86
+ append: [child1],
87
+ },
88
+ child2,
89
+ );
90
+ document.body.appendChild(elem.elem);
91
+
92
+ expect(screen.getByText('Option child')).toBeInTheDocument();
93
+ expect(screen.getByText('Constructor child')).toBeInTheDocument();
94
+
95
+ const parent = screen.getByTestId('parent');
96
+ const spans = within(parent).getAllByRole('generic');
97
+ expect(spans[0]).toHaveTextContent('Option child');
98
+ expect(spans[1]).toHaveTextContent('Constructor child');
99
+ });
100
+ });
101
+
102
+ describe('string representation', () => {
103
+ test('returns correct string representation', () => {
104
+ elem = new Elem();
105
+ expect(elem.toString()).toBe('[object Elem]');
106
+ });
107
+ });
108
+
109
+ describe('hierarchy properties', () => {
110
+ test('provides access to parent elements', () => {
111
+ const parent = new Elem({ tag: 'div' });
112
+ elem = new Elem({ tag: 'span' });
113
+
114
+ parent.append(elem);
115
+
116
+ expect(elem.parentElem).toBe(parent.elem);
117
+ expect(elem.parent).toBe(parent);
118
+ });
119
+
120
+ test('returns null for orphaned elements', () => {
121
+ elem = new Elem();
122
+ expect(elem.parentElem).toBe(null);
123
+ expect(elem.parent).toBe(undefined);
124
+ });
125
+
126
+ test('provides access to child Elem instances', () => {
127
+ elem = new Elem({ tag: 'div' });
128
+ const child1 = new Elem({ tag: 'span' });
129
+ const child2 = new Elem({ tag: 'p' });
130
+
131
+ elem.append(child1, child2);
132
+
133
+ // eslint-disable-next-line testing-library/no-node-access
134
+ expect(elem.children).toEqual([child1, child2]);
135
+
136
+ expect(elem.children.length).toBe(2);
137
+ });
138
+
139
+ test('returns empty array when no Elem children', () => {
140
+ elem = new Elem();
141
+
142
+ // eslint-disable-next-line testing-library/no-node-access
143
+ expect(elem.children).toEqual([]);
144
+ });
145
+ });
146
+
147
+ describe('class manipulation', () => {
148
+ beforeEach(() => {
149
+ elem = new Elem({ className: 'initial btn btn-primary active' });
150
+ document.body.appendChild(elem.elem);
151
+ });
152
+
153
+ test('checks for classes correctly', () => {
154
+ expect(elem.hasClass('btn')).toBe(true);
155
+ expect(elem.hasClass('btn', 'active')).toBe(true);
156
+ expect(elem.hasClass('nonexistent')).toBe(false);
157
+ expect(elem.hasClass('btn', 'nonexistent')).toBe(false);
158
+ });
159
+
160
+ test('supports regex patterns in hasClass', () => {
161
+ expect(elem.hasClass(/btn-primary/)).toBe(true);
162
+ expect(elem.hasClass(/\bbtn\b/)).toBe(true);
163
+ expect(elem.hasClass(/active/)).toBe(true);
164
+ expect(elem.hasClass(/^initial/)).toBe(true);
165
+ expect(elem.hasClass(/nonexistent/)).toBe(false);
166
+
167
+ expect(elem.hasClass(/btn/, 'active')).toBe(true);
168
+ expect(elem.hasClass(/btn/, 'nonexistent')).toBe(false);
169
+ });
170
+
171
+ test('adds classes correctly', () => {
172
+ elem.addClass('new-class', 'another-class');
173
+
174
+ expect(elem.elem).toHaveClass('new-class', 'another-class');
175
+ });
176
+
177
+ test('adds classes from arrays', () => {
178
+ elem.addClass(['array-class1', 'array-class2'], 'single-class');
179
+
180
+ expect(elem.elem).toHaveClass('array-class1', 'array-class2', 'single-class');
181
+ });
182
+
183
+ test('returns this for chaining addClass', () => {
184
+ const result = elem.addClass('chainable');
185
+ expect(result).toBe(elem);
186
+ });
187
+
188
+ test('removes specific classes', () => {
189
+ elem.removeClass('active');
190
+
191
+ expect(elem.elem).not.toHaveClass('active');
192
+ expect(elem.elem).toHaveClass('btn');
193
+ });
194
+
195
+ test('removes classes with regex patterns', () => {
196
+ elem.removeClass(/btn-primary/g);
197
+
198
+ expect(elem.elem).not.toHaveClass('btn-primary');
199
+ expect(elem.elem).toHaveClass('btn', 'active');
200
+ });
201
+
202
+ test('returns this for chaining removeClass', () => {
203
+ const result = elem.removeClass('active');
204
+ expect(result).toBe(elem);
205
+ });
206
+ });
207
+
208
+ describe('content manipulation', () => {
209
+ beforeEach(() => {
210
+ elem = new Elem({ tag: 'div' });
211
+ document.body.appendChild(elem.elem);
212
+ });
213
+
214
+ test('empties element content', () => {
215
+ elem.elem.innerHTML = '<span>child</span><p>another</p>';
216
+ elem.empty();
217
+
218
+ expect(queryByText(document.body, 'child')).not.toBeInTheDocument();
219
+ expect(queryByText(document.body, 'another')).not.toBeInTheDocument();
220
+ });
221
+
222
+ test('sets text content', () => {
223
+ elem.content('Hello world');
224
+ expect(screen.getByText('Hello world')).toBeInTheDocument();
225
+ });
226
+
227
+ test('sets element content', () => {
228
+ const child = new Elem({ tag: 'span', textContent: 'Child content' });
229
+ elem.content(child);
230
+
231
+ expect(screen.getByText('Child content')).toBeInTheDocument();
232
+ });
233
+
234
+ test('replaces existing content when setting new content', () => {
235
+ elem.elem.innerHTML = '<p>Original</p>';
236
+ elem.content('New content');
237
+
238
+ expect(screen.getByText('New content')).toBeInTheDocument();
239
+ expect(queryByText(document.body, 'Original')).not.toBeInTheDocument();
240
+ });
241
+
242
+ test('returns this for chaining content', () => {
243
+ const result = elem.content('test');
244
+ expect(result).toBe(elem);
245
+ });
246
+ });
247
+
248
+ describe('styling', () => {
249
+ beforeEach(() => {
250
+ elem = new Elem();
251
+ document.body.appendChild(elem.elem);
252
+ });
253
+
254
+ test('applies CSS styles', () => {
255
+ elem.setStyle({
256
+ color: 'red',
257
+ fontSize: '16px',
258
+ backgroundColor: 'blue',
259
+ });
260
+
261
+ expect(elem.elem).toHaveStyle({
262
+ color: 'red',
263
+ fontSize: '16px',
264
+ backgroundColor: 'blue',
265
+ });
266
+ });
267
+
268
+ test('ignores invalid style inputs', () => {
269
+ elem.setStyle(null);
270
+ elem.setStyle('not-an-object');
271
+ elem.setStyle(['not', 'an', 'object']);
272
+
273
+ const result = elem.setStyle({ color: 'red' });
274
+ expect(result).toBe(elem);
275
+ expect(elem.elem).toHaveStyle({ color: 'red' });
276
+ });
277
+
278
+ test('ignores invalid style keys', () => {
279
+ elem.setStyle({
280
+ '123invalid': 'value',
281
+ '': 'empty-key',
282
+ color: 'red',
283
+ });
284
+
285
+ expect(elem.elem).toHaveStyle({ color: 'red' });
286
+ });
287
+
288
+ test('returns this for chaining setStyle', () => {
289
+ const result = elem.setStyle({ color: 'red' });
290
+ expect(result).toBe(elem);
291
+ });
292
+ });
293
+
294
+ describe('attributes', () => {
295
+ beforeEach(() => {
296
+ elem = new Elem();
297
+ document.body.appendChild(elem.elem);
298
+ });
299
+
300
+ test('sets HTML attributes', () => {
301
+ elem.setAttributes({
302
+ 'data-id': '123',
303
+ role: 'button',
304
+ 'aria-label': 'Close button',
305
+ });
306
+
307
+ expect(elem.elem).toHaveAttribute('data-id', '123');
308
+ expect(elem.elem).toHaveAttribute('role', 'button');
309
+ expect(elem.elem).toHaveAttribute('aria-label', 'Close button');
310
+ });
311
+
312
+ test('returns this for chaining setAttributes', () => {
313
+ const result = elem.setAttributes({ 'data-test': 'value' });
314
+ expect(result).toBe(elem);
315
+ });
316
+ });
317
+
318
+ describe('child element management', () => {
319
+ beforeEach(() => {
320
+ elem = new Elem({
321
+ tag: 'div',
322
+ attributes: { 'data-testid': 'parent' },
323
+ });
324
+ document.body.appendChild(elem.elem);
325
+ });
326
+
327
+ test('appends child elements in correct order', () => {
328
+ const child1 = new Elem({ tag: 'span', textContent: 'Child 1' });
329
+ const child2 = new Elem({ tag: 'p', textContent: 'Child 2' });
330
+
331
+ elem.append(child1, child2);
332
+
333
+ expect(screen.getByText('Child 1')).toBeInTheDocument();
334
+ expect(screen.getByText('Child 2')).toBeInTheDocument();
335
+
336
+ const child1Element = screen.getByText('Child 1');
337
+ const child2Element = screen.getByText('Child 2');
338
+ expect(child1Element.compareDocumentPosition(child2Element)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
339
+ });
340
+
341
+ test('appends nested arrays of children', () => {
342
+ const child1 = new Elem({ tag: 'span', textContent: 'Child 1' });
343
+ const child2 = new Elem({ tag: 'span', textContent: 'Child 2' });
344
+ const child3 = new Elem({ tag: 'span', textContent: 'Child 3' });
345
+
346
+ elem.append([child1, [child2]], child3);
347
+
348
+ expect(screen.getByText('Child 1')).toBeInTheDocument();
349
+ expect(screen.getByText('Child 2')).toBeInTheDocument();
350
+ expect(screen.getByText('Child 3')).toBeInTheDocument();
351
+
352
+ const child1Element = screen.getByText('Child 1');
353
+ const child2Element = screen.getByText('Child 2');
354
+ const child3Element = screen.getByText('Child 3');
355
+
356
+ expect(child1Element.compareDocumentPosition(child2Element)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
357
+ expect(child2Element.compareDocumentPosition(child3Element)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
358
+ });
359
+
360
+ test('appends raw HTMLElements', () => {
361
+ const rawDiv = document.createElement('div');
362
+ rawDiv.textContent = 'Raw element';
363
+
364
+ elem.append(rawDiv);
365
+
366
+ expect(screen.getByText('Raw element')).toBeInTheDocument();
367
+ });
368
+
369
+ test('ignores null/undefined children in append', () => {
370
+ const validChild = new Elem({ textContent: 'Valid' });
371
+ elem.append(null, undefined, validChild);
372
+
373
+ expect(screen.getByText('Valid')).toBeInTheDocument();
374
+
375
+ expect(screen.queryByText('null')).not.toBeInTheDocument();
376
+ expect(screen.queryByText('undefined')).not.toBeInTheDocument();
377
+ });
378
+
379
+ test('prepends child elements in correct order', () => {
380
+ elem.elem.innerHTML = '<p>Existing</p>';
381
+
382
+ const child1 = new Elem({ tag: 'span', textContent: 'Prepended 1' });
383
+ const child2 = new Elem({ tag: 'div', textContent: 'Prepended 2' });
384
+
385
+ elem.prepend(child1, child2);
386
+
387
+ expect(screen.getByText('Prepended 1')).toBeInTheDocument();
388
+ expect(screen.getByText('Prepended 2')).toBeInTheDocument();
389
+ expect(screen.getByText('Existing')).toBeInTheDocument();
390
+
391
+ const prepended1 = screen.getByText('Prepended 1');
392
+ const prepended2 = screen.getByText('Prepended 2');
393
+ const existing = screen.getByText('Existing');
394
+
395
+ expect(prepended2.compareDocumentPosition(prepended1)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
396
+
397
+ expect(prepended1.compareDocumentPosition(existing)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
398
+ });
399
+
400
+ test('prepends to empty element', () => {
401
+ const child = new Elem({ tag: 'span', textContent: 'First child' });
402
+
403
+ elem.prepend(child);
404
+
405
+ expect(screen.getByText('First child')).toBeInTheDocument();
406
+ });
407
+
408
+ test('prepend maintains correct order with multiple calls', () => {
409
+ const child1 = new Elem({ tag: 'span', textContent: 'First' });
410
+ const child2 = new Elem({ tag: 'span', textContent: 'Second' });
411
+ const child3 = new Elem({ tag: 'span', textContent: 'Third' });
412
+
413
+ elem.prepend(child1);
414
+ elem.prepend(child2);
415
+ elem.prepend(child3);
416
+
417
+ const firstElement = screen.getByText('First');
418
+ const secondElement = screen.getByText('Second');
419
+ const thirdElement = screen.getByText('Third');
420
+
421
+ expect(thirdElement.compareDocumentPosition(secondElement)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
422
+ expect(secondElement.compareDocumentPosition(firstElement)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
423
+ });
424
+
425
+ test('returns this for chaining append/prepend', () => {
426
+ const child = new Elem();
427
+
428
+ expect(elem.append(child)).toBe(elem);
429
+ expect(elem.prepend(child)).toBe(elem);
430
+ });
431
+ });
432
+
433
+ describe('parent manipulation', () => {
434
+ test('appends to parent element', () => {
435
+ const parent = new Elem({
436
+ tag: 'div',
437
+ attributes: { 'data-testid': 'parent' },
438
+ });
439
+ elem = new Elem({ tag: 'span', textContent: 'Child element' });
440
+ document.body.appendChild(parent.elem);
441
+
442
+ elem.appendTo(parent);
443
+
444
+ expect(screen.getByText('Child element')).toBeInTheDocument();
445
+
446
+ const parentElement = screen.getByTestId('parent');
447
+ expect(within(parentElement).getByText('Child element')).toBeInTheDocument();
448
+ });
449
+
450
+ test('appends to raw HTMLElement parent', () => {
451
+ const parent = document.createElement('div');
452
+ parent.setAttribute('data-testid', 'parent');
453
+ elem = new Elem({ tag: 'span', textContent: 'Child element' });
454
+ document.body.appendChild(parent);
455
+
456
+ elem.appendTo(parent);
457
+
458
+ expect(screen.getByText('Child element')).toBeInTheDocument();
459
+
460
+ const parentElement = screen.getByTestId('parent');
461
+ expect(within(parentElement).getByText('Child element')).toBeInTheDocument();
462
+ });
463
+
464
+ test('prepends to parent element maintaining order', () => {
465
+ const parent = new Elem({
466
+ tag: 'div',
467
+ attributes: { 'data-testid': 'parent' },
468
+ });
469
+ parent.elem.innerHTML = '<p>Existing</p>';
470
+ document.body.appendChild(parent.elem);
471
+
472
+ elem = new Elem({ tag: 'span', textContent: 'Prepended' });
473
+ elem.prependTo(parent.elem);
474
+
475
+ expect(screen.getByText('Prepended')).toBeInTheDocument();
476
+ expect(screen.getByText('Existing')).toBeInTheDocument();
477
+
478
+ const prependedElement = screen.getByText('Prepended');
479
+ const existingElement = screen.getByText('Existing');
480
+ expect(prependedElement.compareDocumentPosition(existingElement)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
481
+ });
482
+
483
+ test('prepends to empty parent', () => {
484
+ const parent = document.createElement('div');
485
+ parent.setAttribute('data-testid', 'parent');
486
+ document.body.appendChild(parent);
487
+
488
+ elem = new Elem({ tag: 'span', textContent: 'First child' });
489
+ elem.prependTo(parent);
490
+
491
+ expect(screen.getByText('First child')).toBeInTheDocument();
492
+
493
+ const parentElement = screen.getByTestId('parent');
494
+ expect(within(parentElement).getByText('First child')).toBeInTheDocument();
495
+ });
496
+
497
+ test('returns this for chaining appendTo', () => {
498
+ const parent = new Elem({ tag: 'div' });
499
+ elem = new Elem({ tag: 'span' });
500
+ document.body.appendChild(parent.elem);
501
+
502
+ expect(elem.appendTo(parent)).toBe(elem);
503
+ });
504
+
505
+ test('returns this for chaining prependTo', () => {
506
+ const parent = new Elem({ tag: 'div' });
507
+ elem = new Elem({ tag: 'span' });
508
+ document.body.appendChild(parent.elem);
509
+
510
+ expect(elem.prependTo(parent.elem)).toBe(elem);
511
+ });
512
+
513
+ test('handles invalid parent in appendTo', () => {
514
+ elem = new Elem();
515
+ expect(() => elem.appendTo(null)).not.toThrow();
516
+ expect(() => elem.appendTo({})).not.toThrow();
517
+ });
518
+
519
+ test('before option inserts element before reference Elem', () => {
520
+ const parent = new Elem({ tag: 'div' });
521
+ const existing = new Elem({ tag: 'p', textContent: 'Existing' });
522
+ parent.append(existing);
523
+ document.body.appendChild(parent.elem);
524
+
525
+ elem = new Elem({ tag: 'span', textContent: 'Before', before: existing });
526
+
527
+ const beforeEl = screen.getByText('Before');
528
+ const existingEl = screen.getByText('Existing');
529
+ expect(beforeEl).toBeInTheDocument();
530
+ expect(beforeEl.compareDocumentPosition(existingEl)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
531
+ });
532
+
533
+ test('before option accepts raw HTMLElement', () => {
534
+ const parent = document.createElement('div');
535
+ const existing = document.createElement('p');
536
+ existing.textContent = 'Existing';
537
+ parent.appendChild(existing);
538
+ document.body.appendChild(parent);
539
+
540
+ elem = new Elem({ tag: 'span', textContent: 'Before', before: existing });
541
+
542
+ const beforeEl = screen.getByText('Before');
543
+ expect(beforeEl).toBeInTheDocument();
544
+ expect(beforeEl.compareDocumentPosition(existing)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
545
+ });
546
+
547
+ test('before option is a no-op when reference has no parent', () => {
548
+ const orphan = new Elem({ tag: 'p' });
549
+ elem = new Elem({ tag: 'span' });
550
+ expect(() => {
551
+ elem = new Elem({ tag: 'span', before: orphan });
552
+ }).not.toThrow();
553
+ });
554
+ });
555
+
556
+ describe('option routing', () => {
557
+ test('calls Elem methods for matching option keys', () => {
558
+ const setStyleSpy = spyOn(Elem.prototype, 'setStyle');
559
+ const setAttributesSpy = spyOn(Elem.prototype, 'setAttributes');
560
+
561
+ elem = new Elem({
562
+ style: { color: 'red' },
563
+ attributes: { 'data-test': 'value' },
564
+ });
565
+
566
+ expect(setStyleSpy).toHaveBeenCalledWith({ color: 'red' });
567
+ expect(setAttributesSpy).toHaveBeenCalledWith({ 'data-test': 'value' });
568
+ });
569
+
570
+ test('sets HTMLElement properties directly', () => {
571
+ elem = new Elem({
572
+ id: 'test-id',
573
+ className: 'test-class',
574
+ textContent: 'test-content',
575
+ });
576
+ document.body.appendChild(elem.elem);
577
+
578
+ const element = screen.getByText('test-content');
579
+ expect(element).toHaveAttribute('id', 'test-id');
580
+ expect(element).toHaveClass('test-class');
581
+ });
582
+
583
+ test('unwraps Elem instances when setting properties', () => {
584
+ const parent = new Elem({ tag: 'div' });
585
+ elem = new Elem({ tag: 'span' });
586
+
587
+ parent.elem.appendChild = mock(child => {
588
+ expect(child).toBe(elem.elem);
589
+ });
590
+
591
+ parent._setOption('appendChild', elem);
592
+ });
593
+ });
594
+
595
+ describe('setOptions', () => {
596
+ test('updates multiple options at once', () => {
597
+ elem = new Elem();
598
+
599
+ elem.setOptions({
600
+ textContent: 'Updated text',
601
+ className: 'updated-class',
602
+ id: 'updated-id',
603
+ });
604
+
605
+ expect(elem.options.textContent).toBe('Updated text');
606
+ expect(elem.options.className).toBe('updated-class');
607
+ expect(elem.options.id).toBe('updated-id');
608
+ });
609
+
610
+ test('returns this for chaining', () => {
611
+ elem = new Elem();
612
+ const result = elem.setOptions({ textContent: 'test' });
613
+ expect(result).toBe(elem);
614
+ });
615
+ });
616
+
617
+ describe('integration scenarios', () => {
618
+ test('creates complex nested structure', () => {
619
+ elem = new Elem(
620
+ {
621
+ tag: 'article',
622
+ className: 'card',
623
+ style: { padding: '20px' },
624
+ attributes: { 'data-article': 'featured' },
625
+ },
626
+ new Elem({ tag: 'h2', textContent: 'Article Title' }),
627
+ new Elem(
628
+ { tag: 'div', className: 'content' },
629
+ new Elem({ tag: 'p', textContent: 'Article content goes here.' }),
630
+ new Elem({ tag: 'a', textContent: 'Read more', href: '#' }),
631
+ ),
632
+ );
633
+ document.body.appendChild(elem.elem);
634
+
635
+ const article = screen.getByRole('article');
636
+ expect(article).toHaveClass('card');
637
+ expect(article).toHaveStyle({ padding: '20px' });
638
+ expect(article).toHaveAttribute('data-article', 'featured');
639
+ expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('Article Title');
640
+ expect(screen.getByText('Article content goes here.')).toBeInTheDocument();
641
+ expect(screen.getByRole('link', { name: 'Read more' })).toBeInTheDocument();
642
+ });
643
+
644
+ test('demonstrates method chaining', () => {
645
+ elem = new Elem({ tag: 'button', textContent: 'Click me' })
646
+ .addClass('btn', 'btn-primary')
647
+ .setStyle({ fontSize: '14px', padding: '10px' })
648
+ .setAttributes({ type: 'button', role: 'button' });
649
+ document.body.appendChild(elem.elem);
650
+
651
+ const button = screen.getByRole('button', { name: 'Click me' });
652
+ expect(button).toHaveClass('btn', 'btn-primary');
653
+ expect(button).toHaveStyle({ fontSize: '14px', padding: '10px' });
654
+ expect(button).toHaveAttribute('type', 'button');
655
+ });
656
+
657
+ test('handles event handlers correctly', async () => {
658
+ const clickHandler = mock();
659
+
660
+ elem = new Elem({
661
+ tag: 'button',
662
+ textContent: 'Click me',
663
+ onclick: clickHandler,
664
+ });
665
+ document.body.appendChild(elem.elem);
666
+
667
+ await user.click(await findByRole(document.body, 'button', { name: 'Click me' }));
668
+
669
+ expect(clickHandler).toHaveBeenCalled();
670
+ });
671
+
672
+ test('works with document.body operations', () => {
673
+ elem = new Elem({ tag: 'div', textContent: 'Test content' });
674
+ elem.appendTo(document.body);
675
+
676
+ expect(screen.getByText('Test content')).toBeInTheDocument();
677
+ });
678
+ });
679
+ });