Completed
Push — master ( 598cbd...912844 )
by wiese
432:38 queued 367:30
created

enders when value changedꞌ)   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 20
rs 9.4285
cc 1
nc 1
nop 1
1
2
'use strict';
3
4
var test = require( 'tape-catch' ),
5
	sinon = require( 'sinon' ),
6
	formComponents = require( '../lib/form_components' ),
7
	objectAssign = require( 'object-assign' ),
8
	createSpyingElement = function () {
9
		return {
10
			on: sinon.spy(),
11
			is: sinon.stub(),
12
			val: sinon.spy(),
13
			prop: sinon.stub(),
14
			text: sinon.spy(),
15
			change: sinon.spy()
16
		};
17
	},
18
	assertChangeHandlerWasSet = function ( t, spyingElement, expectedCallCount ) {
19
		t.equal( spyingElement.on.callCount, expectedCallCount || 1, 'event handler was set' );
20
		t.equal( spyingElement.on.firstCall.args[ 0 ], 'change', 'event handler was set for change events' );
21
		t.equal( typeof spyingElement.on.firstCall.args[ 1 ], 'function', 'event handler is a function' );
22
	},
23
	createBankDataConfig = function () {
24
		return {
25
			ibanElement: createSpyingElement(),
26
			bicElement: createSpyingElement(),
27
			accountNumberElement: createSpyingElement(),
28
			bankCodeElement: createSpyingElement(),
29
			bankNameFieldElement: createSpyingElement(),
30
			bankNameDisplayElement: createSpyingElement()
31
		};
32
	},
33
	createAmountParser = function () {
34
		return {
35
			parse: sinon.stub().returnsArg( 0 ),
36
			getDecimalDelimiter: sinon.stub().returns( ',' )
37
		}
38
	}
39
40
	;
41
42
test( 'Components add change handling function to their elements', function ( t ) {
43
	var element = createSpyingElement(),
44
		store = {},
45
		component = formComponents.createTextComponent( store, element, 'value' );
46
	t.ok( element.on.calledOnce, 'event binding function is called once' );
47
	t.ok( element.on.calledWith( 'change', component.onChange ) );
48
	t.end();
49
} );
50
51
test( 'Validating components add change handling function to their elements', function ( t ) {
52
	var element = createSpyingElement(),
53
		store = {},
54
		component = formComponents.createValidatingTextComponent( store, element, 'value' );
55
	t.ok( element.on.calledTwice, 'event binding function is called twice' );
56
	t.ok( element.on.calledWith( 'change', component.onChange ) );
57
	t.end();
58
} );
59
60
test( 'Change handler of components dispatches change action to store', function ( t ) {
61
	var element = createSpyingElement(),
62
		store = {
63
			dispatch: sinon.spy()
64
		},
65
		fakeEvent = { target: { value: 'current value' } },
66
		expectedAction = { type: 'CHANGE_CONTENT', payload: { value: 'current value', contentName: 'value' } },
67
		component = formComponents.createTextComponent( store, element, 'value' );
68
69
	component.onChange( fakeEvent );
70
71
	t.ok( store.dispatch.calledOnce, 'store dispatch is called once' );
72
	t.ok( store.dispatch.calledWith( expectedAction ), 'action contains event value' );
73
74
	t.end();
75
} );
76
77
test( 'Rendering the text component sets the value', function ( t ) {
78
	var element = createSpyingElement(),
79
		store = {},
80
		component = formComponents.createTextComponent( store, element, 'value' );
81
82
	component.render( { value: 'the new awesome value' } );
83
84
	t.ok( element.val.calledTwice, 'value is called twice (get/set)' );
85
	t.ok( element.val.calledWith( 'the new awesome value' ) );
86
	t.end();
87
} );
88
89
test( 'Text value is only set when element does not have focus', function ( t ) {
90
	var element = createSpyingElement(),
91
		store = {},
92
		component = formComponents.createTextComponent( store, element, 'value' );
93
94
	element.is.withArgs( ':focus' ).returns( true );
95
	component.render( { value: 'the new awesome value' } );
96
97
	t.ok( element.val.calledOnce, 'value is only read' );
98
	t.ok( element.val.getCall( 0 ).notCalledWith( 'the new awesome value' ), 'value is only read' );
99
	t.end();
100
} );
101
102
test( 'Rendering the radio component sets the value as array', function ( t ) {
103
	var element = createSpyingElement(),
104
		store = {},
105
		component = formComponents.createRadioComponent( store, element, 'value' );
106
107
	component.render( { value: 'the new awesome value' } );
108
109
	t.ok( element.val.calledOnce, 'value is set once' );
110
	t.ok( element.val.calledWith( [ 'the new awesome value' ] ) );
111
	t.end();
112
} );
113
114
test( 'Given true, rendering the checkbox component applies the checked property', function ( t ) {
115
	var element = createSpyingElement(),
116
		store = {},
117
		component = formComponents.createCheckboxComponent( store, element, 'value' );
118
119
	component.render( { value: true } );
120
121
	t.ok( element.prop.calledOnce, 'value is set once' );
122
	t.ok( element.prop.calledWith( 'checked', true ) );
123
	t.end();
124
} );
125
126
test( 'Given false, rendering the checkbox component applies the checked property', function ( t ) {
127
	var element = createSpyingElement(),
128
		store = {},
129
		component = formComponents.createCheckboxComponent( store, element, 'value' );
130
131
	component.render( { value: false } );
132
133
	t.ok( element.prop.calledOnce, 'value is set once' );
134
	t.ok( element.prop.calledWith( 'checked', false ) );
135
	t.end();
136
} );
137
138
test( 'Event handler for checkbox component stores checked state', function ( t ) {
139
	var element = createSpyingElement(),
140
		store = { dispatch: sinon.spy() },
141
		fakeEvent = { target: { value: 'current value' } },
142
		expectedAction = { type: 'CHANGE_CONTENT', payload: { value: true, contentName: 'value' } },
143
		component = formComponents.createCheckboxComponent( store, element, 'value' );
144
145
	element.prop.returns( true );
146
147
	component.onChange( fakeEvent );
148
149
	t.ok( store.dispatch.calledOnce, 'action is dispatched' );
150
	t.ok( store.dispatch.calledWith( expectedAction ), 'action contains expected value ' );
151
	t.end();
152
} );
153
154
test( 'Rendering the amount component with custom amount clears selection and sets text and hidden fields', function ( t ) {
155
	var textElement = createSpyingElement(),
156
		selectElement = createSpyingElement(),
157
		hiddenElement = createSpyingElement(),
158
		parser = createAmountParser(),
159
		dummyFormatter = { format: function ( v ) {
160
			return "XX" + v + "YY";
161
162
		} },
163
		store = {},
164
		parent = {
165
			addClass: sinon.spy()
166
		}
167
	;
168
169
	textElement.parent = function () {
170
		return parent;
171
	};
172
173
	var component = formComponents.createAmountComponent( store, textElement, selectElement, hiddenElement, parser, dummyFormatter );
174
175
	component.render( { amount: 2300, isCustomAmount: true } );
176
177
	t.ok( textElement.val.calledOnce, 'value is set once' );
178
179
	t.ok( textElement.val.calledWith( 'XX2300YY' ) );
180
	t.ok( parent.addClass.withArgs( 'filled' ).calledOnce );
181
	t.ok( selectElement.val.callCount === 0, 'select element value is not set' );
182
	t.ok( selectElement.prop.calledOnce, 'property was set' );
183
	t.ok( selectElement.prop.calledWith( 'checked', false ), 'check property was removed' );
184
	t.ok( hiddenElement.val.calledOnce, 'hidden element value is set' );
185
	t.ok( hiddenElement.val.calledWith( 'XX2300YY' ), 'hidden element value is set' );
186
187
	t.end();
188
} );
189
190
test( 'Rendering the amount component with non-custom amount sets the hidden field and clears the text field', function ( t ) {
191
	var textElement = createSpyingElement(),
192
		selectElement = createSpyingElement(),
193
		hiddenElement = createSpyingElement(),
194
		parser = createAmountParser(),
195
		dummyFormatter = { format: function ( v ) {
196
			return "XX" + v + "YY";
197
198
		} },
199
		store = {},
200
		parent = {
201
			removeClass: sinon.stub()
202
		}
203
	;
204
205
	textElement.parent = function () {
206
		return parent;
207
	};
208
209
	var component = formComponents.createAmountComponent( store, textElement, selectElement, hiddenElement, parser, dummyFormatter );
210
211
	component.render( { amount: 5000, isCustomAmount: false } );
212
213
	t.ok( textElement.val.calledOnce, 'value is cleared' );
214
	t.ok( textElement.val.calledWith( '' ) );
215
	t.ok( parent.removeClass.withArgs( 'filled' ).calledOnce );
216
	t.ok( selectElement.val.calledOnce, 'select element value is set' );
217
	t.ok( selectElement.val.calledWith( [ '5000' ] ), 'select element value is set' ); // needs to be array for selects
218
	t.ok( hiddenElement.val.calledOnce, 'hidden element value is set' );
219
	t.ok( hiddenElement.val.calledWith( 'XX5000YY' ), 'hidden element value is set' );
220
	t.end();
221
} );
222
223
test( 'Changing the amount selection dispatches select action with selected value', function ( t ) {
224
	var textElement = createSpyingElement(),
225
		selectElement = createSpyingElement(),
226
		hiddenElement = createSpyingElement(),
227
		store = {
228
			dispatch: sinon.spy()
229
		},
230
		dummyAmountParser = createAmountParser(),
231
		fakeEvent = { target: { value: 5000 } },
232
		expectedAction = { type: 'SELECT_AMOUNT', payload: { amount: 5000 } };
233
234
	formComponents.createAmountComponent( store, textElement, selectElement, hiddenElement, dummyAmountParser );
235
236
	t.ok( selectElement.on.calledOnce, 'event handler is attached' );
237
238
	// simulate event trigger by calling event handling function
239
	selectElement.on.args[ 0 ][ 1 ]( fakeEvent );
240
241
	t.ok( store.dispatch.calledOnce, 'event handler triggers store update' );
242
	t.deepEqual( store.dispatch.args[ 0 ][ 0 ], expectedAction, 'event handler generates the correct action' );
243
	t.ok( dummyAmountParser.parse.notCalled, 'amount parser is not called as the amount is passed on 1:1' );
244
	t.end();
245
} );
246
247
test( 'Changing the amount input dispatches input action with parsed content', function ( t ) {
248
	var textElement = createSpyingElement(),
249
		selectElement = createSpyingElement(),
250
		hiddenElement = createSpyingElement(),
251
		dummyAmountParser = createAmountParser(),
252
		store = {
253
			dispatch: sinon.spy()
254
		},
255
		fakeEvent = { target: { value: '99,99' } },
256
		expectedAction = { type: 'INPUT_AMOUNT', payload: { amount: '99,99' } };
257
258
	formComponents.createAmountComponent( store, textElement, selectElement, hiddenElement, dummyAmountParser );
259
260
	t.ok( textElement.on.withArgs( 'change' ).calledOnce, 'text input event handler is attached' );
261
262
	// simulate event trigger by calling event handling function
263
	textElement.on.withArgs( 'change' ).args[ 0 ][ 1 ]( fakeEvent );
264
265
	t.ok( store.dispatch.calledOnce, 'event handler triggers store update' );
266
	t.deepEqual( store.dispatch.args[ 0 ][ 0 ], expectedAction, 'event handler generates the correct action' );
267
	t.ok( dummyAmountParser.parse.calledWith( '99,99' ), 'parser us called with amount' );
268
	t.end();
269
} );
270
271
test( 'Bank data component adds change handling function to its elements', function ( t ) {
272
	var bankDataComponentConfig = createBankDataConfig(),
273
		store = {};
274
275
	formComponents.createBankDataComponent( store, bankDataComponentConfig );
276
277
	assertChangeHandlerWasSet( t, bankDataComponentConfig.ibanElement );
278
	assertChangeHandlerWasSet( t, bankDataComponentConfig.bicElement, 2 );
279
	assertChangeHandlerWasSet( t, bankDataComponentConfig.accountNumberElement );
280
	assertChangeHandlerWasSet( t, bankDataComponentConfig.bankCodeElement );
281
	// Bank data is a pure display field with no change handler
282
	t.end();
283
} );
284
285
test( 'Bank data component renders the store values in its elements', function ( t ) {
286
	var bankDataComponentConfig = createBankDataConfig(),
287
		store = {},
288
		handler = formComponents.createBankDataComponent( store, bankDataComponentConfig );
289
290
	handler.render( {
291
		iban: 'DE12500105170648489890',
292
		bic: 'INGDDEFFXXX',
293
		accountNumber: '0648489890',
294
		bankCode: '50010517',
295
		bankName: 'ING-DiBa',
296
		debitType: 'non-sepa'
297
	} );
298
299
	t.equal( bankDataComponentConfig.ibanElement.val.args[ 0 ][ 0 ], 'DE12500105170648489890', 'IBAN value is set' );
300
	t.equal( bankDataComponentConfig.bicElement.val.args[ 0 ][ 0 ], 'INGDDEFFXXX', 'BIC value is set' );
301
	t.equal( bankDataComponentConfig.accountNumberElement.val.args[ 0 ][ 0 ], '0648489890', 'Account number is set' );
302
	t.equal( bankDataComponentConfig.bankCodeElement.val.args[ 0 ][ 0 ], '50010517', 'BIC value is set' );
303
	t.equal( bankDataComponentConfig.bankNameDisplayElement.text.args[ 0 ][ 0 ], 'ING-DiBa', 'Bank name is displayed' );
304
	t.equal( bankDataComponentConfig.bankNameFieldElement.val.args[ 0 ][ 0 ], 'ING-DiBa', 'Bank name is set in field' );
305
306
	t.end();
307
} );
308
309
test( 'Bank data component checks if all elements in the configuration are set', function ( t ) {
310
	// TODO Do more than a spot check. How do i check all fields without using a loop in the test?
311
	// The test succeeds without checking code in the factory function because of the calls to the event binding
312
	// However, if bankName instead of iban was missing, the test would fail.
313
	var bankDataComponentConfigWithMissingIban = createBankDataConfig(),
314
		store = {};
315
316
	delete bankDataComponentConfigWithMissingIban.ibanElement;
317
318
	t.throws( function () {
319
		formComponents.createBankDataComponent( store, bankDataComponentConfigWithMissingIban );
320
	} );
321
322
	t.end();
323
} );
324
325
test( 'addEagerChangeBehavior calls onChange and validator handler on keypress event', function ( t ) {
326
	var component = {
327
			element: createSpyingElement(),
328
			onChange: sinon.spy(),
329
			validator: sinon.spy()
330
		},
331
		synchronousDebounce = function ( f ) {
332
			return f;
333
		},
334
		fakeEvent = { target: { value: 'Berlin' } };
335
336
	formComponents.addEagerChangeBehavior( component, synchronousDebounce );
337
338
	t.ok( component.element.on.withArgs( 'keypress' ).calledOnce, 'keypress event is attached' );
339
340
	component.element.on.withArgs( 'keypress' ).args[ 0 ][ 1 ]( fakeEvent );
341
342
	t.ok( component.onChange.calledOnce, 'change handler was called once' );
343
	t.ok( component.onChange.calledWith( fakeEvent ), 'change handler was called with event' );
344
345
	t.ok( component.validator.calledOnce, 'validator  was called once' );
346
	t.ok( component.validator.calledWith( fakeEvent ), 'validator was called with event' );
347
	t.end();
348
} );
349
350
test( 'SelectComponent renders when value changed', function ( t ) {
351
	var element = {
352
			val: sinon.stub(),
353
			change: sinon.stub()
354
		},
355
		component = objectAssign( Object.create( formComponents.SelectComponent ), {
356
			element: element,
357
			contentName: 'somesome'
358
		} );
359
360
	element.val.withArgs().returns( '6' ); // .val() is both the getter and setter method
361
362
	component.render( { somesome: '5' } );
363
364
	t.equals( element.val.callCount, 3 );
365
	t.deepEquals( element.val.thirdCall.args[ 0 ], [ '5' ], 'value gets set' );
366
	t.ok( element.change.calledOnce );
367
368
	t.end();
369
} );
370
371
test( 'SelectComponent treats null like empty string in update check', function ( t ) {
372
	var element = {
373
			val: sinon.stub(),
374
			change: sinon.stub()
375
		},
376
		component = objectAssign( Object.create( formComponents.SelectComponent ), {
377
			element: element,
378
			contentName: 'lorem'
379
		} );
380
381
	element.val.withArgs().returns( null ); // .val() is both the getter and setter method
382
383
	component.render( { lorem: '' } );
384
385
	t.equals( element.val.callCount, 2 );
386
	t.ok( element.change.notCalled );
387
388
	t.end();
389
} );
390