1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  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 
  8 // The TreeItemObserver is tested based on the common use cases.
  9 
 10 var content, delegate, obs, flattened, extra, extrachild, extranested, root;
 11 
 12 // default delegate class.  Does the bare minimum for tree item to function
 13 var Delegate = SC.Object.extend(SC.TreeItemContent, {
 14 
 15   treeItemChildrenKey: "children",
 16   treeItemIsExpandedKey: "isExpanded",
 17 
 18   // This method is used to record range change info
 19 
 20   rangeIndexes: null,
 21   rangeCallCount: 0,
 22 
 23   rangeDidChange: function(array, objects, key, indexes) {
 24     this.rangeCallCount++;
 25     this.rangeIndexes = indexes.frozenCopy();
 26   }
 27 
 28 });
 29 
 30 var TestObject = SC.Object.extend({
 31   toString: function() { return "TestObject(%@)".fmt(this.get('title')); }
 32 });
 33 
 34 /**
 35   Verifies that the passed observer object has the proper content.  This will
 36   iterate over the passed expected array, calling objectAt() on the observer
 37   to verify that it matches.  If you passed the expected index set, it will
 38   also verify that the range observer on the observer was fire with the
 39   matching set of indexes.
 40 
 41   Finally, pass an optional description.
 42 */
 43 function verifyObjectAt(obs, expected, eindexes, desc) {
 44   var idx, len = expected.get('length'), actual;
 45 
 46   // eindexes is optional
 47   if (desc === undefined) {
 48     desc = eindexes;
 49     eindexes = undefined;
 50   }
 51 
 52   equals(obs.get('length'), len, "%@ - length should match".fmt(desc));
 53   for(idx=0;idx<len;idx++) {
 54     actual = obs.objectAt(idx);
 55     equals(actual, expected[idx], "%@ - observer.objectAt(%@) should match expected".fmt(desc, idx));
 56   }
 57 
 58   if (eindexes !== undefined) {
 59     if (eindexes) {
 60       ok(delegate.rangeCallCount>0, 'range observer should be called (actual callCount=%@)'.fmt(delegate.rangeCallCount));
 61     } else {
 62       ok(delegate.rangeCallCount===0, 'range observer should NOT be called (actual callCount=%@)'.fmt(delegate.rangeCallCount));
 63     }
 64 
 65     same(delegate.rangeIndexes, eindexes, 'range observer should be called with expected indexes');
 66   }
 67 
 68 }
 69 
 70 module("SC.TreeItemObserver - Outline Use Case", {
 71   setup: function() {
 72     content = [
 73       TestObject.create({
 74         title: "A",
 75         isExpanded: YES,
 76         outline: 0,
 77 
 78         children: [
 79           TestObject.create({ title: "A.i", outline: 1 }),
 80 
 81           TestObject.create({ title: "A.ii",
 82             outline: 1,
 83             isExpanded: NO,
 84             children: [
 85               TestObject.create({ title: "A.ii.1", outline: 2 }),
 86               TestObject.create({ title: "A.ii.2", outline: 2 }),
 87               TestObject.create({ title: "A.ii.3", outline: 2 })]
 88           }),
 89 
 90           TestObject.create({ title: "A.iii", outline: 1 })]
 91       }),
 92 
 93       TestObject.create({
 94         title: "B",
 95         isExpanded: YES,
 96         outline: 0,
 97         children: [
 98           TestObject.create({ title: "B.i",
 99             isExpanded: YES,
100             outline: 1,
101             children: [
102               TestObject.create({ title: "B.i.1", outline: 2 }),
103               TestObject.create({ title: "B.i.2", outline: 2 }),
104               TestObject.create({ title: "B.i.3", outline: 2 })]
105           }),
106 
107           TestObject.create({ title: "B.ii", outline: 1 }),
108           TestObject.create({ title: "B.iii", outline: 1 })]
109       }),
110 
111       TestObject.create({
112         outline: 0,
113         title: "C"
114       })];
115 
116     root = TestObject.create({
117       title: "ROOT",
118       children: content,
119       isExpanded: YES
120     });
121 
122     extra = TestObject.create({ title: "EXTRA" });
123 
124     extrachild = TestObject.create({
125       title: "EXTRA",
126       isExpanded: YES,
127       children: "0 1".w().map(function(x) {
128         return TestObject.create({ title: "EXTRA.%@".fmt(x) });
129       })
130     });
131 
132     extranested = TestObject.create({
133       title: "EXTRA",
134       isExpanded: YES,
135 
136       children: [
137         TestObject.create({ title: "EXTRA.i" }),
138         TestObject.create({
139           title: "EXTRA.ii",
140           isExpanded: YES,
141           children: "0 1 2".w().map(function(x) {
142             return TestObject.create({ title: "EXTRA.ii.%@".fmt(x) });
143           })
144         }),
145         TestObject.create({ title: "EXTRA.ii" })]
146     });
147 
148     flattened = [
149       content[0],
150       content[0].children[0],
151       content[0].children[1],
152       content[0].children[2],
153       content[1],
154       content[1].children[0],
155       content[1].children[0].children[0],
156       content[1].children[0].children[1],
157       content[1].children[0].children[2],
158       content[1].children[1],
159       content[1].children[2],
160       content[2]];
161 
162     delegate = Delegate.create();
163 
164     obs = SC.TreeItemObserver.create({ delegate: delegate, item: root });
165     obs.addRangeObserver(null, delegate, delegate.rangeDidChange);
166 
167   },
168 
169   teardown: function() {
170     if (obs) obs.destroy(); // cleanup
171     content = delegate = obs = null ;
172   }
173 });
174 
175 // ..........................................................
176 // LENGTH
177 //
178 
179 test("length on create", function() {
180   equals(obs.get('length'), flattened.length, 'should have length of array on create');
181 });
182 
183 // ..........................................................
184 // OBJECT AT
185 //
186 
187 test("objectAt on create", function() {
188   verifyObjectAt(obs, flattened, null, "on create");
189 });
190 
191 // ..........................................................
192 // CHANGING MODEL LAYER CONTENT - NESTED LEVEL
193 //
194 
195 test("pushing object to group", function() {
196   var base = content[1].children[0].children;
197   SC.run(function() { base.pushObject(extra); });
198   flattened.insertAt(9, extra);
199 
200   // changed reflect nearest top-level group
201   var change = SC.IndexSet.create(4, flattened.length-4);
202   verifyObjectAt(obs, flattened, change, "after pushing");
203 });
204 
205 test("popping object from nested", function() {
206   var base = content[1].children[0].children;
207   SC.run(function() { base.popObject(); });
208   flattened.removeAt(8);
209 
210   // changed reflect nearest top-level group
211   var change = SC.IndexSet.create(4, flattened.length-3);
212   verifyObjectAt(obs, flattened, change, "after popping");
213 });
214 
215 test("inserting object in middle of nested", function() {
216   var base = content[1].children[0].children;
217   SC.run(function() { base.insertAt(1,extra); });
218   flattened.insertAt(7, extra);
219 
220   // changed reflect nearest top-level group
221   var change = SC.IndexSet.create(4, flattened.length-4);
222   verifyObjectAt(obs, flattened, change, "after insert");
223 });
224 
225 test("replacing object in nested", function() {
226   var base = content[1].children[0].children;
227   SC.run(function() { base.replace(1,1, [extra]); });
228   flattened.replace(7, 1, [extra]);
229 
230   // changed reflect nearest top-level group
231   var change = SC.IndexSet.create(4, flattened.length-5);
232   verifyObjectAt(obs, flattened, change, "after replacing");
233 });
234 
235 test("removing object in gorup", function() {
236   var base = content[1].children[0].children;
237   SC.run(function() { base.removeAt(1); });
238   flattened.removeAt(7);
239 
240   // changed reflect nearest top-level group
241   var change = SC.IndexSet.create(4, flattened.length-3);
242   verifyObjectAt(obs, flattened, change, "after removing");
243 });
244 
245 test("replacing nested children array", function() {
246   var children = extrachild.children;
247   var base = content[1].children[0];
248   SC.run(function() { base.set('children', children); });
249   flattened.replace(6,3,children);
250 
251   // changed reflect nearest top-level group
252   var change = SC.IndexSet.create(4, flattened.length-3);
253   verifyObjectAt(obs, flattened, change, "after replace children array");
254 });
255 
256 test("changing expansion property on nested", function() {
257   var base = content[1].children[0];
258   SC.run(function() { base.set('isExpanded', NO); });
259   flattened.removeAt(6,3);
260 
261   // changed reflect nearest top-level group
262   var change = SC.IndexSet.create(4, flattened.length-1);
263   verifyObjectAt(obs, flattened, change, "after removing");
264 });
265 
266 test("changing expansion property on top level", function() {
267   var base = content[1];
268   SC.run(function() { base.set('isExpanded', NO); });
269   flattened.removeAt(5,6);
270 
271   // changed reflect nearest top-level group
272   var change = SC.IndexSet.create(4, 8);
273   verifyObjectAt(obs, flattened, change, "after removing");
274 });
275 
276 // ..........................................................
277 // MODIFYING OBSERVER -> MODEL, NESTED-LEVEL
278 //
279 
280 test("adding regular item to end of nested group", function() {
281 
282   var base     = content[1].children[0].children,
283       expected = base.slice();
284 
285   SC.run(function() {
286     obs.replace(9, 0, [extra], SC.DROP_AFTER);
287   });
288   flattened.replace(9, 0, [extra]);
289   expected.pushObject(extra);
290 
291   // verify round trip - change covers effected group
292   var change = SC.IndexSet.create(4, flattened.length-4);
293   verifyObjectAt(obs, flattened, change, 'after pushing object');
294 
295   // verify content change
296   same(base, expected, 'content.children should change');
297 });
298 
299 test("adding regular item after nested group", function() {
300 
301   var base     = content[1].children,
302       expected = base.slice();
303 
304   SC.run(function() {
305     obs.replace(9, 0, [extra], SC.DROP_BEFORE);
306   });
307   flattened.replace(9, 0, [extra]);
308   expected.insertAt(1, extra);
309 
310   // verify round trip - change covers effected group
311   var change = SC.IndexSet.create(4, flattened.length-4);
312   verifyObjectAt(obs, flattened, change, 'after pushing object');
313 
314   // verify content change
315   same(base, expected, 'content.children should change');
316 });
317 
318 test("removing regular item to end of nested", function() {
319 
320   var base     = content[1].children[0].children,
321       expected = base.slice();
322 
323   SC.run(function() {
324     obs.removeAt(8);
325   });
326   flattened.removeAt(8);
327   expected.popObject();
328 
329   // verify round trip - change covers effected group
330   var change = SC.IndexSet.create(4, flattened.length-3);
331   verifyObjectAt(obs, flattened, change, 'after removing object');
332 
333   // verify content change
334   same(base, expected, 'content.children should change');
335 });
336 
337 test("adding regular item to beginning of nested", function() {
338 
339   var base     = content[1].children[0].children,
340       expected = base.slice();
341 
342   SC.run(function() { obs.insertAt(6, extra); });
343   flattened.insertAt(6, extra);
344   expected.insertAt(0, extra);
345 
346   // verify round trip
347   var change = SC.IndexSet.create(4,flattened.length-4);
348   verifyObjectAt(obs, flattened, change, 'after pushing object - should have item');
349 
350   // verify content change
351   same(base, expected, 'content should have new extra item');
352 });
353 
354 test("removing regular item to beginning", function() {
355 
356   var base     = content[1].children[0].children,
357       expected = base.slice();
358 
359   SC.run(function() { obs.removeAt(6); });
360   flattened.removeAt(6);
361   expected.removeAt(0);
362 
363   // verify round trip
364   var change = SC.IndexSet.create(4,flattened.length-3);
365   verifyObjectAt(obs, flattened, change, 'after pushing object - should have item');
366 
367   // verify content change
368   same(base, expected, 'content should have new extra item');
369 });
370 
371 test("adding regular item to middle", function() {
372 
373   var base     = content[1].children[0].children,
374       expected = base.slice();
375 
376   SC.run(function() { obs.insertAt(7, extra); });
377   flattened.insertAt(7, extra);
378   expected.insertAt(1, extra);
379 
380   // verify round trip
381   var change = SC.IndexSet.create(4,flattened.length-4);
382   verifyObjectAt(obs, flattened, change, 'after adding object');
383 
384   // verify content change
385   same(base, expected, 'content should have new extra item');
386 });
387 
388 test("removing regular item to middle", function() {
389 
390   var base     = content[1].children[0].children,
391       expected = base.slice();
392 
393   SC.run(function() { obs.removeAt(7); });
394   flattened.removeAt(7);
395   expected.removeAt(1);
396 
397   // verify round trip
398   var change = SC.IndexSet.create(4,flattened.length-3);
399   verifyObjectAt(obs, flattened, change, 'after adding object');
400 
401   // verify content change
402   same(base, expected, 'content should have new extra item');
403 });
404 
405 // ..........................................................
406 // SC.COLLECTION CONTENT SUPPORT
407 //
408 
409 test("contentGroupIndexes - not grouped", function() {
410   equals(delegate.get('treeItemIsGrouped'), NO, 'precond - delegate.treeItemIsGrouped == NO');
411   equals(obs.contentGroupIndexes(null, obs), null, 'contentGroupIndexes should be null');
412 
413   var idx, len = obs.get('length');
414   for(idx=0;idx<len;idx++) {
415     equals(obs.contentIndexIsGroup(null, obs, idx), NO, 'obs.contentIndexIsGroup(null, obs, %@) should be NO'.fmt(idx));
416   }
417 });
418 
419 test("contentGroupIndexes - grouped", function() {
420   delegate.set('treeItemIsGrouped', YES);
421   equals(delegate.get('treeItemIsGrouped'), YES, 'precond - delegate.treeItemIsGrouped == YES');
422 
423   var set = SC.IndexSet.create(0).add(4);
424   same(obs.contentGroupIndexes(null, obs), set, 'contentGroupIndexes should cover just top level group items');
425 
426   var idx, len = obs.get('length');
427   for(idx=0;idx<len;idx++) {
428     equals(obs.contentIndexIsGroup(null, obs, idx), set.contains(idx), 'obs.contentIndexIsGroup(null, obs, %@) (%@)'.fmt(idx, flattened[idx]));
429   }
430 });
431 
432 test("contentIndexOutlineLevel", function() {
433   var idx, len = obs.get('length');
434   for(idx=0;idx<len;idx++) {
435     var expected = flattened[idx].outline;
436 
437     equals(obs.contentIndexOutlineLevel(null, obs, idx), expected, 'obs.contentIndexOutlineLevel(null, obs, %@) (%@)'.fmt(idx, flattened[idx]));
438   }
439 });
440 
441 test("contentIndexDisclosureState", function() {
442   var idx, len = obs.get('length');
443   for(idx=0;idx<len;idx++) {
444     var expected = flattened[idx].isExpanded;
445     expected = (expected === NO) ? SC.BRANCH_CLOSED : (expected ? SC.BRANCH_OPEN : SC.LEAF_NODE);
446 
447     var str ;
448     switch(expected) {
449       case SC.BRANCH_CLOSED:
450         str = "SC.BRANCH_CLOSED";
451         break;
452       case SC.BRANCH_OPEN:
453         str = "SC.BRANCH_OPEN";
454         break;
455       default:
456          str = "SC.LEAF_NODE";
457          break;
458     }
459 
460     equals(obs.contentIndexDisclosureState(null, obs, idx), expected, 'obs.contentIndexDisclosureState(null, obs, %@) (%@) should eql %@'.fmt(idx,flattened[idx], str));
461   }
462 });
463 
464 // ..........................................................
465 // SPECIAL CASES
466 //
467 
468 test("moving nested item from one parent to another", function() {
469   var parent1 = content[1].children[0].children;
470   var parent2 = content[0].children;
471   var item    = parent1[1];
472 
473   SC.run(function() {
474     parent1.removeObject(item);
475     parent2.insertAt(0, item);
476   });
477 
478   flattened.removeObject(item);
479   flattened.insertAt(flattened.indexOf(content[0])+1, item);
480 
481   var change = SC.IndexSet.create(0, flattened.length);
482   verifyObjectAt(obs, flattened, change, "after moving");
483 });
484 
485