ampersand-view/test/main.js

Maintainability

65.77

Lines of code

722

Created with Raphaël 2.1.002550751002015-1-52014-12-42014-12-3

2015-5-18
Maintainability: 65.77

Created with Raphaël 2.1.002004006008002015-1-52014-12-42014-12-3

2015-5-18
Lines of Code: 722

Difficulty

57.75

Estimated Errors

7.86

Function weight

By Complexity

Created with Raphaël 2.1.0getView3

By SLOC

Created with Raphaël 2.1.0<anonymous>38
1
var test = require('tape');
2
var AmpersandModel = require('ampersand-model');
3
var AmpersandView = require('../ampersand-view');
4
 
5
var contains = function (str1, str2) {
6
    return str1.indexOf(str2) !== -1;
7
};
8
 
9
var Model = AmpersandModel.extend({
10
    props: {
11
        id: 'number',
12
        name: ['string', true],
13
        html: 'string',
14
        url: 'string',
15
        something: 'string',
16
        fireDanger: 'string'
17
    },
18
    session: {
19
        active: 'boolean'
20
    },
21
    derived: {
22
        classes: {
23
            deps: ['something', 'fireDanger', 'active'],
24
            fn: function () {
25
                return this.something + this.active;
26
            }
27
        }
28
    }
29
});
30
 
31
function getView(bindings, model) {
32
    if (!bindings.template) {
33
        bindings.template = '<li><span></span><img></li>';
34
    }
35
    var View = AmpersandView.extend(bindings);
36
    var view = new View({
37
        model: model || new Model()
38
    });
39
    return view.renderWithTemplate();
40
}
41
 
42
test('registerSubview', function (t) {
43
    var removeCalled = 0;
44
    var SubView = AmpersandView.extend({
45
        template: '<div></div>',
46
        render: function () {
47
            this.renderWithTemplate();
48
            this.el.className = 'subview';
49
        },
50
        remove: function () {
51
            removeCalled++;
52
        }
53
    });
54
    var View = AmpersandView.extend({
55
        template: '<section><div id="parent"></div></section>',
56
        render: function () {
57
            this.renderWithTemplate();
58
            // all of these should work
59
            this.renderSubview(new SubView(), this.query('#parent'));
60
            this.renderSubview(new SubView(), '#parent');
61
 
62
            // some other thing with a remove method
63
            this.registerSubview({remove: function () {
64
                removeCalled++;
65
            }});
66
        }
67
    });
68
 
69
    var main = new View({
70
        el: document.createElement('div')
71
    });
72
 
73
    main.render();
74
    t.equal(main.queryAll('.subview').length, 2);
75
    main.remove();
76
    t.equal(removeCalled, 3);
77
    t.end();
78
});
79
 
80
test('registerSubview: default container to this.el', function (t) {
81
    var removeCalled = 0;
82
    var SubView = AmpersandView.extend({
83
        template: '<div></div>',
84
        render: function () {
85
            this.renderWithTemplate();
86
            this.el.className = 'subview';
87
        },
88
        remove: function () {
89
            removeCalled++;
90
        }
91
    });
92
    var View = AmpersandView.extend({
93
        template: '<section></section>',
94
        render: function () {
95
            this.renderWithTemplate();
96
            this.renderSubview(new SubView());
97
            this.renderSubview(new SubView());
98
        }
99
    });
100
 
101
    var main = new View({
102
        el: document.createElement('div')
103
    });
104
 
105
    main.render();
106
    t.equal(main.queryAll('.subview').length, 2);
107
    t.equal(main.el.childNodes.length, 2);
108
    main.remove();
109
    t.equal(removeCalled, 2);
110
    t.end();
111
});
112
 
113
test('caching elements', function(t) {
114
  var View = AmpersandView.extend({
115
    template: '<p><span></span></p>',
116
    render: function () {
117
      this.renderWithTemplate();
118
      return this.cacheElements(({span:'span'}));
119
    }
120
  });
121
  var instance = new View(),
122
     rendered  = instance.render();
123
  t.equal(instance, rendered);
124
  t.equal(typeof rendered.span, 'object');
125
  t.end();
126
});
127
 
128
test('listen to and run', function (t) {
129
    t.plan(1);
130
    var model = new Model({
131
        props: {
132
            name: 'string'
133
        }
134
    });
135
    var View = AmpersandView.extend({
136
        initialize: function () {
137
            this.model = model;
138
            this.listenToAndRun(this.model, 'something', this.handler);
139
            t.end();
140
        },
141
        handler: function () {
142
            t.pass('handler ran');
143
        }
144
    });
145
    new View();
146
});
147
 
148
test('text bindings', function (t) {
149
    var view = getView({
150
        bindings: {
151
            'model.name': 'span'
152
        }
153
    });
154
    t.equal(view.query('span').textContent, '');
155
    view.model.set('name', 'henrik');
156
    t.equal(view.query('span').textContent, 'henrik');
157
    t.end();
158
});
159
 
160
test('src bindings', function (t) {
161
    var view = getView({
162
        bindings: {
163
            'model.url': {
164
                type: 'attribute',
165
                name: 'src',
166
                selector: 'img'
167
            }
168
        }
169
    });
170
    var img = view.query('img');
171
    t.equal(img.getAttribute('src'), '');
172
    view.model.set('url', 'http://robohash.com/whammo');
173
    t.equal(img.getAttribute('src'), 'http://robohash.com/whammo');
174
    t.end();
175
});
176
 
177
test('href bindings', function (t) {
178
    var view = getView({
179
        template: '<a href=""></a>',
180
        bindings: {
181
            'model.url': {
182
                type: 'attribute',
183
                name: 'href',
184
                selector: ''
185
            }
186
        }
187
    });
188
    var el = view.el;
189
    t.equal(el.getAttribute('href'), '');
190
    view.model.set('url', 'http://robohash.com/whammo');
191
    t.equal(el.getAttribute('href'), 'http://robohash.com/whammo');
192
    t.end();
193
});
194
 
195
test('input bindings', function (t) {
196
    var view = getView({
197
        template: '<li><input></li>',
198
        bindings: {
199
            'model.something': {
200
                type: 'attribute',
201
                selector: 'input',
202
                name: 'value'
203
            }
204
        }
205
    });
206
    var input = view.query('input');
207
    t.equal(input.value, '');
208
    view.model.set('something', 'yo');
209
    t.equal(input.value, 'yo');
210
    t.end();
211
});
212
 
213
test('class bindings', function (t) {
214
    var model = new Model();
215
    model.set({
216
        fireDanger: 'high',
217
        active: true
218
    });
219
    var view = getView({
220
        template: '<li></li>',
221
        bindings: {
222
            'model.fireDanger': {
223
                type: 'class'
224
            },
225
            'model.active': {
226
                type: 'booleanClass'
227
            }
228
        }
229
    }, model);
230
    var className = view.el.className;
231
    t.ok(contains(className, 'active'));
232
    t.ok(contains(className, 'high'));
233
    model.set('fireDanger', 'low');
234
    className = view.el.className;
235
    t.ok(!contains(className, 'high'));
236
    t.ok(contains(className, 'low'));
237
    model.set('active', false);
238
    className = view.el.className;
239
    t.ok(!contains(className, 'active'));
240
    t.ok(contains(className, 'low'));
241
    t.end();
242
});
243
 
244
test('nested binding definitions', function (t) {
245
    var model = new Model();
246
    model.set({
247
        active: true
248
    });
249
    var View = AmpersandView.extend({
250
        autoRender: true,
251
        template: '<li><div></div></li>',
252
        bindings: {
253
            'model.active': [
254
                {
255
                    type: 'booleanAttribute',
256
                    name: 'data-active',
257
                    selector: 'div'
258
                },
259
                {
260
                    type: 'booleanAttribute',
261
                    name: 'data-something',
262
                    selector: 'div'
263
                },
264
                {
265
                    selector: 'div'
266
                },
267
                {
268
                    type: 'booleanClass'
269
                }
270
            ]
271
        }
272
    });
273
    var view = new View({model: model});
274
    var li = view.el;
275
    var div = li.firstChild;
276
    t.ok(div.hasAttribute('data-active'));
277
    t.ok(div.hasAttribute('data-something'));
278
    t.equal(div.textContent, 'true');
279
    t.ok(contains(li.className, 'active'));
280
    t.end();
281
});
282
 
283
test('renderAndBind with no model', function (t) {
284
    var View = AmpersandView.extend({
285
        template: '<li><span></span><img></li>'
286
    });
287
    var view = new View();
288
    t.ok(view.renderWithTemplate()); //Should not throw error
289
    t.end();
290
});
291
 
292
test('queryByHook', function (t) {
293
    var View = AmpersandView.extend({
294
        template: '<li data-hook="list-item"><span data-hook="username"></span><img data-hook="user-avatar"></li>'
295
    });
296
    var view = new View();
297
    view.renderWithTemplate();
298
    t.ok(view.queryByHook('username') instanceof Element, 'should find username element');
299
    t.ok(view.queryByHook('user-avatar') instanceof Element, 'should find username');
300
    t.ok(view.queryByHook('nothing') === undefined, 'should find username');
301
    t.ok(view.queryByHook('list-item') instanceof Element, 'should also work for root element');
302
    t.end();
303
});
304
 
305
test('queryAllByHook', function (t) {
306
    var View = AmpersandView.extend({
307
        template: '<li data-hook="list-item"><span data-hook="username info"></span><img data-hook="user-avatar info"></li>'
308
    });
309
    var view = new View();
310
    view.renderWithTemplate();
311
    t.ok(view.queryAllByHook('info') instanceof Array, 'should return array of results');
312
    t.equal(view.queryAllByHook('info').length, 2, 'should find all relevant elements');
313
    t.ok(view.queryAllByHook('info')[0] instanceof Element, 'should be able to access found elements');
314
    t.ok(view.queryAllByHook('info')[1] instanceof Element, 'should be able to access found elements');
315
    t.deepEqual(view.queryAllByHook('nothing'), [], 'should return empty array if no results found');
316
    t.end();
317
});
318
 
319
test('throw on multiple root elements', function (t) {
320
    var View = AmpersandView.extend({
321
        template: '<li></li><div></div>'
322
    });
323
    var view = new View();
324
    t.throws(view.renderWithTemplate, Error, 'Throws error on multiple root elements');
325
    t.end();
326
});
327
 
328
test('queryAll should return an array', function (t) {
329
    var View = AmpersandView.extend({
330
        autoRender: true,
331
        template: '<ul><li></li><li></li><li></li></ul>'
332
    });
333
    var view = new View();
334
    var all = view.queryAll('li');
335
    t.ok(all instanceof Array);
336
    t.ok(all.forEach);
337
    t.equal(all.length, 3);
338
    t.end();
339
});
340
 
341
test('get should return undefined if no match', function (t) {
342
    var View = AmpersandView.extend({
343
        autoRender: true,
344
        template: '<ul></ul>'
345
    });
346
    var view = new View();
347
    var el = view.query('div');
348
    t.equal(typeof el, 'undefined');
349
    t.strictEqual(view.query(''), view.el);
350
    t.strictEqual(view.query(), view.el);
351
    t.strictEqual(view.query(view.el), view.el);
352
    t.end();
353
});
354
 
355
test('get should work for root element too', function (t) {
356
    var View = AmpersandView.extend({
357
        autoRender: true,
358
        template: '<ul></ul>'
359
    });
360
    var view = new View();
361
    t.equal(view.query('ul'), view.el);
362
    t.end();
363
});
364
 
365
test('queryAll should include root element if matches', function (t) {
366
    var View = AmpersandView.extend({
367
        autoRender: true,
368
        template: '<div class="test"><div class="test deep"><div class="test deep"></div></div></div>'
369
    });
370
    var view = new View();
371
    var hasTestClass = view.queryAll('.test');
372
    var hasDeepClass = view.queryAll('.deep');
373
    t.equal(hasTestClass.length, 3);
374
    t.equal(hasDeepClass.length, 2);
375
    t.ok(hasTestClass instanceof Array);
376
    t.ok(hasDeepClass instanceof Array);
377
    t.ok(view.queryAll('bogus') instanceof Array);
378
    t.end();
379
});
380
 
381
 
382
//test('focus/blur events should work in events hash. Issue #8', function (t) {
383
//    t.plan(2);
384
//    var View = AmpersandView.extend({
385
//        events: {
386
//            'focus #thing': 'handleFocus',
387
//            'blur #thing': 'handleBlur'
388
//        },
389
//        autoRender: true,
390
//        template: '<div><input id="thing"></div></div>',
391
//        handleFocus: function () {
392
//            t.pass('focus called');
393
//        },
394
//        handleBlur: function () {
395
//            t.pass('blur called');
396
//            t.end();
397
//        }
398
//    });
399
//    var view = new View();
400
//    // should be able to do this without
401
//    // ending up with too many handlers
402
//    view.delegateEvents();
403
//    view.delegateEvents();
404
//    view.delegateEvents();
405
//
406
//    document.body.appendChild(view.el);
407
//    view.el.firstChild.focus();
408
//    view.el.firstChild.blur();
409
//    document.body.removeChild(view.el);
410
//});
411
 
412
test('ability to mix in state properties', function (t) {
413
    var View = AmpersandView.extend({
414
        template: '<div></div>',
415
        render: function () {
416
            this.el = document.createElement('div');
417
        }
418
    });
419
    var view = new View();
420
    view.on('change:el', function () {
421
        t.pass('woohoo!');
422
        t.end();
423
    });
424
    view.render();
425
});
426
 
427
test('Ability to add other state properties', function (t) {
428
    var View = AmpersandView.extend({
429
        props: {
430
            thing: 'boolean'
431
        },
432
        template: '<div></div>'
433
    });
434
    var view = new View();
435
    view.on('change:thing', function () {
436
        t.pass('woohoo!');
437
        t.end();
438
    });
439
    view.thing = true;
440
});
441
 
442
test('Multi-inheritance of state properties works too', function (t) {
443
    t.plan(2);
444
    var View = AmpersandView.extend({
445
        props: {
446
            thing: 'boolean'
447
        },
448
        template: '<div></div>'
449
    });
450
    var SecondView = View.extend({
451
        props: {
452
            otherThing: 'boolean'
453
        }
454
    });
455
    var view = window.view = new SecondView();
456
    view.on('change:thing', function () {
457
        t.pass('woohoo!');
458
    });
459
    view.on('change:otherThing', function () {
460
        t.pass('woohoo!');
461
        t.end();
462
    });
463
    view.thing = true;
464
    view.otherThing = true;
465
});
466
 
467
test('Setting an `el` should only fire change if new instance of element', function (t) {
468
    t.plan(1);
469
    var View = AmpersandView.extend({
470
        template: '<div></div>',
471
        autoRender: true
472
    });
473
    var view = new View();
474
    t.ok(view.el);
475
    t.once('change:el', function () {
476
        t.pass('this should fire');
477
    });
478
    t.el = document.createElement('div');
479
    t.once('change:el', function () {
480
        t.fail('this should *not* fire');
481
    });
482
    var el = t.el;
483
    el.innerHTML = '<span></span>';
484
    t.el = el;
485
    t.end();
486
});
487
 
488
test('Should be able to bind multiple models in bindings hash', function (t) {
489
    var Person = Model.extend({
490
        props: {
491
            name: 'string'
492
        }
493
    });
494
    var View = AmpersandView.extend({
495
        template: '<div><span id="model1"></span><span id="model2"></span></div>',
496
        autoRender: true,
497
        props: {
498
            model1: 'model',
499
            model2: 'model'
500
        },
501
        bindings: {
502
            'model1.name': '#model1',
503
            'model2.name': {
504
                type: 'class',
505
                selector: '#model2'
506
            }
507
        }
508
    });
509
    var view = new View({
510
        model1: new Person({name: 'henrik'}),
511
        model2: new Person({name: 'larry'})
512
    });
513
    t.equal(view.el.firstChild.textContent, 'henrik');
514
    t.equal(view.el.children[1].className.trim(), 'larry');
515
    t.end();
516
});
517
 
518
test('Should be able to declare bindings first, before model is added', function (t) {
519
    var Person = Model.extend({props: {name: 'string'}});
520
    var View = AmpersandView.extend({
521
        template: '<div></div>',
522
        autoRender: true,
523
        bindings: {
524
            'model.name': ''
525
        }
526
    });
527
    var view = new View();
528
    t.equal(view.el.textContent, '');
529
    view.model = new Person({name: 'henrik'});
530
    t.equal(view.el.textContent, 'henrik');
531
    view.model.name = 'something new';
532
    t.equal(view.el.textContent, 'something new');
533
    t.end();
534
});
535
 
536
test('Should be able to swap out models and bindings should still work', function (t) {
537
    var Person = Model.extend({props: {name: 'string'}});
538
    var View = AmpersandView.extend({
539
        template: '<div></div>',
540
        autoRender: true,
541
        bindings: {
542
            'model.name': ''
543
        }
544
    });
545
    var p1 = new Person({name: 'first'});
546
    var p2 = new Person({name: 'second'});
547
    var view = new View();
548
    t.equal(view.el.textContent, '');
549
    view.model = p1;
550
    t.equal(view.el.textContent, 'first');
551
    view.model = p2;
552
    t.equal(view.el.textContent, 'second');
553
    // make sure it's not still bound to first
554
    p1.name = 'third';
555
    t.equal(view.el.textContent, 'second');
556
    t.end();
557
});
558
 
559
test('Should be able to re-render and maintain bindings', function (t) {
560
    var Person = Model.extend({props: {name: 'string'}});
561
    var View = AmpersandView.extend({
562
        template: '<div></div>',
563
        autoRender: true,
564
        bindings: {
565
            'model.name': ''
566
        }
567
    });
568
    var p1 = new Person({name: 'first'});
569
    var view = new View({model: p1});
570
    var el1 = view.el;
571
    t.equal(view.el.textContent, 'first');
572
    view.renderWithTemplate();
573
    var el2 = view.el;
574
    t.ok(el1 !== el2, 'sanity check to make sure it\'s a new element');
575
    t.equal(el2.textContent, 'first', 'new one should have the binding still');
576
    p1.name = 'third';
577
    t.equal(el2.textContent, 'third', 'new element should also get the change');
578
    t.end();
579
});
580
 
581
test('trigger `remove` when view is removed', function (t) {
582
    var View = AmpersandView.extend({
583
        template: '<div></div>',
584
        autoRender: true
585
    });
586
    var view = new View();
587
    view.on('remove', function () {
588
        t.pass('remove fired');
589
        t.end();
590
    });
591
    view.remove();
592
});
593
 
594
test('declarative subViews basics', function (t) {
595
    var Sub = AmpersandView.extend({
596
        template: '<span></span>'
597
    });
598
 
599
    var View = AmpersandView.extend({
600
        template: '<div><div class="container"></div></div>',
601
        autoRender: true,
602
        subviews: {
603
            sub1: {
604
                container: '.container',
605
                constructor: Sub
606
            }
607
        }
608
    });
609
    var view = new View();
610
 
611
    t.equal(view.el.innerHTML, '<span></span>');
612
 
613
    t.end();
614
});
615
 
616
test('subview hook can include special characters', function (t) {
617
    var Sub = AmpersandView.extend({
618
        template: '<span></span>'
619
    });
620
 
621
    var View = AmpersandView.extend({
622
        template: '<div><div data-hook="test.hi-there"></div></div>',
623
        autoRender: true,
624
        subviews: {
625
            sub1: {
626
                hook: 'test.hi-there',
627
                constructor: Sub
628
            }
629
        }
630
    });
631
    var view = new View();
632
 
633
    t.equal(view.el.innerHTML, '<span></span>');
634
 
635
    t.end();
636
});
637
 
638
test('make sure subviews dont fire until their `waitFor` is done', function (t) {
639
    var Sub = AmpersandView.extend({
640
        template: '<span>yes</span>'
641
    });
642
 
643
    var View = AmpersandView.extend({
644
        template: '<div><span class="container"></span><span data-hook="sub"></span></div>',
645
        autoRender: true,
646
        props: {
647
            model2: 'state'
648
        },
649
        subviews: {
650
            sub1: {
651
                waitFor: 'model',
652
                container: '.container',
653
                constructor: Sub
654
            },
655
            sub2: {
656
                waitFor: 'model2',
657
                hook: 'sub',
658
                constructor: Sub
659
            }
660
        }
661
    });
662
    var view = new View();
663
    t.equal(view._events.change.length, 2);
664
    t.equal(view.el.outerHTML, '<div><span class="container"></span><span data-hook="sub"></span></div>');
665
    view.model = new Model();
666
    t.equal(view._events.change.length, 1);
667
    t.equal(view.el.outerHTML, '<div><span>yes</span><span data-hook="sub"></span></div>');
668
    view.model2 = new Model();
669
    t.equal(view.el.outerHTML, '<div><span>yes</span><span>yes</span></div>');
670
    t.notOk(view._events.change);
671
 
672
    t.end();
673
});
674
 
675
test('make sure template can return a dom node', function (t) {
676
    var Sub = AmpersandView.extend({
677
        template: function () {
678
            return document.createElement('div');
679
        }
680
    });
681
 
682
    var view = new Sub();
683
    view.render();
684
 
685
    t.end();
686
});
687
 
688
test('template can be passed as viewOption', function (t) {
689
    t.plan(1);
690
 
691
    var View = AmpersandView.extend({
692
        autoRender: true
693
    });
694
 
695
    var view = new View({
696
        template: '<span></span>'
697
    });
698
 
699
    t.equal(view.el.outerHTML, '<span></span>');
700
 
701
    t.end();
702
});
703
 
704
test('events are bound if there is an el in the constructor', function (t) {
705
    t.plan(1);
706
    var event = document.createEvent("MouseEvent");
707
    var View = AmpersandView.extend({
708
        template: function () {
709
            return document.createElement('div');
710
        },
711
        events: {
712
            'click div': 'divClicked'
713
        },
714
        divClicked: function (e) {
715
            t.ok(true, 'event fired');
716
            t.end();
717
        }
718
    });
719
    var view = new View({el: document.createElement('div')});
720
    event.initMouseEvent('click');
721
    view.el.dispatchEvent(event);
722
});