// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ==========================================================================

/*globals throws */

var content, controller, extra;

var TestObject = SC.Object.extend({

title: "test",
toString: function() { return "TestObject(%@)".fmt(this.get("title")); }

});

var ComplexTestObject = SC.Object.extend({

firstName: null,
lastName: null,
toString: function() { return "TestObject(%@ %@)".fmt(this.get("firstName"), this.get('lastName')); }

});

// .….….….….….….….….….….….….….….. // EMPTY //

module(“SC.ArrayController - array_case - EMPTY”, {

setup: function() {
  content = [];
  controller = SC.ArrayController.create({ content: content });
  extra = TestObject.create({ title: "FOO" });
},

teardown: function() {
  controller.destroy();
}

});

test(“state properties”, function() {

equals(controller.get("hasContent"), YES, 'c.hasContent');
equals(controller.get("canRemoveContent"), YES, "c.canRemoveContent");
equals(controller.get("canReorderContent"), YES, "c.canReorderContent");
equals(controller.get("canAddContent"), YES, "c.canAddContent");

});

// addObject should append to end of array + notify observers on Array itself test(“addObject”, function() {

var callCount = 0;
controller.addObserver('[]', function() { callCount++; });

SC.run(function() { controller.addObject(extra); });

same(content, [extra], 'addObject(extra) should work');
equals(callCount, 1, 'should notify observer that content has changed');
equals(content.get('length'), 1, 'should update length of controller');

});

test(“removeObject”, function() {

var callCount = 0;
controller.addObserver('[]', function() { callCount++; });

SC.run(function() { controller.removeObject(extra); });

same(content, [], 'removeObject(extra) should have no effect');
equals(callCount, 0, 'should not notify observer since content did not change');

});

test(“basic array READ operations”, function() {

equals(controller.get("length"), 0, 'length should be empty');
equals(controller.objectAt(0), undefined, "objectAt() should return undefined");

});

test(“basic array WRITE operations”, function() {

var callCount = 0;
controller.addObserver('[]', function() { callCount++; });

controller.replace(0,1,[extra]);

same(content, [extra], 'should modify content');
equals(callCount, 1, 'should notify observer that content has changed');
equals(content.get('length'), 1, 'should update length of controller');

});

test(“arrangedObjects”, function() {

equals(controller.get("arrangedObjects"), controller, 'c.arrangedObjects should return receiver');

});

// .….….….….….….….….….….….….….….. // NON-EMPTY ARRAY //

module(“SC.ArrayController - array_case - NON-EMPTY”, {

setup: function() {
  content = "1 2 3 4 5".w().map(function(x) {
    return TestObject.create({ title: x });
  });

  controller = SC.ArrayController.create({ content: content });
  extra = TestObject.create({ title: "FOO" });
},

teardown: function() {
  controller.destroy();
}

});

test(“state properties”, function() {

equals(controller.get("hasContent"), YES, 'c.hasContent');
equals(controller.get("canRemoveContent"), YES, "c.canRemoveContent");
equals(controller.get("canReorderContent"), YES, "c.canReorderContent");
equals(controller.get("canAddContent"), YES, "c.canAddContent");

});

// addObject should append to end of array + notify observers on Array itself test(“addObject”, function() {

var expected = content.slice();
expected.push(extra);

var callCount = 0;
controller.addObserver('[]', function() { callCount++; });

SC.run(function() { controller.addObject(extra); });

same(content, expected, 'addObject(extra) should work');
equals(callCount, 1, 'should notify observer that content has changed');
equals(content.get('length'), expected.length, 'should update length of controller');

});

test(“removeObject”, function() {

var expected = content.slice(), obj = expected[3];
expected.removeObject(obj);

var callCount = 0;
controller.addObserver('[]', function() { callCount++; });

SC.run(function() { controller.removeObject(obj); });

same(content, expected, 'removeObject(extra) should remove object');
equals(callCount, 1, 'should notify observer that content has changed');
equals(content.get('length'), expected.length, 'should update length of controller');

});

test(“basic array READ operations”, function() {

equals(controller.get("length"), content.length, 'length should be empty');

var loc = content.length+1; // verify 1 past end as well
while(--loc>=0) {
  equals(controller.objectAt(loc), content[loc], "objectAt(%@) should return same value at content[%@]".fmt(loc, loc));
}

});

test(“basic array WRITE operations”, function() {

var expected = content.slice();
expected.replace(3,1,[extra]);

var callCount = 0;
controller.addObserver('[]', function() { callCount++; });

controller.replace(3,1,[extra]);

same(content, expected, 'should modify content');
equals(callCount, 1, 'should notify observer that content has changed');
equals(content.get('length'), expected.length, 'should update length of controller');

});

test(“arrangedObjects”, function() {

equals(controller.get("arrangedObjects"), controller, 'c.arrangedObjects should return receiver');

});

test(“The computed properties firstObject, firstSelectableObject & lastObject should update when content changes.”, function(){

equals(controller.get('firstObject'), content[0], 'first object should be the first object in content');
equals(controller.get('firstSelectableObject'), content[0], 'first selectable object should be the first object in content');
equals(controller.get('lastObject'), content[4], 'lastObject should be the last object in content');

// Reorder the content
var newObject = TestObject.create({ title: "BLAH" });
controller.set('content', [newObject]);

equals(controller.get('firstObject'), newObject, 'first object should be the new first object in content');
equals(controller.get('firstSelectableObject'), newObject, 'first selectable object should be the new first object in content');
equals(controller.get('lastObject'), newObject, 'lastObject should be the new last object in content');

});

test(“The computed properties firstObject, firstSelectableObject & lastObject should update when content items change.”, function(){

equals(controller.get('firstObject'), content[0], 'first object should be the first object in content');
equals(controller.get('firstSelectableObject'), content[0], 'first selectable object should be the first object in content');
equals(controller.get('lastObject'), content[4], 'lastObject should be the last object in content');

// Change the items.
var newObject = TestObject.create({ title: "BLAH" });
controller.replace(0, 5, [newObject]);

equals(controller.get('firstObject'), newObject, 'first object should be the new first object in content');
equals(controller.get('firstSelectableObject'), newObject, 'first selectable object should be the new first object in content');
equals(controller.get('lastObject'), newObject, 'lastObject should be the new last object in content');

});

// .….….….….….….….….….….….….….….. // orderBy //

test(“array orderBy using String”, function(){

var testController = SC.ArrayController.create({
  content: content,
  orderBy: 'title ASC'
});

equals(testController.get('firstSelectableObject'), content[0], 'first selectable object should be the first object in arrangedObjects');
equals(testController.get('lastObject'), content[4], 'lastObject should be the last object in content');

// Reorder the content
testController.set('orderBy', 'title DESC');

equals(testController.get('firstSelectableObject'), content[4], 'first selectable object should be the first object in arrangedObjects (changed order)');
equals(testController.get('lastObject'), content[0], 'lastObject should be the first object in content (changed order)');

});

test(“array orderBy using Array”, function(){

var complexContent,
    familyNames = "Keating Zane Alberts Keating Keating".w(),
    givenNames = "Travis Harold Brian Alvin Peter".w(),
    testController;

complexContent = familyNames.map(function(x, i) {
  return ComplexTestObject.create({ lastName: x, firstName: givenNames.objectAt(i) });
});

testController = SC.ArrayController.create({
  content: complexContent
});

equals(testController.get('firstSelectableObject'), complexContent[0], 'first selectable object should be the first object in arrangedObjects');

// Reorder the content
testController.set('orderBy', ['lastName', 'firstName']); // Brian Alberts, Alvin Keating, Peter Keating, Travis Keating, Harold Zane
equals(testController.get('firstSelectableObject'), complexContent[2], 'first selectable object should be the first object in arrangedObjects (changed order)');
equals(testController.objectAt(1), complexContent[3], 'fourth content object should be the second object in arrangedObjects (changed order)');

// Reorder the content
testController.set('orderBy', ['lastName', 'firstName DESC']); // Brian Alberts, Travis Keating, Peter Keating, Alvin Keating,Harold Zane
equals(testController.objectAt(3), complexContent[3], 'fourth content object should be the fourth object in arrangedObjects (changed order)');

});

test(“array orderBy using function”, function(){

var testFunc = function(a,b){
  if(a.get('title') > b.get('title')) return -1;
  else if (a.get('title') == b.get('title')) return 0;
  else return 1;
};
var expected = content.slice();
expected.sort(testFunc);

var testController = SC.ArrayController.create({
  content: content,
  orderBy: testFunc
});
same(testController.get('arrangedObjects').toArray(), expected, 'arrangedObjects should be sortable by a custom function');

});

test(“verify length is correct in arrayObserver didChange method when orderBy is set”, function () {

content = [];
controller = SC.ArrayController.create({
  content: content,
  orderBy: 'i haz your content!'
});
expect(2);

controller.addArrayObservers({
  willChange: function () {
    equals(this.get('length'), 0, 'length should be 0');
  },

  didChange: function () {
    equals(this.get('length'), 1, 'length should be 1');
  }
});

content.pushObject(":{");

});

// orderBy impacts arrayContentDidChange calls.

test(“verify range observers fire correctly when object added at different sorted index than absolute index”, function() {

content = [ TestObject.create({ value: 1 }), TestObject.create({ value: 2 }) ];
controller = SC.ArrayController.create({
  content: content,
  orderBy: 'value ASC'
});
var callCount = 0;
controller.addRangeObserver(SC.IndexSet.create(0, 2), null, function() { callCount++; });
controller.content.pushObject(TestObject.create({ value: 0 }));
ok(callCount === 1, "Range observer should have fired based on inclusion in the sorted range rather than the raw content range.");

});

// Tests bug introduced in e33416fdd28363479b598bdbab081d5abd9737f7 (see github.com/sproutcore/sproutcore/issues/1214). Verified // more generally in test below. test(“verify enumerable propety chains invalidate without error on ArrayController with orderBy.”, function() {

controller = SC.ArrayController.create({
  content: [],
  orderBy: 'value ASC',
  // Though nonsensical (could be '[]' without error), this property path is our canary.
  rangeProperty: function() {}.property('*content.[]')
});

var didError = NO;
try {
  controller.content.pushObject(TestObject.create({ value: 0 }));
} catch (e) {
  didError = YES;
}

ok(!didError, "Adding an object to an empty array controller with orderBy and an enumerable property chain proceeds without error.");

});

test(“verify arrayContentWillChange and arrayContentDidChange are called with correct values when orderBy is present.”, function() {

// Set up test values.
var expectedStart = 0,
    expectedRemoved = 0,
    expectedAdded = 0,
    testMessage = "PRELIM %@: Creating array controller, '%@' should be";
// Create controller.
controller = SC.ArrayController.create({
  content: [],
  orderBy: 'value ASC',
  arrayContentWillChange: function(start, removed, added) {
    equals(start, expectedStart, testMessage.fmt('arrayContentWillChange', 'start'));
    equals(removed, expectedRemoved, testMessage.fmt('arrayContentWillChange', 'removed'));
    equals(added, expectedAdded, testMessage.fmt('arrayContentWillChange', 'added'));
    return sc_super();
  },
  arrayContentDidChange: function(start, removed, added) {
    equals(start, expectedStart, testMessage.fmt('arrayContentDidChange', 'start'));
    equals(removed, expectedRemoved, testMessage.fmt('arrayContentDidChange', 'removed'));
    equals(added, expectedAdded, testMessage.fmt('arrayContentDidChange', 'added'));
    return sc_super();
  }
});

// NOTE THAT THE FOLLOWING TESTS DEPEND ON THE CONTENT AS SET BY THE PREVIOUS TEST. (SORRY.)

// Adding one item to empty array.
expectedStart = 0;
expectedRemoved = 0;
expectedAdded = 1;
testMessage = "%@: adding a single item to an empty array, '%@' should be";
controller.content.pushObject(TestObject.create({ value: 0 }));

// Adding one item to an array with one item.
expectedStart = 0;
expectedRemoved = 1;
expectedAdded = 2;
testMessage = "%@: adding a single item to an array with one item, '%@' should be";
controller.content.pushObject(TestObject.create({ value: 0 }));

// Removing the first item from a two-item array
expectedStart = 0;
expectedRemoved = 2;
expectedAdded = 1;
testMessage = "%@: adding a single item to an array with one item, '%@' should be";
controller.content.removeAt(0);

// Replacing the first item in a one-item array with two items.
expectedStart = 0;
expectedRemoved = 1;
expectedAdded = 2
testMessage = "%@: adding a single item to an array with one item, '%@' should be";
controller.content.pushObject(0, 1, [TestObject.create({ value: 1 }), TestObject.create({ value: 0 })]);

});

// .….….….….….….….….….….….….….….. // ADD SPECIAL CASES HERE //

test(“verify rangeObserver fires when content is deleted”, function() {

content = "1 2 3 4 5".w().map(function(x) {
  return TestObject.create({ title: x });
});

controller = SC.ArrayController.create({ content: content });

var cnt = 0,
    observer = SC.Object.create({ method: function() { cnt++; } });

controller.addRangeObserver(SC.IndexSet.create(0,2), observer, observer.method);

SC.RunLoop.begin();
content.replace(0, content.length, []);
SC.RunLoop.end();

equals(cnt, 1, 'range observer should have fired once');

});

test(“should invalidate computed property once per changed key”, function() {

var setCalls = 0;
var getCalls = 0;

window.peopleController = SC.ArrayController.create({
  foo: YES,
  content: [SC.Object.create({name:'Juan'}),
            SC.Object.create({name:'Camilo'}),
            SC.Object.create({name:'Pinzon'}),
            SC.Object.create({name:'Señor'}),
            SC.Object.create({name:'Daaaaaale'})],

  fullNames: function(key, value) {
    if (value !== undefined) {
      setCalls++;
      this.setEach('name', value);
    } else {
      getCalls++;
    }

    return this.getEach('name').join(' ');
  }.property('@each.name')
});

try {
  var peopleWatcher = SC.Object.create({
    namesBinding: 'peopleController.fullNames'
  });

  SC.run();
  SC.run(function() { peopleWatcher.set('names', 'foo bar baz'); });
  equals(setCalls, 1, "calls set once");
  // equals(getCalls, 3, "calls get three times");
  // TODO: Figure out what the right number is. Recent optimizations have reduced
  // it significantly, but we can't get it below 7.
} finally {
  window.peopleController = undefined;
}

});

module(“SC.ArrayController - dependent keys with @each”);

test(“should invalidate property when property on any enumerable changes”, function() {

var inventory = [];
var recomputed = 0;

for (var idx = 0; idx < 20; idx++) {
  inventory.pushObject(SC.Object.create({
    price: 5
  }));
}
var restaurant = SC.ArrayController.create({
  content: inventory,

  totalCost: function() {
    recomputed++;
    return inventory.reduce(function(prev, item) {
      return prev+item.get('price');
    }, 0);
  }.property('@each.price').cacheable()
});

equals(restaurant.get('totalCost'), 100, "precond - computes cost of all items");
inventory[0].set('price', 6);

equals(restaurant.get('totalCost'), 101, "recalculates after dependent key on an enumerable item changes");
inventory[19].set('price', 6);

equals(restaurant.get('totalCost'), 102, "recalculates after dependent key on a different item changes");
inventory.pushObject(SC.Object.create({
  price: 5
}));
equals(restaurant.get('totalCost'), 107, "recalculates after adding an item to the enumerable");

var item = inventory.popObject();
equals(restaurant.get('totalCost'), 102, "recalculates after removing an item from the enumerable");

recomputed = 0;
item.set('price', 0);
equals(recomputed, 0, "does not recalculate after changing key on removed item");

});

test(“should invalidate property when property of array item changes after content has changed”, function() {

var inventory = [];
var recomputed = 0;

for (var idx = 0; idx < 20; idx++) {
  inventory.pushObject(SC.Object.create({
    price: 5
  }));
}
var restaurant = SC.ArrayController.create({
  content: [],

  totalCost: function() {
    recomputed++;
    return inventory.reduce(function(prev, item) {
      return prev+item.get('price');
    }, 0);
  }.property('@each.price').cacheable()
});

restaurant.set('content', inventory);

equals(restaurant.get('totalCost'), 100, "precond - computes cost of all items");
inventory[0].set('price', 6);

equals(restaurant.get('totalCost'), 101, "recalculates after dependent key on an enumerable item changes");
inventory[19].set('price', 6);

equals(restaurant.get('totalCost'), 102, "recalculates after dependent key on a different item changes");
inventory.pushObject(SC.Object.create({
  price: 5
}));
equals(restaurant.get('totalCost'), 107, "recalculates after adding an item to the enumerable");

var item = inventory.popObject();
equals(restaurant.get('totalCost'), 102, "recalculates after removing an item from the enumerable");

recomputed = 0;
item.set('price', 0);
equals(recomputed, 0, "does not recalculate after changing key on removed item");

});

// .….….….….….….….….….….….….….….. // VERIFY SC.ARRAY COMPLIANCE //

SC.ArraySuite.generate(“SC.ArrayController”, {

newObject: function(amt) {
  if (amt === undefined || typeof amt === SC.T_NUMBER) {
    amt = this.expected(amt);
  }
  return SC.ArrayController.create({ content: amt });
}

});