1 // ==========================================================================
  2 // Project:   SproutCore Costello - Property Observing Library
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 /*global module, test, ok, isObj, equals, expects */
  8 
  9 
 10 var enumerables; // global variables
 11 var DummyEnumerable = SC.Object.extend(SC.Enumerable, {
 12 
 13   content: [],
 14 
 15   length: function () { return this.content.length; }.property(),
 16 
 17   objectAt: function (idx) { return this.content[idx]; },
 18 
 19   nextObject: function (idx) { return this.content[idx]; },
 20 
 21   // add support for reduced properties.
 22   unknownProperty: function (key, value) {
 23     var ret = this.reducedProperty(key, value);
 24     if (ret === undefined) {
 25       if (value !== undefined) this[key] = value;
 26       ret = value;
 27     }
 28     return ret;
 29   },
 30 
 31   replace: function (start, removed, added) {
 32     var ret = this.content.replace(start, removed, added),
 33       addedLength = added ? added.length : 0;
 34 
 35     this.enumerableContentDidChange(start, addedLength, addedLength - removed);
 36     return ret;
 37   },
 38 
 39   unshiftObject: function (object) {
 40     this.replace(0, 0, [object]);
 41     return object;
 42   },
 43 
 44   shiftObject: function () {
 45     var ret = this.replace(0, 1);
 46     return ret;
 47   },
 48 
 49   pushObject: function (object) {
 50     this.replace(this.content.length - 1, 0, [object]);
 51     return object;
 52   },
 53 
 54   popObject: function () {
 55     var ret = this.replace(this.content.length - 1, 1);
 56     return ret;
 57   }
 58 
 59 });
 60 
 61 var runFunc = function (a, b) { return ['DONE', a, b]; };
 62 var invokeWhileOK = function () { return "OK"; };
 63 var invokeWhileNotOK = function () { return "FAIL"; };
 64 var reduceTestFunc = function (prev, item, idx, e, pname) { return pname || 'TEST'; };
 65 
 66 var CommonArray = [
 67   {
 68     first: "Charles",
 69     gender: "male",
 70     californian: NO,
 71     ready: YES,
 72     visited: "Prague",
 73     doneTravelling: NO,
 74     run: runFunc,
 75     invokeWhileTest: invokeWhileOK,
 76     balance: 1
 77   },
 78 
 79   {
 80     first: "Jenna",
 81     gender: "female",
 82     californian: YES,
 83     ready: YES,
 84     visited: "Prague",
 85     doneTravelling: NO,
 86     run: runFunc,
 87     invokeWhileTest: invokeWhileOK,
 88     balance: 2
 89   },
 90 
 91   {
 92     first: "Peter",
 93     gender: "male",
 94     californian: NO,
 95     ready: YES,
 96     visited: "Prague",
 97     doneTravelling: NO,
 98     run: runFunc,
 99     invokeWhileTest: invokeWhileNotOK,
100     balance: 3
101   },
102 
103   {
104     first: "Chris",
105     gender: "male",
106     californian: NO,
107     ready: YES,
108     visited: "Prague",
109     doneTravelling: NO,
110     run: runFunc,
111     invokeWhileTest: invokeWhileOK,
112     balance: 4
113   }
114 ];
115 
116 module("Real Array & DummyEnumerable", {
117 
118   setup: function () {
119     enumerables = [SC.$A(CommonArray), DummyEnumerable.create({ content: SC.clone(CommonArray) })];
120   },
121 
122   teardown: function () {
123     enumerables = null;
124     delete Array.prototype["@max(balance)"]; // remove cached value
125     delete Array.prototype["@min(balance)"];
126   }
127 
128 });
129 
130 test("should get enumerator that iterates through objects", function () {
131   var src, ary2 = enumerables;
132   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
133     src = ary2[idx2];
134     var e = src.enumerator();
135     ok(e !== null, 'enumerator must not be null');
136 
137     var idx = 0;
138     var cur;
139     while(cur = e.nextObject()) {
140       equals(src.objectAt(idx), cur, "object at index %@".fmt(idx));
141       idx++;
142     }
143 
144     equals(src.get('length'), idx);
145   }
146 });
147 
148 test("should return firstObject for item with content", function () {
149   var src, ary2 = enumerables;
150   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
151     src = ary2[idx2];
152     equals(src.firstObject(), CommonArray[0], 'firstObject should return first object');
153   }
154 
155   equals([].firstObject(), undefined, 'firstObject() on empty enumerable should return undefined');
156 });
157 
158 test("should run forEach() to go through objects", function () {
159   var src, ary2 = enumerables;
160   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
161     src = ary2[idx2];
162     var idx = 0;
163 
164     // save for testing later
165     var items = [];
166     var indexes = [];
167     var arrays = [];
168     var targets = [];
169 
170     src.forEach(function (item, index, array) {
171       items.push(item);
172       indexes.push(index);
173       arrays.push(array);
174       targets.push(this);
175     }, this);
176 
177     var len = src.get('length');
178     for(idx=0;idx<len;idx++) {
179       equals(items[idx], src.objectAt(idx));
180       equals(indexes[idx], idx);
181       equals(arrays[idx], src);
182 
183       // use this method because equals() is taking too much time to log out
184       // results.  probably an issue with jsDump
185       ok(targets[idx] === this, 'target should always be this');
186     }
187   }
188 });
189 
190 test("should map to values while passing proper params", function () {
191   var src, ary2 = enumerables;
192   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
193     src = ary2[idx2];
194     var idx = 0;
195 
196     // save for testing later
197     var items = [];
198     var indexes = [];
199     var arrays = [];
200     var targets = [];
201 
202     var mapped = src.map(function (item, index, array) {
203       items.push(item);
204       indexes.push(index);
205       arrays.push(array);
206       targets.push(this);
207 
208       return index;
209     }, this);
210 
211     var len = src.get('length');
212     for(idx=0;idx<len;idx++) {
213       equals(src.objectAt(idx), items[idx], "items");
214       equals(idx, indexes[idx], "indexes");
215       equals(src, arrays[idx], 'arrays');
216       equals(SC.guidFor(this), SC.guidFor(targets[idx]), "this");
217 
218       equals(idx, mapped[idx], "mapped");
219     }
220   }
221 });
222 
223 test("should filter to items that return for callback", function () {
224   var src, ary2 = enumerables;
225   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
226     src = ary2[idx2];
227     var idx = 0;
228 
229     // save for testing later
230     var items = [];
231     var indexes = [];
232     var arrays = [];
233     var targets = [];
234 
235     var filtered = src.filter(function (item, index, array) {
236       items.push(item);
237       indexes.push(index);
238       arrays.push(array);
239       targets.push(this);
240 
241       return item.gender === "female";
242     }, this);
243 
244     var len = src.get('length');
245     for(idx=0;idx<len;idx++) {
246       equals(src.objectAt(idx), items[idx], "items");
247       equals(idx, indexes[idx], "indexes");
248       equals(src, arrays[idx], 'arrays');
249       equals(SC.guidFor(this), SC.guidFor(targets[idx]), "this");
250     }
251 
252     equals(filtered.length, 1);
253     equals(filtered[0].first, "Jenna");
254   }
255 });
256 
257 test("should return true if function for every() returns true", function () {
258   var src, ary2 = enumerables;
259   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
260     src = ary2[idx2];
261     var idx = 0;
262 
263     // save for testing later
264     var items = [];
265     var indexes = [];
266     var arrays = [];
267     var targets = [];
268 
269     var result = src.every(function (item, index, array) {
270       items.push(item);
271       indexes.push(index);
272       arrays.push(array);
273       targets.push(this);
274 
275       return true;
276     }, this);
277 
278     var len = src.get('length');
279     for(idx=0;idx<len;idx++) {
280       equals(src.objectAt(idx), items[idx], "items");
281       equals(idx, indexes[idx], "indexes");
282       equals(src, arrays[idx], 'arrays');
283       equals(SC.guidFor(this), SC.guidFor(targets[idx]), "this");
284     }
285 
286     equals(result, YES);
287   }
288 });
289 
290 test("should return false if one function for every() returns false", function () {
291   var src, ary2 = enumerables;
292   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
293     src = ary2[idx2];
294     var result = src.every(function (item, index, array) {
295       return item.gender === "male";
296     }, this);
297     equals(result, NO);
298   }
299 });
300 
301 test("should return false if all functions for some() returns false", function () {
302   var src, ary2 = enumerables;
303   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
304     src = ary2[idx2];
305     var idx = 0;
306 
307     // save for testing later
308     var items = [];
309     var indexes = [];
310     var arrays = [];
311     var targets = [];
312 
313     var result = src.some(function (item, index, array) {
314       items.push(item);
315       indexes.push(index);
316       arrays.push(array);
317       targets.push(this);
318 
319       return false;
320     }, this);
321 
322     var len = src.get('length');
323     for(idx=0;idx<len;idx++) {
324       equals(src.objectAt(idx), items[idx], "items");
325       equals(idx, indexes[idx], "indexes");
326       equals(src, arrays[idx], 'arrays');
327       equals(SC.guidFor(this), SC.guidFor(targets[idx]), "this");
328     }
329 
330     equals(result, NO);
331   }
332 });
333 
334 test("should return true if one function for some() returns true", function () {
335   var src, ary2 = enumerables;
336   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
337     src = ary2[idx2];
338     var result = src.some(function (item, index, array) {
339       return item.gender !== "male";
340     }, this);
341     equals(result, YES);
342   }
343 });
344 
345 test("should mapProperty for all items", function () {
346   var src, ary2 = enumerables;
347   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
348     src = ary2[idx2];
349     var mapped = src.mapProperty("first");
350     var idx;
351     var len = src.get('length');
352     for(idx=0;idx<len;idx++) {
353       equals(mapped[idx], src.objectAt(idx).first);
354     }
355   }
356 });
357 
358 test("should filterProperty with match", function () {
359   var src, ary2 = enumerables;
360   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
361     src = ary2[idx2];
362     var filtered = src.filterProperty("gender", "female");
363     equals(filtered.length, 1);
364     equals(filtered[0].first, "Jenna");
365   }
366 });
367 
368 test("should filterProperty with default bool", function () {
369   var src, ary2 = enumerables;
370   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
371     src = ary2[idx2];
372     var filtered = src.filterProperty("californian");
373     equals(filtered.length, 1);
374     equals(filtered[0].first, "Jenna");
375   }
376 });
377 
378 test("should groupBy a given property", function () {
379   var src, ary2 = enumerables;
380   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
381     src = ary2[idx2];
382     var filtered = src.groupBy("gender");
383     equals(filtered.length, 2);
384     equals(filtered[1][0].first, "Jenna");
385   }
386 });
387 
388 
389 test("everyProperty should return true if all properties macth", function () {
390   var src, ary2 = enumerables;
391   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
392     src = ary2[idx2];
393     var ret = src.everyProperty('visited', 'Prague');
394     equals(YES, ret, "visited");
395   }
396 });
397 
398 test("everyProperty should return true if all properties true", function () {
399   var src, ary2 = enumerables;
400   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
401     src = ary2[idx2];
402     var ret = src.everyProperty('ready');
403     equals(YES, ret, "ready");
404   }
405 });
406 
407 test("everyProperty should return false if any properties false", function () {
408   var src, ary2 = enumerables;
409   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
410     src = ary2[idx2];
411     var ret = src.everyProperty('gender', 'male');
412     equals(NO, ret, "ready");
413   }
414 });
415 
416 test("someProperty should return false if all properties not match", function () {
417   var src, ary2 = enumerables;
418   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
419     src = ary2[idx2];
420     var ret = src.someProperty('visited', 'Timbuktu');
421     equals(NO, ret, "visited");
422   }
423 });
424 
425 test("someProperty should return false if all properties false", function () {
426   var src, ary2 = enumerables;
427   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
428     src = ary2[idx2];
429     var ret = src.someProperty('doneTravelling');
430     equals(NO, ret, "doneTravelling");
431   }
432 });
433 
434 test("someProperty should return true if any properties true", function () {
435   var src, ary2 = enumerables;
436   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
437     src = ary2[idx2];
438     var ret = src.someProperty('first', 'Charles');
439     equals(YES, ret, "first");
440   }
441 });
442 
443 test("invokeWhile should call method on member objects until return does not match", function () {
444   var src, ary2 = enumerables;
445   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
446     src = ary2[idx2];
447     var ret = src.invokeWhile("OK", "invokeWhileTest", "item2");
448     equals("FAIL", ret, "return value");
449   }
450 });
451 
452 test("get @min(balance) should return the minimum balance", function () {
453   var src, ary2 = enumerables;
454   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
455     src = ary2[idx2];
456     equals(1, src.get('@min(balance)'));
457   }
458 });
459 
460 test("get @max(balance) should return the maximum balance", function () {
461   var src, ary2 = enumerables;
462   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
463     src = ary2[idx2];
464     equals(4, src.get('@max(balance)'));
465   }
466 });
467 
468 test("get @minObject(balance) should return the record with min balance", function () {
469   var src, ary2 = enumerables;
470   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
471     src = ary2[idx2];
472     equals(src.objectAt(0), src.get('@minObject(balance)'));
473   }
474 });
475 
476 test("get @maxObject(balance) should return the record with the max balance", function () {
477   var src, ary2 = enumerables;
478   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
479     src = ary2[idx2];
480     equals(src.objectAt(3), src.get('@maxObject(balance)'));
481   }
482 });
483 
484 test("get @sum(balance) should return the sum of the balances.", function () {
485   var src, ary2 = enumerables;
486   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
487     src = ary2[idx2];
488     equals(1+2+3+4, src.get("@sum(balance)"));
489   }
490 });
491 
492 test("get @average(balance) should return the average of balances", function () {
493   var src, ary2 = enumerables;
494   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
495     src = ary2[idx2];
496     equals((1+2+3+4)/4, src.get("@average(balance)"));
497   }
498 });
499 
500 test("should invoke custom reducer", function () {
501   var src, ary2 = enumerables;
502   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
503     src = ary2[idx2];
504     // install reducer method
505     src.reduceTest = reduceTestFunc;
506     equals("TEST", src.get("@test"));
507     equals("prop", src.get("@test(prop)"));
508   }
509 });
510 
511 test("Should trigger observer on lastObject property when it changes", function () {
512 
513   // Perform tests on each sample enumerable in enumerables.
514   for (var i = 0, len = enumerables.length; i < len; i++) {
515     var enumerable = enumerables[i],
516       enumerableLength = enumerable.get('length'),
517       callCount = 0,
518       testObject = {
519         first: "John",
520       };
521 
522     // Observe the enumerable for updates to `lastObject`.
523     enumerable.addObserver("lastObject", function () {
524       callCount++;
525     });
526 
527     // Inserting an item in the middle doesn't change lastObject.
528     enumerable.replace(1, 0, [testObject]);
529     equals(callCount, 0, "The lastObject observer should have fired this many times (replace on enumerable %@)".fmt(i + 1));
530 
531     // Removing an item in the middle doesn't change lastObject.
532     enumerable.replace(1, 1);
533     equals(callCount, 0, "The lastObject observer should have fired this many times (replace on enumerable %@)".fmt(i + 1));
534 
535     // Shifting an item to the front doesn't change lastObject.
536     enumerable.shiftObject(testObject);
537     equals(callCount, 0, "The lastObject observer should have fired this many times (shiftObject on enumerable %@)".fmt(i + 1));
538 
539     // Unshifting an item from the front doesn't change lastObject.
540     enumerable.unshiftObject(testObject);
541     equals(callCount, 0, "The lastObject observer should have fired this many times (unshiftObject on enumerable %@)".fmt(i + 1));
542 
543     // Appending an item to the end changes the lastObject.
544     enumerable.pushObject(testObject);
545     equals(callCount, 1, "The lastObject observer should have fired this many times (pushObject on enumerable %@)".fmt(i + 1));
546 
547     // Popping an item from the end changes the lastObject.
548     enumerable.popObject();
549     equals(callCount, 2, "The lastObject observer should have fired this many times (popObject on enumerable %@)".fmt(i + 1));
550 
551     // Replacing only the last item changes the lastObject.
552     enumerable.replace(enumerable.get('length') - 1, 1, [testObject]);
553     equals(callCount, 3, "The lastObject observer should have fired this many times (replace on enumerable %@)".fmt(i + 1));
554 
555     // Replacing the last two items with one greater number changes the lastObject.
556     enumerable.replace(enumerable.get('length') - 2, 2, [testObject, testObject, testObject]);
557     equals(callCount, 4, "The lastObject observer should have fired this many times (replace on enumerable %@)".fmt(i + 1));
558 
559     // Replacing the last two items with same number changes the lastObject.
560     enumerable.replace(enumerable.get('length') - 2, 2, [testObject, testObject]);
561     equals(callCount, 5, "The lastObject observer should have fired this many times (replace on enumerable %@)".fmt(i + 1));
562 
563     // Replacing the last two items with one fewer number changes the lastObject.
564     enumerable.replace(enumerable.get('length') - 2, 2, [testObject]);
565     equals(callCount, 6, "The lastObject observer should have fired this many times (replace on enumerable %@)".fmt(i + 1));
566 
567     // Replacing the last two items with two fewer number changes the lastObject.
568     enumerable.replace(enumerable.get('length') - 2, 2);
569     equals(callCount, 7, "The lastObject observer should have fired this many times (replace on enumerable %@)".fmt(i + 1));
570   }
571 });
572 
573 test("should trigger observer on property when firstObject changes", function () {
574   var src, ary2 = enumerables;
575   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
576     src = ary2[idx2];
577 
578     var callCount = 0;
579     src.addObserver("firstObject", function () {
580       callCount++;
581     });
582 
583     src.shiftObject();
584 
585     equals(callCount, 1, "callCount");
586   }
587 });
588 
589 test("should trigger observer of reduced prop when array changes once property retrieved once", function () {
590   var src, ary2 = enumerables;
591   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
592     src = ary2[idx2];
593     // get the property...this will install the reducer property...
594     src.get("@max(balance)");
595 
596     // install observer
597     var observedValue = null;
598     src.addObserver("@max(balance)", function () {
599       observedValue = src.get("@max(balance)");
600     });
601 
602     //src.addProbe('[]');
603     //src.addProbe('@max(balance)');
604 
605     // add record to array
606     src.pushObject({
607       first: "John",
608       gender: "male",
609       californian: NO,
610       ready: YES,
611       visited: "Paris",
612       balance: 5
613     });
614 
615     //SC.NotificationQueue.flush(); // force observers to trigger
616 
617     // observed value should now be set because the reduced property observer
618     // was triggered when we changed the array contents.
619     equals(5, observedValue, "observedValue");
620   }
621 });
622 
623 
624 test("should trigger observer of reduced prop when array changes - even if you never retrieved the property before", function () {
625   var src, ary2 = enumerables;
626   for (var idx2=0, len2=ary2.length; idx2<len2; idx2++) {
627     src = ary2[idx2];
628     // install observer
629     var observedValue = null;
630     src.addObserver("@max(balance)", function () {
631       observedValue = src.get("@max(balance)");
632     });
633 
634     // add record to array
635     src.pushObject({
636       first: "John",
637       gender: "male",
638       californian: NO,
639       ready: YES,
640       visited: "Paris",
641       balance: 5
642     });
643 
644     //SC.NotificationQueue.flush(); // force observers to trigger
645 
646     // observed value should now be set because the reduced property observer
647     // was triggered when we changed the array contents.
648     equals(5, observedValue, "observedValue");
649   }
650 });
651 
652 test("should find the first element matching the criteria", function () {
653   var people = enumerables[1];
654   var jenna = people.find(function (person) { return person.gender == 'female'; });
655   equals(jenna.first, 'Jenna');
656 });
657 
658 var source; // global variables
659 
660 module("Real Array", {
661 
662   setup: function () {
663     source = SC.$A(CommonArray);
664   },
665 
666   teardown: function () {
667     delete source;
668 
669     delete Array.prototype["@max(balance)"]; // remove cached value
670     delete Array.prototype["@min(balance)"];
671   }
672 
673 });
674 
675 /*
676   This is a particular problem because reduced properties are registered
677   as dependent keys, which are not automatically configured in native
678   Arrays (where the SC.Object.init method is not run).
679 
680   The fix for this problem was to add an initObservable() method to
681   SC.Observable that will configure bindings and dependent keys.  This
682   method is called from SC.Object.init() and it is called in
683   SC.Observable._notifyPropertyChanges if it has not been called already.
684 
685   SC.Enumerable was in turn modified to register reducers as dependent
686   keys so that now they will be registered on the Array before any
687   property change notifications are sent.
688 */
689 test("should notify observers even if reduced property is cached on prototype", function () {
690   // make sure reduced property is cached
691   source.get("@max(balance)");
692 
693   // now make a clone and observe
694   source = SC.$A(CommonArray);
695 
696   // get the property...this will install the reducer property...
697   source.get("@max(balance)");
698 
699   // install observer
700   var observedValue = null;
701   source.addObserver("@max(balance)", function () {
702     observedValue = source.get("@max(balance)");
703   });
704 
705   //source.addProbe('[]');
706   //source.addProbe('@max(balance)');
707 
708   // add record to array
709   source.pushObject({
710     first: "John",
711     gender: "male",
712     californian: NO,
713     ready: YES,
714     visited: "Paris",
715     balance: 5
716   });
717 
718   //SC.NotificationQueue.flush(); // force observers to trigger
719 
720   // observed value should now be set because the reduced property observer
721   // was triggered when we changed the array contents.
722   equals(5, observedValue, "observedValue");
723 });
724