1 | var test = require('tape');
|
2 | var mixins = require('ampersand-collection-underscore-mixin');
|
3 | var Collection = require('ampersand-collection').extend(mixins);
|
4 | var SubCollection = require('../ampersand-subcollection');
|
5 | var Model = require('ampersand-state');
|
6 | var _ = require('underscore');
|
7 |
|
8 |
|
9 |
|
10 | var Widget = Model.extend({
|
11 | props: {
|
12 | id: 'number',
|
13 | name: 'string',
|
14 | awesomeness: 'number',
|
15 | sweet: 'boolean'
|
16 | }
|
17 | });
|
18 |
|
19 |
|
20 | var Widgets = Collection.extend(mixins, {
|
21 | model: Widget,
|
22 | comparator: 'awesomeness'
|
23 | });
|
24 |
|
25 |
|
26 | function getBaseCollection() {
|
27 | var widgets = new Widgets();
|
28 |
|
29 |
|
30 | var items = 100;
|
31 | while (items--) {
|
32 | widgets.add({
|
33 | id: items,
|
34 | name: 'abcdefghij'.split('')[items % 10],
|
35 | awesomeness: (items % 10),
|
36 | sweet: (items % 2 === 0)
|
37 | });
|
38 | }
|
39 | return widgets;
|
40 | }
|
41 |
|
42 | test('basic init, length', function (t) {
|
43 | var base = getBaseCollection();
|
44 | var sub = new SubCollection(base);
|
45 | t.equal(sub.length, 100);
|
46 | t.end();
|
47 | });
|
48 |
|
49 | test('it should report as being a collection', function (t) {
|
50 | var base = getBaseCollection();
|
51 | var sub = new SubCollection(base);
|
52 | t.ok(sub.isCollection);
|
53 |
|
54 | sub.isCollection = false;
|
55 | t.ok(sub.isCollection);
|
56 | t.end();
|
57 | });
|
58 |
|
59 | test('basic `where` filtering', function (t) {
|
60 | var base = getBaseCollection();
|
61 | var sub = new SubCollection(base, {
|
62 | where: {
|
63 | sweet: true
|
64 | }
|
65 | });
|
66 | t.equal(sub.length, 50);
|
67 | t.end();
|
68 | });
|
69 |
|
70 | test('add a filter after init', function (t) {
|
71 | var base = getBaseCollection();
|
72 | var sub = new SubCollection(base);
|
73 | sub.addFilter(function (model) {
|
74 | return model.awesomeness === 5;
|
75 | });
|
76 |
|
77 | t.equal(sub.length, 10);
|
78 | t.end();
|
79 | });
|
80 |
|
81 | test('remove a filter', function (t) {
|
82 | var base = getBaseCollection();
|
83 | var sub = new SubCollection(base);
|
84 | var isPartiallyAwesome = function (model) {
|
85 | return model.awesomeness === 5;
|
86 | };
|
87 |
|
88 | sub.addFilter(isPartiallyAwesome);
|
89 | t.equal(sub.length, 10);
|
90 |
|
91 | sub.removeFilter(isPartiallyAwesome);
|
92 | t.equal(sub.length, 100);
|
93 |
|
94 | t.end();
|
95 | });
|
96 |
|
97 | test('swap filters', function (t) {
|
98 | var base = getBaseCollection();
|
99 | var sub = new SubCollection(base);
|
100 | var isPartiallyAwesome = function (model) {
|
101 | return model.awesomeness === 5;
|
102 | };
|
103 | var isSweet = function (model) {
|
104 | return model.sweet;
|
105 | };
|
106 |
|
107 | sub.addFilter(isPartiallyAwesome);
|
108 | t.equal(sub.length, 10);
|
109 |
|
110 | sub.swapFilters(isSweet, isPartiallyAwesome);
|
111 | t.equal(sub.length, 50);
|
112 |
|
113 | t.end();
|
114 | });
|
115 |
|
116 | test('swap all filters', function (t) {
|
117 | var base = getBaseCollection();
|
118 | var sub = new SubCollection(base);
|
119 | var isPartiallyAwesome = function (model) {
|
120 | return model.awesomeness === 5;
|
121 | };
|
122 | var isSweet = function (model) {
|
123 | return model.sweet;
|
124 | };
|
125 |
|
126 | sub.addFilter(isPartiallyAwesome);
|
127 | t.equal(sub.length, 10);
|
128 |
|
129 | sub.swapFilters(isSweet);
|
130 | t.equal(sub.length, 50);
|
131 |
|
132 | t.end();
|
133 | });
|
134 |
|
135 | test('function based filtering', function (t) {
|
136 | var base = getBaseCollection();
|
137 | var sub = new SubCollection(base, {
|
138 | filter: function (model) {
|
139 | return model.awesomeness > 5;
|
140 | }
|
141 | });
|
142 | t.ok(sub.length > 0, 'should have some that match');
|
143 | t.ok(sub.length < 100, 'but not all');
|
144 | t.end();
|
145 | });
|
146 |
|
147 | test('multiple filter functions', function (t) {
|
148 | var base = getBaseCollection();
|
149 | var sub = new SubCollection(base, {
|
150 | filters: [
|
151 | function (model) {
|
152 | return model.awesomeness > 5;
|
153 | },
|
154 | function (model) {
|
155 | return model.name === 'j';
|
156 | }
|
157 | ]
|
158 | });
|
159 | t.equal(sub.length, 10);
|
160 | t.end();
|
161 | });
|
162 |
|
163 | test('mixed filter and `where`', function (t) {
|
164 | var base = getBaseCollection();
|
165 | var sub = new SubCollection(base, {
|
166 | filter: function (model) {
|
167 | return model.awesomeness > 5;
|
168 | },
|
169 | where: {
|
170 | name: 'j'
|
171 | }
|
172 | });
|
173 | t.equal(sub.length, 10);
|
174 | t.end();
|
175 | });
|
176 |
|
177 | test('should sort independent of base', function (t) {
|
178 | var base = getBaseCollection();
|
179 | var sub = new SubCollection(base, {
|
180 | comparator: 'id'
|
181 | });
|
182 | t.equal(sub.length, 100);
|
183 | t.notEqual(sub.at(0), base.at(0));
|
184 | t.end();
|
185 | });
|
186 |
|
187 | test('should be able to specify/update offset and limit', function (t) {
|
188 | var base = getBaseCollection();
|
189 | var sub = new SubCollection(base, {
|
190 | comparator: 'id',
|
191 | limit: 10
|
192 | });
|
193 | t.equal(sub.length, 10);
|
194 | t.equal(sub.at(0).id, 0);
|
195 | sub.configure({limit: 5});
|
196 | t.equal(sub.length, 5);
|
197 | sub.configure({offset: 5});
|
198 | t.equal(sub.at(0).id, 5);
|
199 | sub.configure({offset: null});
|
200 | sub.configure({limit: null});
|
201 | t.equal(sub.length, 100);
|
202 | t.end();
|
203 | });
|
204 |
|
205 | test('should fire `add` events only if removed items match filter', function (t) {
|
206 | t.plan(1);
|
207 | var base = getBaseCollection();
|
208 | var sub = new SubCollection(base, {
|
209 | filter: function (model) {
|
210 | return model.awesomeness > 5;
|
211 | }
|
212 | });
|
213 | var awesomeWidget = new Widget({
|
214 | name: 'awesome',
|
215 | id: 999,
|
216 | awesomeness: 11,
|
217 | sweet: true
|
218 | });
|
219 | var lameWidget = new Widget({
|
220 | name: 'lame',
|
221 | id: 1000,
|
222 | awesomeness: 0,
|
223 | sweet: false
|
224 | });
|
225 | sub.on('add', function (model) {
|
226 | t.equal(model, awesomeWidget);
|
227 | t.end();
|
228 | });
|
229 | base.add([lameWidget, awesomeWidget]);
|
230 | });
|
231 |
|
232 | test('should fire `remove` events only if removed items match filter', function (t) {
|
233 | t.plan(3);
|
234 | var base = getBaseCollection();
|
235 | var sub = new SubCollection(base, {
|
236 | filter: function (model) {
|
237 | return model.awesomeness > 5;
|
238 | }
|
239 | });
|
240 |
|
241 | var lameWidget = base.find(function (model) {
|
242 | return model.awesomeness < 5;
|
243 | });
|
244 |
|
245 | var awesomeWidget = base.find(function (model) {
|
246 | return model.awesomeness > 5;
|
247 | });
|
248 | sub.on('remove', function (model) {
|
249 | t.equal(model, awesomeWidget);
|
250 | t.end();
|
251 | });
|
252 | t.ok(awesomeWidget);
|
253 | t.ok(lameWidget);
|
254 | base.remove([lameWidget, awesomeWidget]);
|
255 | });
|
256 |
|
257 | test('should fire `add` and `remove` events after models are updated', function (t) {
|
258 | t.plan(2);
|
259 | var base = getBaseCollection();
|
260 | var sub = new SubCollection(base);
|
261 | var awesomeWidget = new Widget({
|
262 | name: 'awesome',
|
263 | id: 999,
|
264 | awesomeness: 11,
|
265 | sweet: true
|
266 | });
|
267 | sub.on('add', function () {
|
268 | t.equal(sub.models.length, 101);
|
269 | });
|
270 | sub.on('remove', function () {
|
271 | t.equal(sub.models.length, 100);
|
272 | t.end();
|
273 | });
|
274 | base.add(awesomeWidget);
|
275 | base.remove(awesomeWidget);
|
276 | });
|
277 |
|
278 | test('make sure changes to `where` properties are reflected in sub collections', function (t) {
|
279 | t.plan(3);
|
280 | var base = getBaseCollection();
|
281 | var sub = new SubCollection(base, {
|
282 | where: {
|
283 | sweet: true
|
284 | }
|
285 | });
|
286 | var firstSweet = sub.first();
|
287 | sub.on('remove', function (model) {
|
288 | t.equal(model, firstSweet);
|
289 | t.equal(firstSweet.sweet, false);
|
290 | t.end();
|
291 | });
|
292 | t.ok(firstSweet);
|
293 | firstSweet.sweet = false;
|
294 | });
|
295 |
|
296 | test('should be able to `get` a model by id or other index', function (t) {
|
297 | var base = getBaseCollection();
|
298 | var sub = new SubCollection(base, {
|
299 | where: {
|
300 | sweet: true
|
301 | }
|
302 | });
|
303 | var cool = sub.first();
|
304 | var lame = base.find(function (model) {
|
305 | return model.sweet === false;
|
306 | });
|
307 |
|
308 | t.ok(cool.sweet);
|
309 | t.notOk(lame.sweet);
|
310 | t.ok(sub.models.indexOf(lame) === -1);
|
311 | t.ok(base.models.indexOf(lame) !== -1);
|
312 | t.notEqual(sub.get(lame.id), lame);
|
313 | t.equal(sub.get(cool.id), cool);
|
314 | t.end();
|
315 | });
|
316 |
|
317 | test('should be able to listen for `change:x` events on subcollection', function (t) {
|
318 | t.plan(1);
|
319 | var base = getBaseCollection();
|
320 | var sub = new SubCollection(base, {
|
321 | where: {
|
322 | sweet: true
|
323 | }
|
324 | });
|
325 | sub.on('change:name', function () {
|
326 | t.pass('handler called');
|
327 | t.end();
|
328 | });
|
329 | var cool = sub.first();
|
330 | cool.name = 'new name';
|
331 | });
|
332 |
|
333 | test('should be able to listen for general `change` events on subcollection', function (t) {
|
334 | t.plan(1);
|
335 | var base = getBaseCollection();
|
336 | var sub = new SubCollection(base, {
|
337 | where: {
|
338 | sweet: true
|
339 | }
|
340 | });
|
341 | sub.on('change', function () {
|
342 | t.pass('handler called');
|
343 | t.end();
|
344 | });
|
345 | var cool = sub.first();
|
346 | cool.name = 'new name';
|
347 | });
|
348 |
|
349 | test('have the correct ordering saved when processing a sort event', function (t) {
|
350 | t.plan(3);
|
351 | var base = getBaseCollection();
|
352 | var sub = new SubCollection(base, {
|
353 | where: {
|
354 | sweet: true
|
355 | },
|
356 | comparator: 'name'
|
357 | });
|
358 |
|
359 | var third = sub.at(42);
|
360 |
|
361 | third.sweet = false;
|
362 |
|
363 | t.notEqual(third.id, sub.at(42).id);
|
364 | t.notOk(sub.get(third.id));
|
365 |
|
366 | sub.on('sort', function () {
|
367 | t.equal(third.id, sub.at(42).id);
|
368 | });
|
369 |
|
370 | third.sweet = true;
|
371 | });
|
372 |
|
373 | test('reset works correctly/efficiently when passed to configure', function (t) {
|
374 | var base = getBaseCollection();
|
375 | var sub = new SubCollection(base, {
|
376 | where: {
|
377 | sweet: true
|
378 | }
|
379 | });
|
380 | var itemsRemoved = [];
|
381 | var itemsAdded = [];
|
382 |
|
383 | t.equal(sub.length, 50, 'should be 50 that match initial filter');
|
384 |
|
385 | sub.on('remove', function (model) {
|
386 | itemsRemoved.push(model);
|
387 | });
|
388 | sub.on('add', function (model) {
|
389 | itemsAdded.push(model);
|
390 | });
|
391 |
|
392 | var oldResetFilters = sub._resetFilters;
|
393 | var resetCalls = [];
|
394 | sub._resetFilters = function (resetComparator) {
|
395 | resetCalls.push(_.toArray(arguments));
|
396 | oldResetFilters.call(this, resetComparator);
|
397 | };
|
398 |
|
399 | sub.configure({
|
400 | where: {
|
401 | sweet: true,
|
402 | awesomeness: 6
|
403 | }
|
404 | }, true);
|
405 |
|
406 | t.same(resetCalls[0], [true], 'configure calls _resetFilters(true) when reset is true');
|
407 | sub._resetFilters = oldResetFilters;
|
408 |
|
409 | t.equal(sub.length, 10, 'should be 10 that match second filter');
|
410 | t.equal(itemsRemoved.length, 40, '10 of the items should have been removed');
|
411 | t.equal(itemsAdded.length, 0, 'nothing should have been added');
|
412 | t.equal(sub.comparator, void 0, 'comparator is reset');
|
413 |
|
414 | t.ok(_.every(itemsRemoved, function (item) {
|
415 | return item.sweet === true && item.awesomeness !== 6;
|
416 | }), 'every item removed should have awesomeness not 6 and be sweet: true');
|
417 |
|
418 | t.end();
|
419 | });
|
420 |
|
421 | test('_watched contains members of spec.watched but is not spec.watched', function (t) {
|
422 | t.plan(2);
|
423 | var watched = ['name', 'awesomeness'];
|
424 | var comparator = 'sweet';
|
425 | var base = getBaseCollection();
|
426 | var sub = new SubCollection(base, {
|
427 | limit: 10,
|
428 | comparator: comparator,
|
429 | watched: watched,
|
430 | filter: function (item) {
|
431 | return item.awesomeness > 5 || item.name > 'd';
|
432 | }
|
433 | });
|
434 |
|
435 | t.notEqual(sub._watched, watched, '_watched should not be the same array as spec.watched');
|
436 | t.same(sub._watched, watched, '_watched should contain spec.watched members');
|
437 | t.end();
|
438 | });
|
439 |
|
440 | test('_resetFilters', function (t) {
|
441 | t.plan(6);
|
442 | var comparator = 'sweet';
|
443 | var where = {
|
444 | awesomeness: 5,
|
445 | name: 'b'
|
446 | };
|
447 | var base = getBaseCollection();
|
448 | var sub = new SubCollection(base, {
|
449 | comparator: comparator,
|
450 | where: where,
|
451 | watched: ['id']
|
452 | });
|
453 |
|
454 | t.same(sub._watched.sort(), _.keys(where).concat('id').sort(), '_watched should contain spec.watched members');
|
455 |
|
456 | sub._resetFilters();
|
457 |
|
458 | t.same(sub._watched, [], '_resetFilters() empties _watched');
|
459 | t.same(sub._filters, [], '_resetFilters() empties _filters');
|
460 | t.same([sub.offset, sub.limit], [void 0, void 0], '_resetFilters() unsets offset and limit');
|
461 | t.equal(sub.comparator, comparator, '_resetFilters() does NOT reset comparator');
|
462 |
|
463 | sub._resetFilters(true);
|
464 |
|
465 | t.equal(sub.comparator, void 0, '_resetFilters(true) resets comparator to undefined');
|
466 |
|
467 | t.end();
|
468 | });
|
469 |
|
470 | test('string comparator causes _runFilters to be called when comparator prop changes on models', function (t) {
|
471 | t.plan(1);
|
472 | var comparator = 'sweet';
|
473 | var where = {
|
474 | awesomeness: 5,
|
475 | name: 'b'
|
476 | };
|
477 | var base = getBaseCollection();
|
478 | var sub = new SubCollection(base, {
|
479 | comparator: comparator,
|
480 | where: where
|
481 | });
|
482 | var filtersRan = 0;
|
483 | sub._runFilters = function () { filtersRan++; };
|
484 | sub.models.forEach(function (model) { model.sweet = !model.sweet; });
|
485 | t.equal(filtersRan, sub.models.length, '_runFilters called once for each model');
|
486 | t.end();
|
487 | });
|
488 |
|
489 | test('reset', function (t) {
|
490 | var base = getBaseCollection();
|
491 | var sub = new SubCollection(base, {
|
492 | where: {
|
493 | sweet: true
|
494 | },
|
495 | comparator: 'id',
|
496 | filter: function (model) {
|
497 | return model.awesomeness === 6;
|
498 | }
|
499 | });
|
500 |
|
501 | var itemsRemoved = [];
|
502 | var itemsAdded = [];
|
503 | var sortTriggered = 0;
|
504 |
|
505 | t.equal(sub.length, 10, 'should be 10 that match initial filters');
|
506 |
|
507 | sub.on('remove', function (model) {
|
508 | itemsRemoved.push(model);
|
509 | });
|
510 | sub.on('add', function (model) {
|
511 | itemsAdded.push(model);
|
512 | });
|
513 | sub.on('sort', function () {
|
514 | sortTriggered++;
|
515 | });
|
516 |
|
517 | sub.reset();
|
518 |
|
519 | t.equal(sub.length, base.length, 'should be same as base length');
|
520 | t.equal(itemsAdded.length, 90, '90 should have been added back');
|
521 | t.equal(itemsRemoved.length, 0, '0 should have been removed');
|
522 |
|
523 | t.deepEqual(sub._watched, [], 'should not be watching any properties');
|
524 | t.equal(sub.comparator, void 0, 'comparator should be undefined');
|
525 | t.equal(sortTriggered, 1, 'should have triggered a `sort`');
|
526 |
|
527 | t.deepEqual(_.pluck(sub.models, 'id'), base.pluck('id'));
|
528 |
|
529 | t.ok(_.every(itemsAdded, function (item) {
|
530 | return item.sweet !== true || item.awesomeness !== 6;
|
531 | }), 'every item added back should either not be sweet or have awesomness of 6');
|
532 |
|
533 | t.end();
|
534 | });
|
535 |
|
536 | test('clear filters', function (t) {
|
537 | var base = getBaseCollection();
|
538 | var sub = new SubCollection(base, {
|
539 | where: {
|
540 | sweet: true
|
541 | },
|
542 | comparator: 'id',
|
543 | filter: function (model) {
|
544 | return model.awesomeness === 6;
|
545 | }
|
546 | });
|
547 |
|
548 | var itemsRemoved = [];
|
549 | var itemsAdded = [];
|
550 | var sortTriggered = 0;
|
551 |
|
552 | t.equal(sub.length, 10, 'should be 10 that match initial filters');
|
553 |
|
554 | sub.on('remove', function (model) {
|
555 | itemsRemoved.push(model);
|
556 | });
|
557 | sub.on('add', function (model) {
|
558 | itemsAdded.push(model);
|
559 | });
|
560 | sub.on('sort', function () {
|
561 | sortTriggered++;
|
562 | });
|
563 |
|
564 | sub.clearFilters();
|
565 |
|
566 | t.equal(sub.length, base.length, 'should be same as base length');
|
567 | t.equal(itemsAdded.length, 90, '90 should have been added back');
|
568 | t.equal(itemsRemoved.length, 0, '0 should have been removed');
|
569 |
|
570 | t.deepEqual(sub._watched, [], 'should not be watching any properties');
|
571 | t.equal(sub.comparator, 'id', 'should still have comparator');
|
572 | t.equal(sortTriggered, 1, 'should trigger `sort` for renderers sake');
|
573 |
|
574 | t.ok(_.every(itemsAdded, function (item) {
|
575 | return item.sweet !== true || item.awesomeness !== 6;
|
576 | }), 'every item added back should either not be sweet or have awesomness of 6');
|
577 |
|
578 | t.end();
|
579 | });
|