Files
cappuccino/Tests/AppKit/CPArrayControllerTest.j
T
Alexander Ljungberg febdf4092e New implementation of change notifications from CPArrayController.
It is crucial that we send notification only before and after complete changes. In the middle notifications might cause observers to see or react to inconsistent data (e.g. selection indexes pointing to rows no longer present). Added some unit tests - more might be needed in the future. Fixed: before and after values when observing array controller key paths during content changes or rearranges were wrong. Fixed: the array controller sent out multiple redundant change notifications.
2010-08-23 18:53:29 -04:00

273 lines
11 KiB
Plaintext

@implementation CPArrayControllerTest : OJTestCase
{
CPArrayController _arrayController @accessors(property=arrayController);
CPArray _contentArray @accessors(property=contentArray);
CPArray observations;
}
- (void)setUp
{
_contentArray = [];
[_contentArray addObject:[Person personWithName:@"Francisco" age:21]];
[_contentArray addObject:[Person personWithName:@"Ross" age:30]];
[_contentArray addObject:[Person personWithName:@"Tom" age:15]];
// Copy the array since we'll reuse the original array later. Also see issue #795.
_arrayController = [[CPArrayController alloc] initWithContent:[[self contentArray] copy]];
}
- (void)testInitWithContent
{
[self assert:[self contentArray] equals:[[self arrayController] contentArray]];
[self assert:[_CPObservableArray class] equals:[[[self arrayController] arrangedObjects] class] message:"arranged objects should be observable"];
}
- (void)testSetContent
{
otherContent = [@"5", @"6"];
[[self arrayController] setContent:otherContent];
// This has not been decided on yet. See Issue #795.
// [self assertFalse:otherContent === [[self arrayController] contentArray] message:@"array controller should copy it's content"];
[self assert:otherContent equals:[[self arrayController] contentArray]];
[self assert:[_CPObservableArray class] equals:[[[self arrayController] arrangedObjects] class]];
}
- (void)testInsertObjectAtArrangedObjectIndex
{
var object = [Person personWithName:@"Klaas Pieter" age:24],
arrayController = [self arrayController];
[arrayController setSortDescriptors:[[CPSortDescriptor sortDescriptorWithKey:@"age" ascending:YES]]];
[arrayController insertObject:object atArrangedObjectIndex:1];
[self assert:object equals:[[arrayController arrangedObjects] objectAtIndex:1]];
}
- (void)testSelectPrevious
{
var arrayController = [self arrayController];
// Selection index: 1
[arrayController setSelectionIndexes:[CPIndexSet indexSetWithIndex:1]];
[self assertTrue:[arrayController canSelectPrevious] message:@"index > 0; canSelectPrevious should return YES"]
[arrayController selectPrevious:self];
[self assert:[CPIndexSet indexSetWithIndex:0] equals:[arrayController selectionIndexes]];
// Selection index: 0
[self assertFalse:[arrayController canSelectPrevious] message:@"index <= 0; canSelectPrevious should return NO"];
[arrayController selectPrevious:self];
[self assert:[CPIndexSet indexSetWithIndex:0] equals:[arrayController selectionIndexes]];
}
- (void)testSelectNext
{
var arrayController = [self arrayController],
count = [[arrayController arrangedObjects] count];
// Selection index: count - 2
[arrayController setSelectionIndexes:[CPIndexSet indexSetWithIndex:count - 2]];
[self assertTrue:[arrayController canSelectNext] message:@"index < (count - 1); canSelectNext should return YES"];
[arrayController selectNext:self];
[self assert:[CPIndexSet indexSetWithIndex:count - 1] equals:[arrayController selectionIndexes]];
// Selection index: count - 1
[self assertFalse:[arrayController canSelectNext] message:@"index >= (count - 1) canSelectNext should return NO"];
[arrayController selectNext:self];
[self assert:[CPIndexSet indexSetWithIndex:count - 1] equals:[arrayController selectionIndexes]];
}
- (void)testRemoveObjects
{
var arrayController = [self arrayController];
[arrayController setPreservesSelection:NO];
[arrayController setSelectionIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(1, 2)]];
[arrayController removeObjects:[arrayController selectedObjects]]
[self assert:[CPIndexSet indexSet] equals:[arrayController selectionIndexes]
message:@"selection should be empty if arraycontroller doesn't preserve selection"];
arrayController = [[CPArrayController alloc] initWithContent:[self contentArray]];
[arrayController setPreservesSelection:YES];
// Remove from middle
var selectionIndexes = [CPIndexSet indexSetWithIndex:1];
[arrayController setSelectionIndexes:selectionIndexes];
[arrayController removeObjects:[arrayController selectedObjects]];
[self assert:selectionIndexes equals:[arrayController selectionIndexes] message:@"selection should stay the same"];
// Remove from end
[arrayController removeObjects:[[[arrayController content] objectAtIndex:1]]];
[self assert:[CPIndexSet indexSetWithIndex:0] equals:[arrayController selectionIndexes]
message:@"last object removed; selection should shift to first available index"];
// Remove from all
[arrayController removeObjects:[[[arrayController content] objectAtIndex:0]]];
[self assert:[CPIndexSet indexSet] equals:[arrayController selectionIndexes] message:@"no objects left, selection should disappear"];
}
- (void)testContentBinding
{
[[self arrayController] bind:@"contentArray" toObject:self withKeyPath:@"contentArray" options:0];
[self assert:[[self arrayController] contentArray] equals:[self contentArray]];
[[self mutableArrayValueForKey:@"contentArray"] addObject:@"4"];
[self assert:[self contentArray] equals:[[self arrayController] contentArray]
message:@"object 4 was added; contentArray should reflect this"];
[[self arrayController] insertObject:@"2" atArrangedObjectIndex:1];
[self assert:[[self arrayController] contentArray] equals:[self contentArray]];
}
- (CPArray)setupObservationFixture
{
// objj_msgSend_decorate(objj_backtrace_decorator);
var ac = [self arrayController];
[ac setSelectionIndex:0];
[ac setPreservesSelection:YES];
var newContent = [_contentArray copy];
[newContent removeObjectAtIndex:0];
[ac addObserver:self forKeyPath:"content" options:CPKeyValueObservingOptionOld | CPKeyValueObservingOptionNew context:nil];
[ac addObserver:self forKeyPath:"selectionIndexes" options:CPKeyValueObservingOptionOld | CPKeyValueObservingOptionNew context:nil];
[ac addObserver:self forKeyPath:"selectedObjects" options:CPKeyValueObservingOptionOld | CPKeyValueObservingOptionNew context:nil];
[ac addObserver:self forKeyPath:"arrangedObjects" options:CPKeyValueObservingOptionOld | CPKeyValueObservingOptionNew context:nil];
observations = [];
return newContent;
}
- (void)testObservationDuringSetContent
{
/*
There are two areas where CPArrayController can get into trouble with observation.
The more serious one is that the before values when observing selectedObjects
need to be correct and not affected by the new setContent: values.
Second, the array controller should not send out more than one of each notification
since repeated notifications for the same change can be a huge performance drain
from such a central piece of code.
*/
var ac = [self arrayController],
newContent = [self setupObservationFixture];
[ac setContent:newContent];
[observations sortUsingFunction:function(a, b) { return [a.keyPath compare:b.keyPath] } context:nil];
[self assert:4 equals:[observations count] message:"exactly 4 change notifications should be sent for new content"];
// for (var i=0; i<observations.length; i++)
// CPLog.error(observations[i].keyPath);
var observation = observations[0];
[self assert:"arrangedObjects" equals:observation.keyPath];
[self assert:_contentArray equals:observation.oldValue message:"old arranged content should be correct"];
[self assert:newContent equals:observation.newValue message:"new arranged content should be correct"];
observation = observations[1];
[self assert:"content" equals:observation.keyPath];
[self assert:_contentArray equals:observation.oldValue message:"old content should be correct"];
[self assert:newContent equals:observation.newValue message:"new content should be correct"];
observation = observations[2];
[self assert:"selectedObjects" equals:observation.keyPath];
[self assert:[_contentArray[0]] equals:observation.oldValue message:"old selected objects should be previous object at selected index"];
[self assert:[] equals:observation.newValue message:"new selected objects should be nothing"];
observation = observations[3];
[self assert:"selectionIndexes" equals:observation.keyPath];
[self assert:[CPIndexSet indexSetWithIndex:0] equals:observation.oldValue message:"old selected index should be 0"];
[self assert:[CPIndexSet indexSet] equals:observation.newValue message:"new selected index should be nothing"];
}
- (void)testObservationDuringRearrange
{
var ac = [self arrayController],
newContent = [self setupObservationFixture],
selection = [CPIndexSet indexSetWithIndex:0];
[selection addIndex:1];
// Francisco and Ross selected.
[ac setSelectionIndexes:selection];
observations = [];
[ac setFilterPredicate:[CPPredicate predicateWithFormat:@"(name != %@)", "Francisco"]];
[observations sortUsingFunction:function(a, b) { return [a.keyPath compare:b.keyPath] } context:nil];
//for (var i=0; i<observations.length; i++)
// CPLog.error(observations[i].keyPath);
[self assert:3 equals:[observations count] message:"exactly 3 change notifications should be sent for new filter"];
var observation = observations[0];
[self assert:"arrangedObjects" equals:observation.keyPath];
[self assert:_contentArray equals:observation.oldValue message:"old arranged content should be correct"];
[self assert:newContent equals:observation.newValue message:"new arranged content should be correct"];
observation = observations[1];
[self assert:"selectedObjects" equals:observation.keyPath];
[self assert:[_contentArray[0], _contentArray[1]] equals:observation.oldValue message:"old selected objects should be previous object at selected index"];
[self assert:[_contentArray[1]] equals:observation.newValue message:"new selected objects should be only non filtered ones"];
observation = observations[2];
[self assert:"selectionIndexes" equals:observation.keyPath];
[self assert:selection equals:observation.oldValue message:"old selected index should be 0 and 1"];
[self assert:[CPIndexSet indexSetWithIndex:0] equals:observation.newValue message:"new selected index should be 0"];
}
- (void)observeValueForKeyPath:keyPath
ofObject:anActivity
change:change
context:context
{
// CPLog.warn("observeValueForKeyPath:" + keyPath);
// objj_backtrace_print(CPLog.error);
[observations addObject:{
keyPath: keyPath,
oldValue: [change valueForKey:CPKeyValueChangeOldKey],
newValue: [change valueForKey:CPKeyValueChangeNewKey]
}];
}
@end
@implementation Person : CPObject
{
CPString _name @accessors(property=name);
int _age @accessors(property=age);
}
+ (id)personWithName:(CPString)aName age:(int)anAge
{
return [[self alloc] initWithName:aName age:anAge];
}
- (id)initWithName:(CPString)aName age:(int)anAge
{
if (self = [super init])
{
_name = aName;
_age = anAge;
}
return self;
}
- (CPString)description
{
return [CPString stringWithFormat:@"<Person %@ : %@>", [self name], [self age]];
}
@end