Skip to content

Commit f416fe9

Browse files
authored
Merge pull request #39 from TanmayRanaware/unit-test-mixin
Added comprehensive unit test suite for Mixin sink
2 parents cd1b034 + ed6215a commit f416fe9

File tree

1 file changed

+378
-0
lines changed

1 file changed

+378
-0
lines changed

src/sinks/mixin-sink.test.ts

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
import { MockElement } from '../test-support';
2+
import { Mixin, MIXIN_SINK_TAG } from './mixin-sink';
3+
import { AttributeObjectSink } from './attribute-sink';
4+
import { SINK_TAG } from '../constants';
5+
6+
describe('Mixin Sink', () => {
7+
8+
describe('Given a plain object mixin', () => {
9+
describe('when creating sink configuration', () => {
10+
it('creates sink binding configuration with correct properties', () => {
11+
const source = {
12+
'data-foo': 'bar',
13+
'class': 'test-class',
14+
'id': 'test-id'
15+
};
16+
17+
const config = Mixin(source);
18+
19+
expect(config.type).toBe(SINK_TAG);
20+
expect(config.t).toBe(MIXIN_SINK_TAG);
21+
expect(config.source).toBe(source);
22+
expect(config.sink).toBe(AttributeObjectSink);
23+
});
24+
});
25+
26+
describe('when applying attributes to element', () => {
27+
describe('when regular attributes are provided', () => {
28+
it('applies plain object attributes to element immediately', () => {
29+
const el = MockElement();
30+
const source = {
31+
'data-foo': 'bar',
32+
'class': 'test-class',
33+
'id': 'test-id',
34+
'title': 'test-title'
35+
};
36+
37+
const config = Mixin(source);
38+
const sink = config.sink(el);
39+
sink(config.source);
40+
41+
expect(el.dataset.foo).toBe('bar');
42+
expect(el.className).toBe('test-class');
43+
expect(el.id).toBe('test-id');
44+
expect(el.getAttribute('title')).toBe('test-title');
45+
});
46+
});
47+
48+
describe('when boolean attributes are provided', () => {
49+
it('handles boolean attributes correctly', () => {
50+
const el = MockElement();
51+
const source = {
52+
'disabled': true,
53+
'readonly': 'readonly',
54+
'checked': false
55+
};
56+
57+
const config = Mixin(source);
58+
const sink = config.sink(el);
59+
sink(config.source);
60+
61+
expect(el.disabled).toBe(true);
62+
expect(el.readOnly).toBe('readonly');
63+
expect(el.checked).toBe(false);
64+
});
65+
});
66+
67+
describe('when falsey values are provided', () => {
68+
it('removes attributes when set to falsey values', () => {
69+
const el = MockElement();
70+
71+
// Set initial attributes
72+
el.setAttribute('data-foo', 'bar');
73+
el.setAttribute('title', 'initial-title');
74+
el.className = 'initial-class';
75+
76+
const source = {
77+
'data-foo': false,
78+
'title': null,
79+
'class': undefined
80+
};
81+
82+
const config = Mixin(source);
83+
const sink = config.sink(el);
84+
sink(config.source);
85+
86+
expect(el.getAttribute('data-foo')).toBeUndefined();
87+
expect(el.getAttribute('title')).toBeUndefined();
88+
expect(el.className).toBe('');
89+
});
90+
});
91+
});
92+
});
93+
94+
describe('Given a future/promise mixin', () => {
95+
describe('when creating sink configuration', () => {
96+
it('creates sink binding configuration for future source', () => {
97+
const futureSource = Promise.resolve({
98+
'data-future': 'value',
99+
'class': 'future-class'
100+
});
101+
102+
const config = Mixin(futureSource);
103+
104+
expect(config.type).toBe(SINK_TAG);
105+
expect(config.t).toBe(MIXIN_SINK_TAG);
106+
expect(config.source).toBe(futureSource);
107+
expect(config.sink).toBe(AttributeObjectSink);
108+
});
109+
});
110+
111+
describe('when applying future attributes', () => {
112+
describe('when promise resolves successfully', () => {
113+
it('applies future attributes when promise resolves', async () => {
114+
const el = MockElement();
115+
const futureSource = Promise.resolve({
116+
'data-future': 'resolved-value',
117+
'class': 'resolved-class',
118+
'title': 'resolved-title'
119+
});
120+
121+
const config = Mixin(futureSource);
122+
const sink = config.sink(el);
123+
124+
// Apply the sink (this should handle the promise)
125+
sink(config.source);
126+
127+
// Wait for promise to resolve
128+
await new Promise(resolve => setTimeout(resolve, 10));
129+
130+
expect(el.dataset.future).toBe('resolved-value');
131+
expect(el.className).toBe('resolved-class');
132+
expect(el.getAttribute('title')).toBe('resolved-title');
133+
});
134+
});
135+
});
136+
});
137+
138+
describe('Given event listener mixins', () => {
139+
describe('when applying event listeners from mixin object', () => {
140+
describe('when multiple event handlers are provided', () => {
141+
it('applies event listeners from mixin object', () => {
142+
const el = MockElement();
143+
const clickHandler = jest.fn();
144+
const mouseoverHandler = jest.fn();
145+
146+
const source = {
147+
'onclick': clickHandler,
148+
'onmouseover': mouseoverHandler,
149+
'class': 'event-class'
150+
};
151+
152+
const config = Mixin(source);
153+
const sink = config.sink(el);
154+
sink(config.source);
155+
156+
expect(el.className).toBe('event-class');
157+
158+
// Verify event listeners are attached
159+
const clickEvent = new Event('click');
160+
el.dispatchEvent(clickEvent);
161+
expect(clickHandler).toHaveBeenCalledWith(clickEvent);
162+
163+
const mouseoverEvent = new Event('mouseover');
164+
el.dispatchEvent(mouseoverEvent);
165+
expect(mouseoverHandler).toHaveBeenCalledWith(mouseoverEvent);
166+
});
167+
});
168+
});
169+
170+
describe('when handling future event listeners', () => {
171+
describe('when promise resolves with event handlers', () => {
172+
it('handles future event listeners', async () => {
173+
const el = MockElement();
174+
const futureHandler = jest.fn();
175+
176+
const futureSource = Promise.resolve({
177+
'onclick': futureHandler,
178+
'data-future-event': 'true'
179+
});
180+
181+
const config = Mixin(futureSource);
182+
const sink = config.sink(el);
183+
sink(config.source);
184+
185+
// Wait for promise to resolve
186+
await new Promise(resolve => setTimeout(resolve, 10));
187+
188+
expect(el.dataset.futureEvent).toBe('true');
189+
190+
const clickEvent = new Event('click');
191+
el.dispatchEvent(clickEvent);
192+
expect(futureHandler).toHaveBeenCalledWith(clickEvent);
193+
});
194+
});
195+
});
196+
});
197+
198+
describe('Given complex mixin scenarios', () => {
199+
describe('when handling mixed attribute types in single mixin', () => {
200+
describe('when all attribute types are combined', () => {
201+
it('handles mixed attribute types in single mixin', () => {
202+
const el = MockElement();
203+
const clickHandler = jest.fn();
204+
205+
const source = {
206+
// Regular attributes
207+
'id': 'complex-mixin',
208+
'class': 'complex-class',
209+
'title': 'Complex Mixin',
210+
211+
// Data attributes
212+
'data-complex': 'value',
213+
'data-number': 42,
214+
215+
// Boolean attributes
216+
'disabled': false,
217+
'readonly': true,
218+
219+
// Event listeners
220+
'onclick': clickHandler,
221+
222+
// Style (if supported)
223+
'style': 'color: red; font-weight: bold;'
224+
};
225+
226+
const config = Mixin(source);
227+
const sink = config.sink(el);
228+
sink(config.source);
229+
230+
expect(el.id).toBe('complex-mixin');
231+
expect(el.className).toBe('complex-class');
232+
expect(el.getAttribute('title')).toBe('Complex Mixin');
233+
expect(el.dataset.complex).toBe('value');
234+
expect(el.dataset.number).toBe('42');
235+
expect(el.disabled).toBe(false);
236+
expect(el.readOnly).toBe(true);
237+
expect(el.getAttribute('style')).toBe('color: red; font-weight: bold;');
238+
239+
const clickEvent = new Event('click');
240+
el.dispatchEvent(clickEvent);
241+
expect(clickHandler).toHaveBeenCalledWith(clickEvent);
242+
});
243+
});
244+
});
245+
246+
describe('when applying multiple mixins to same element', () => {
247+
describe('when second mixin overwrites first mixin', () => {
248+
it('overwrites previous attributes when applied multiple times', () => {
249+
const el = MockElement();
250+
251+
const firstSource = {
252+
'id': 'first-id',
253+
'class': 'first-class',
254+
'data-value': 'first'
255+
};
256+
257+
const secondSource = {
258+
'id': 'second-id',
259+
'class': 'second-class',
260+
'data-value': 'second'
261+
};
262+
263+
const config1 = Mixin(firstSource);
264+
const config2 = Mixin(secondSource);
265+
266+
const sink1 = config1.sink(el);
267+
const sink2 = config2.sink(el);
268+
269+
sink1(config1.source);
270+
expect(el.id).toBe('first-id');
271+
expect(el.className).toBe('first-class');
272+
expect(el.dataset.value).toBe('first');
273+
274+
sink2(config2.source);
275+
expect(el.id).toBe('second-id');
276+
expect(el.className).toBe('second-class');
277+
expect(el.dataset.value).toBe('second');
278+
});
279+
});
280+
});
281+
});
282+
283+
describe('Given edge cases', () => {
284+
describe('when an empty mixin object is given', () => {
285+
describe('when no attributes are provided', () => {
286+
it('handles empty mixin object', () => {
287+
const el = MockElement();
288+
const source = {};
289+
290+
const config = Mixin(source);
291+
const sink = config.sink(el);
292+
sink(config.source);
293+
294+
// Should not throw and element should remain unchanged
295+
expect(el.className).toBe('');
296+
expect(el.id).toBe('');
297+
});
298+
});
299+
});
300+
301+
describe('when null or undefined values are given', () => {
302+
describe('when attributes have null/undefined values', () => {
303+
it('handles null and undefined values in mixin', () => {
304+
const el = MockElement();
305+
306+
// Set initial attributes
307+
el.setAttribute('data-foo', 'bar');
308+
el.className = 'initial-class';
309+
310+
const source = {
311+
'data-foo': null,
312+
'class': undefined,
313+
'title': '',
314+
'id': 'valid-id'
315+
};
316+
317+
const config = Mixin(source);
318+
const sink = config.sink(el);
319+
sink(config.source);
320+
321+
expect(el.getAttribute('data-foo')).toBeUndefined();
322+
expect(el.className).toBe('');
323+
expect(el.getAttribute('title')).toBeUndefined();
324+
expect(el.id).toBe('valid-id');
325+
});
326+
});
327+
});
328+
329+
describe('when string "false" values are given', () => {
330+
describe('when attributes contain string "false"', () => {
331+
it('handles string "false" values correctly', () => {
332+
const el = MockElement();
333+
334+
const source = {
335+
'data-false': 'false',
336+
'data-true': 'true',
337+
'disabled': 'false',
338+
'readonly': 'false'
339+
};
340+
341+
const config = Mixin(source);
342+
const sink = config.sink(el);
343+
sink(config.source);
344+
345+
// String "false" should be treated as falsy for attribute removal
346+
expect(el.getAttribute('data-false')).toBeUndefined();
347+
expect(el.dataset.true).toBe('true');
348+
expect(el.disabled).toBe(false);
349+
expect(el.getAttribute('readonly')).toBeUndefined();
350+
});
351+
});
352+
});
353+
});
354+
355+
describe('Given sink configuration structure', () => {
356+
describe('when creating sink configuration', () => {
357+
describe('when valid source is provided', () => {
358+
it('returns correct sink binding configuration type', () => {
359+
const source = { 'test': 'value' };
360+
const config = Mixin(source);
361+
362+
expect(config).toHaveProperty('type', SINK_TAG);
363+
expect(config).toHaveProperty('t', MIXIN_SINK_TAG);
364+
expect(config).toHaveProperty('source', source);
365+
expect(config).toHaveProperty('sink', AttributeObjectSink);
366+
});
367+
368+
it('preserves source reference in configuration', () => {
369+
const source = { 'preserved': 'reference' };
370+
const config = Mixin(source);
371+
372+
expect(config.source).toBe(source);
373+
});
374+
});
375+
});
376+
});
377+
378+
});

0 commit comments

Comments
 (0)