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