Skip to content

Commit 241ae5d

Browse files
author
TanmayRanaware
committed
Add comprehensive unit test suite for Mixin sink
- Created BDD-style test suite following existing patterns - Tests cover plain objects, futures/promises, event listeners - Includes edge cases and complex scenarios - Validates sink configuration structure - Ensures proper attribute application and removal
1 parent adc7bd5 commit 241ae5d

File tree

1 file changed

+342
-0
lines changed

1 file changed

+342
-0
lines changed

src/sinks/mixin-sink.test.ts

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

0 commit comments

Comments
 (0)