Files
cappuccino/Tests/AppKit/CPKeyValueBindingTest.j
T

404 lines
15 KiB
Plaintext

@import <AppKit/CPKeyValueBinding.j>
@import <AppKit/CPTextField.j>
// FIXME These imports are needed to prevent the following two errors:
// error=ReferenceError: Can't find variable: CPTopVerticalTextAlignment
// error=ReferenceError: Can't find variable: CPImageLeft
// CPControl uses these but in ojtest runs the dependencies don't
// get imported for some reason.
@import <AppKit/CPButton.j>
@import <AppKit/_CPImageAndTextView.j>
@import <AppKit/CPClipView.j>
@import <AppKit/CPTableView.j>
@import <AppKit/CPArrayController.j>
@implementation CPKeyValueBindingTest : OJTestCase
{
id FOO;
CPArrayController arrayController @accessors;
}
- (void)testExposingBindings
{
[BindingTester exposeBinding:@"foo"];
[BindingTester exposeBinding:@"bar"];
[CPObject exposeBinding:@"zoo"];
[self assert:["foo", "bar", "zoo"] equals:[[BindingTester new] exposedBindings]];
}
- (void)testBindTo
{
var binder = [BindingTester new];
binder.cheese = "orange";
[CPKeyValueBindingTest exposeBinding:@"FOO"];
[self bind:"FOO" toObject:binder withKeyPath:"cheese" options:nil];
[binder setCheese:@"banana"];
[self assertTrue:[self valueForKey:@"FOO"] === "banana" message:"Bound value should have been updated to banana, was " + FOO];
}
- (void)testBindOptions
{
var bindTesterA = [BindingTestWithBool new],
bindTesterB = [BindingTestWithBool new];
[bindTesterB bind:@"stringValue" toObject:bindTesterA withKeyPath:@"stringValue" options:[CPDictionary dictionary]];
[self assert:nil equals:[bindTesterA stringValue] message:"initial A value unchanged"];
[self assert:nil equals:[bindTesterB stringValue] message:"initial B value unchanged"];
[bindTesterA setStringValue:@"My string"];
[self assert:@"My string" equals:[bindTesterA stringValue] message:"A value set"];
[self assert:@"My string" equals:[bindTesterB stringValue] message:"B value updated"];
[bindTesterA setStringValue:nil];
[self assert:nil equals:[bindTesterA stringValue] message:"A value reset"];
[self assert:nil equals:[bindTesterB stringValue] message:"B value updated with nil"];
[bindTesterA unbind:@"stringValue"];
[bindTesterB bind:@"boolValue" toObject:bindTesterA withKeyPath:@"stringValue" options:[CPDictionary dictionaryWithObject:CPIsNotNilTransformerName forKey:CPValueTransformerNameBindingOption]];
[bindTesterB setBoolValue:YES];
[bindTesterA setStringValue:nil];
[self assert:nil equals:[bindTesterA stringValue] message:"A value reset"];
[self assert:NO equals:[bindTesterB boolValue] message:"B value updated"];
[bindTesterB setBoolValue:NO];
[bindTesterA setStringValue:@"My string"];
[self assert:@"My string" equals:[bindTesterA stringValue] message:"A value updated"];
[self assert:YES equals:[bindTesterB boolValue] message:"B value updated"];
[bindTesterB setBoolValue:NO];
[bindTesterA setStringValue:@""];
[self assert:@"" equals:[bindTesterA stringValue] message:"A value updated"];
[self assert:YES equals:[bindTesterB boolValue] message:"B value updated"];
[bindTesterA unbind:@"boolValue"];
[bindTesterB bind:@"boolValue" toObject:bindTesterA withKeyPath:@"stringValue" options:[CPDictionary dictionaryWithObject:CPIsNilTransformerName forKey:CPValueTransformerNameBindingOption]];
[bindTesterB setBoolValue:NO];
[bindTesterA setStringValue:nil];
[self assert:nil equals:[bindTesterA stringValue] message:"A value reset"];
[self assert:YES equals:[bindTesterB boolValue] message:"B value updated"];
[bindTesterB setBoolValue:YES];
[bindTesterA setStringValue:@"My string"];
[self assert:@"My string" equals:[bindTesterA stringValue] message:"A value updated"];
[self assert:NO equals:[bindTesterB boolValue] message:"B value updated"];
[bindTesterB setBoolValue:YES];
[bindTesterA setStringValue:@""];
[self assert:@"" equals:[bindTesterA stringValue] message:"A value updated"];
[self assert:NO equals:[bindTesterB boolValue] message:"B value updated"];
[bindTesterA unbind:@"boolValue"];
[bindTesterB bind:@"stringValue" toObject:bindTesterA withKeyPath:@"stringValue" options:[CPDictionary dictionaryWithObject:@"placeholder" forKey:CPNullPlaceholderBindingOption]];
[bindTesterA setStringValue:@""];
[self assert:@"" equals:[bindTesterA stringValue] message:"A value set (placeholder)"];
[self assert:@"" equals:[bindTesterB stringValue] message:"B value updated (placeholder)"];
[bindTesterA setStringValue:@"My string"];
[self assert:@"My string" equals:[bindTesterA stringValue] message:"A value set (placeholder)"];
[self assert:@"My string" equals:[bindTesterB stringValue] message:"B value updated (placeholder)"];
[bindTesterA setStringValue:nil];
[self assert:nil equals:[bindTesterA stringValue] message:"A value reset (placeholder)"];
[self assert:@"placeholder" equals:[bindTesterB stringValue] message:"B value updated (placeholder)"];
[bindTesterA setStringValue:[CPNull null]];
[self assert:[CPNull null] equals:[bindTesterA stringValue] message:"A value reset (placeholder)"];
[self assert:@"placeholder" equals:[bindTesterB stringValue] message:"B value updated (placeholder)"];
}
- (void)testControl
{
FOO = "bingo";
var binder = [BindingTester new];
binder.cheese = "yellow";
var control = [[CPTextField alloc] init];
[control setStringValue:@"brown"];
[CPControl exposeBinding:CPValueBinding];
[[self class] exposeBinding:@"FOO"];
//[control addObserver:self forKeyPath:CPValueBinding options:nil context:"testControl"];
[control bind:CPValueBinding toObject:self withKeyPath:@"FOO" options:nil];
// Should this even work? It's a two way binding. Anyhow, it currently crashes
// objj disrupting the other unit tests, so commenting it out for now.
//[self bind:@"FOO" toObject:control withKeyPath:CPValueBinding options:nil];
[control setStringValue:@"banana"];
[control setStringValue:@"grapefruit"];
[self setValue:@"BAR" forKey:@"FOO"];
[self assert:FOO equals:[control stringValue]];
[control setStringValue:@"pina colada"];
//[self assert:FOO equals:[control stringValue]];
}
- (void)testTableColumn
{
var tableView = [CPTableView new],
tableColumn = [[CPTableColumn alloc] initWithIdentifier:"A Column"],
arrayController = [CPArrayController new];
[tableView addTableColumn:tableColumn];
content = [
[AccessCounter counterWithValueA:"1" valueB:"2"],
[AccessCounter counterWithValueA:"3" valueB:"4"],
[AccessCounter counterWithValueA:"5" valueB:"6"],
];
[arrayController setContent:content];
[tableColumn bind:@"value" toObject:arrayController withKeyPath:@"arrangedObjects.valueA" options:nil];
// Reset these if they were read during initialization.
for (var i = 0; i < [content count]; i++)
[content[i] setAccesses:0];
var testView = [DataViewTester new];
[tableColumn _prepareDataView:testView forRow:0];
[self assert:'1' equals:testView.lastValue];
[self assert:'objectValue' equals:testView.lastKey];
[tableColumn _prepareDataView:testView forRow:1];
[self assert:'3' equals:testView.lastValue];
[self assert:'objectValue' equals:testView.lastKey];
// Test that CPTableColumn is optimized to only read one value per row.
[self assert:0 equals:[content[2] accesses] message:"row 2 used " + [content[2] accesses] + " accesses but was never prepared"];
[self assert:1 equals:[content[0] accesses] message:"row 0 used " + [content[0] accesses] + " accesses to prepare"];
[self assert:1 equals:[content[1] accesses] message:"row 1 used " + [content[1] accesses] + " accesses to prepare"];
// Try the case where a key path is not used.
content = ["plain", "old", "space crystals"];
[tableColumn bind:@"value" toObject:arrayController withKeyPath:@"arrangedObjects" options:nil];
[arrayController setContent:content];
[tableColumn _prepareDataView:testView forRow:1];
[self assert:'old' equals:testView.lastValue];
[self assert:'objectValue' equals:testView.lastKey];
}
- (void)testTableColumnAutomaticBindings
{
var tableView = [CPTableView new],
tableColumn = [[CPTableColumn alloc] initWithIdentifier:"A Column"];
arrayController = [CPArrayController new];
[tableView addTableColumn:tableColumn];
[tableColumn bind:@"value" toObject:arrayController withKeyPath:@"arrangedObjects.valueA" options:nil];
[self assertTrue:[[tableView infoForBinding:"content"] valueForKey:CPObservedObjectKey] === arrayController message:"when a column of a table is bound to an array controller a 'content' binding should automatically be made to the array controller"];
[self assertTrue:[[tableView infoForBinding:"selectionIndexes"] valueForKey:CPObservedObjectKey] === arrayController message:"when a column of a table is bound to an array controller a 'selectionIndexes' binding should automatically be made to the array controller"];
// This should also work if the AC is referenced through a compound path.
tableView = [CPTableView new];
tableColumn = [[CPTableColumn alloc] initWithIdentifier:"A Column"];
[tableView addTableColumn:tableColumn];
[tableColumn bind:@"value" toObject:self withKeyPath:@"arrayController.arrangedObjects.valueA" options:nil];
[self assertTrue:[[tableView infoForBinding:"content"] valueForKey:CPObservedObjectKey] === arrayController message:"automatic 'content' binding should work even with compound keypath for column binding"];
[self assertTrue:[[tableView infoForBinding:"selectionIndexes"] valueForKey:CPObservedObjectKey] === arrayController message:"automatic 'selectionIndexes' binding should work even with compound keypath for column binding"];
}
- (void)testTextField
{
var textField = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
[textField setPlaceholderString:@"cheese"];
// Establish a random binding.
[textField bind:@"hidden" toObject:self withKeyPath:@"FOO" options:nil];
[self assert:@"cheese" equals:[textField placeholderString] message:"placeholder should not be cleared when a binding is established"];
content = [
[BindingTester testerWithCheese:@"yellow"],
[BindingTester testerWithCheese:@"green"],
];
arrayController = [[CPArrayController alloc] initWithContent:content];
[arrayController setSelectionIndex:0];
var options = [CPDictionary dictionaryWithJSObject:{CPMultipleValuesPlaceholderBindingOption:@"Multiple Values"}];
[textField bind:@"value" toObject:arrayController withKeyPath:@"selection.cheese" options:options];
[self assert:@"yellow" equals:[textField stringValue] message:@"text field string value should be 'yellow'"];
[arrayController setSelectionIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, 2)]];
[self assert:@"" equals:[textField stringValue] message:@"text field string value should be cleared"];
[self assert:@"Multiple Values" equals:[textField placeholderString] message:@"text field placeholder should be 'Multiple Values'"];
[textField unbind:@"value"];
// Cocoa doesn't do this
// [self assert:@"cheese" equals:[textField placeholderString] message:@"text field placeholder should be reset"];
[textField bind:@"value" toObject:arrayController withKeyPath:@"selection.cheese" options:options];
[arrayController setSelectionIndex:0];
[self assert:@"yellow" equals:[textField stringValue] message:"text field string value should be 'yellow'"];
// Cocoa doesn't do this
// [self assert:@"cheese" equals:[textField placeholderString] message:"text field placeholder should be restored"];
textField = [[CPTextField alloc] init];
[textField bind:@"value" toObject:arrayController withKeyPath:@"selection.cheese" options:options];
[arrayController setSelectionIndexes:[CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, 2)]];
[self assert:@"Multiple Values" equals:[textField placeholderString] message:@"text field placeholder should 'Multiple Values'"];
// Cocoa doesn't do this
// [arrayController setSelectionIndex:0];
// [self assert:@"" equals:[textField placeholderString] message:@"empty text field placeholder should be restored"];
}
- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)aContext
{
CPLog(@"here: " + aKeyPath + " value: " + [anObject valueForKey:aKeyPath]);
}
- (void)testSuppressNotification
{
var control = [[CPTextField alloc] init],
anotherControl = [[CPTextField alloc] init];
[control setStringValue:@"brown"];
[control bind:CPValueBinding toObject:self withKeyPath:@"FOO" options:nil];
[self setValue:@"green" forKeyPath:@"FOO"];
[self assert:@"green" equals:[control stringValue] message:@"normal binding action"];
var binding = [CPBinder getBinding:CPValueBinding forObject:control];
[binding suppressSpecificNotificationFromObject:self keyPath:@"FOO"];
[self setValue:@"orange" forKeyPath:@"FOO"];
[self assert:@"green" equals:[control stringValue] message:@"binding update suppressed"];
[binding unsuppressSpecificNotificationFromObject:anotherControl keyPath:@"FOO"];
[self setValue:@"blue" forKeyPath:@"FOO"];
[self assert:@"green" equals:[control stringValue] message:@"binding update still suppressed"];
[binding unsuppressSpecificNotificationFromObject:self keyPath:@"FOO"];
[self setValue:@"octarine" forKeyPath:@"FOO"];
[self assert:@"octarine" equals:[control stringValue] message:@"binding update no longer suppressed"];
}
- (void)testReverseSetValueForDoesNotSetObjectValueOnSource
{
var control = [[TextField alloc] init];
[control setStringValue:@"brown"];
var oc = [[CPObjectController alloc] initWithContent:[CPDictionary dictionaryWithObject:@"toto" forKey:@"foo"]];
[oc setAutomaticallyPreparesContent:YES];
[control bind:CPValueBinding toObject:oc withKeyPath:@"selection.foo" options:nil];
[control setObjectValueSetterCount:0];
// This will force a binding update -reverseSetValueFor:
[control sendAction:nil to:nil];
[self assert:0 equals:[control objectValueSetterCount] message:@"-setObjectValue should not be called"];
}
@end
@implementation TextField : CPTextField
{
CPInteger objectValueSetterCount @accessors;
}
- (void)setObjectValue:(id)aValue
{
objectValueSetterCount++;
[super setObjectValue:aValue];
}
@end
@implementation BindingTester : CPObject
{
id cheese;
}
+ (id)testerWithCheese:(id)aCheese
{
var tester = [[self alloc] init];
[tester setCheese:aCheese];
return tester;
}
- (void)setCheese:(id)aCheese
{
cheese = aCheese;
}
- (id)cheese
{
return cheese;
}
@end
@implementation BindingTestWithBool : CPObject
{
CPString stringValue @accessors;
BOOL boolValue @accessors;
}
@end
@implementation DataViewTester : CPObject
{
id lastValue;
id lastKey;
}
- (void)setValue:value forKey:aKey
{
lastValue = value;
lastKey = aKey;
}
@end
@implementation AccessCounter : CPObject
{
id valueA @accessors;
id valueB @accessors;
CPNumber accesses @accessors;
}
+ (AccessCounter)counterWithValueA:aValue valueB:anotherValue
{
r = [self new];
[r setValueA:aValue];
[r setValueB:anotherValue];
return r;
}
- (id)valueA
{
accesses++;
return valueA;
}
- (id)valueB
{
accesses++;
return valueB;
}
@end