Files
cappuccino/Tests/AppKit/CPTableViewTest.j
T

887 lines
33 KiB
Plaintext

@import <AppKit/AppKit.j>
@import "CPNotificationCenterHelper.j"
@implementation CPTableViewTest : OJTestCase
{
CPWindow theWindow;
CPTableView tableView;
CPTableColumn tableColumn;
BOOL doubleActionReceived;
int selectionIsChangingNotificationsReceived;
int selectionDidChangeNotificationsReceived;
}
- (void)setUp
{
// This will init the global var CPApp which are used internally in the AppKit
[[CPApplication alloc] init];
// setup a reasonable table
theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 1024.0, 768.0) styleMask:CPWindowNotSizable];
tableView = [[FirstResponderConfigurableTableView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
tableColumn = [[CPTableColumn alloc] initWithIdentifier:@"Foo"];
tableView.acceptsFirstResponder = YES;
[tableView addTableColumn:tableColumn];
[[theWindow contentView] addSubview:tableView];
}
- (void)testDoubleAction
{
doubleActionReceived = NO;
[tableView setTarget:self];
[tableView setDoubleAction:@selector(doubleAction:)];
// CPEvent with 2 clickCount
var dblClkDown = [CPEvent mouseEventWithType:CPLeftMouseDown location:CGPointMake(50, 50) modifierFlags:0
timestamp:0 windowNumber:[theWindow windowNumber] context:nil eventNumber:0 clickCount:2 pressure:0],
dblClkUp = [CPEvent mouseEventWithType:CPLeftMouseUp location:CGPointMake(50, 50) modifierFlags:0
timestamp:0 windowNumber:[theWindow windowNumber] context:nil eventNumber:0 clickCount:2 pressure:0];
[[CPApplication sharedApplication] sendEvent:dblClkDown];
[tableView trackMouse:dblClkDown];
[[CPApplication sharedApplication] sendEvent:dblClkUp];
[tableView trackMouse:dblClkUp];
[self assertTrue:doubleActionReceived];
// The event should also work even if the table is not the first responder.
tableView.acceptsFirstResponder = NO;
[theWindow makeFirstResponder:nil];
doubleActionReceived = NO;
[[CPApplication sharedApplication] sendEvent:dblClkDown];
[tableView trackMouse:dblClkDown];
[[CPApplication sharedApplication] sendEvent:dblClkUp];
[tableView trackMouse:dblClkUp];
[self assertTrue:doubleActionReceived];
}
- (void)doubleAction:(id)sender
{
doubleActionReceived = YES;
}
/*!
Test that proper notifications are sent - or not sent - during the course of a table data
change which might affect the selection.
*/
- (void)testNumberOfRowsChangedSelectionNotification
{
var dataSource = [TestDataSource new];
[dataSource setTableEntries:["A", "B", "C"]];
[tableView setDataSource:dataSource];
selectionIsChangingNotificationsReceived = 0;
selectionDidChangeNotificationsReceived = 0;
[[CPNotificationCenter defaultCenter] addObserver:self
selector:@selector(selectionIsChanging:)
name:CPTableViewSelectionIsChangingNotification
object:tableView];
[[CPNotificationCenter defaultCenter] addObserver:self
selector:@selector(selectionDidChange:)
name:CPTableViewSelectionDidChangeNotification
object:tableView];
[tableView selectRowIndexes:[CPIndexSet indexSetWithIndex:2] byExtendingSelection:NO];
[self assert:0 equals:selectionIsChangingNotificationsReceived message:"no isChanging notifications when programmatically selecting rows"];
[self assert:1 equals:selectionDidChangeNotificationsReceived message:"didChange notifications when selecting rows"];
selectionIsChangingNotificationsReceived = 0;
selectionDidChangeNotificationsReceived = 0;
[tableView selectRowIndexes:[CPIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
[self assert:selectionDidChangeNotificationsReceived equals:1 message:"CPTableViewSelectionDidChangeNotification expected when selecting rows"];
[[dataSource tableEntries] removeObjectAtIndex:1];
[tableView reloadData];
[self assert:selectionDidChangeNotificationsReceived equals:1 message:"no CPTableViewSelectionDidChangeNotification expected when removing a row which does not change the selection"];
// Reset everything.
[dataSource setTableEntries:["A", "B", "C"]];
[tableView reloadData];
[tableView deselectAll];
selectionIsChangingNotificationsReceived = 0;
selectionDidChangeNotificationsReceived = 0;
[tableView selectRowIndexes:[CPIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
[tableView selectRowIndexes:[CPIndexSet indexSetWithIndex:2] byExtendingSelection:NO];
[self assert:2 equals:selectionDidChangeNotificationsReceived message:"notification for select row 0, 2"];
selectionIsChangingNotificationsReceived = 0;
selectionDidChangeNotificationsReceived = 0;
[tableView deselectAll];
[self assert:1 equals:selectionDidChangeNotificationsReceived message:"notification for deselect all"];
[tableView selectRowIndexes:[CPIndexSet indexSetWithIndex:2] byExtendingSelection:NO];
selectionIsChangingNotificationsReceived = 0;
selectionDidChangeNotificationsReceived = 0;
// If we remove the last row, the selection should change and we should be notified.
[[dataSource tableEntries] removeObjectAtIndex:2];
[tableView reloadData];
[self assert:0 equals:selectionIsChangingNotificationsReceived message:"no isChanging notifications when selected rows disappear"];
[self assert:1 equals:selectionDidChangeNotificationsReceived message:"didChange notifications when selected rows disappear"];
}
- (void)selectionIsChanging:(CPNotification)aNotification
{
selectionIsChangingNotificationsReceived++;
}
- (void)selectionDidChange:(CPNotification)aNotification
{
selectionDidChangeNotificationsReceived++;
}
/*!
Test inline table editing.
*/
- (void)testEditCell
{
var dataSource = [TestDataSource new];
[dataSource setTableEntries:["A", "B", "C"]];
[tableView setDataSource:dataSource];
[tableView setDelegate:[EditableTableDelegate new]];
[theWindow makeFirstResponder:tableView];
[tableView selectRowIndexes:[CPIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
[tableView editColumn:0 row:1 withEvent:nil select:YES];
// Now some text field should be the first responder.
var fieldEditor = [theWindow firstResponder];
[self assert:CPTextField equals:[fieldEditor class] message:"table cell editor should be a text field"];
[fieldEditor setStringValue:"edited text"];
[theWindow makeFirstResponder:tableView];
[self assert:"edited text" equals:[dataSource tableEntries][1] message:"table cell edit should propagate to model"]
// The first responder status should revert to the table view so that, for example, the user may continue
// keyboard navigation to edit the next row.
[self assert:[theWindow firstResponder] equals:tableView message:"table view should be first responder after cell edit"];
}
/*!
Verify that all data appears as expected, using the right data views etc. This is a bit of a kitchen
sink test, verifying multiple behaviours of the table view.
*/
- (void)testLayout
{
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0, 0, 100.0, 100.0)],
tableColumn1 = [[CPTableColumn alloc] initWithIdentifier:@"Bar"];
[tableView addTableColumn:tableColumn1];
[scrollView setDocumentView:tableView];
[tableColumn setWidth:50.0];
[tableColumn1 setWidth:50.0];
var arrayController = [CPArrayController new],
contentArray = [];
for (var row = 0; row < 50; row++)
{
contentArray.push([CPDictionary dictionaryWithObjects:["R" + row + "C0", "R" + row + "C1"] forKeys:["c1", "c2"]]);
}
[arrayController setContent:contentArray];
[tableColumn bind:CPValueBinding toObject:arrayController withKeyPath:@"arrangedObjects.c1" options:nil];
[tableColumn1 bind:CPValueBinding toObject:arrayController withKeyPath:@"arrangedObjects.c2" options:nil];
[tableColumn setDataView:[CustomTextView0 new]];
[tableColumn1 setDataView:[CustomTextView1 new]];
[tableView reloadData];
// Process all events immediately to make sure table data views are reloaded.
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
[self assert:50 equals:[tableView numberOfRows] message:"tableView numberOfRows should reflect content array length"];
function AssertCorrectCellsVisible(startingRow)
{
var allViews = getAllViews(tableView);
// Sort views from left to right, top to bottom.
[allViews sortUsingFunction:keyViewComparator context:nil];
// We only care about the placement of data views - what else the table puts in there is up to it.
[allViews filterUsingPredicate:[CPPredicate predicateWithFormat:@"self.class.description beginswith %@", "CustomTextView"]];
for (var i = 0; i < [allViews count]; i++)
{
var view = allViews[i],
column = i % 2,
row = startingRow + FLOOR(i / 2); // Two columns per row.
//CPLog.error([view stringValue] + " frame: " + CPStringFromRect([tableView convertRect:[view frame] toView:nil]));
[self assert:("R" + row + "C" + column) equals:[view stringValue] message:"(" + row + ", " + column + ") string value"];
[self assert:"CustomTextView" + column equals:[[view class] description] message:"(" + row + ", " + column + ") data view"];
}
return allViews;
}
var allViews = AssertCorrectCellsVisible(0),
visibleHeight = [tableView visibleRect].size.height,
fullRowHeight = [tableView rowHeight] + [tableView intercellSpacing].height,
visibleRows = CEIL(visibleHeight / fullRowHeight);
[self assert:2 * visibleRows equals:[allViews count] message:"only as many data views as necessary should be present"];
// Now if we scroll down, new views should come in and others should go out.
var rowTwentyFiveAndAHalfY = FLOOR(25.5 * fullRowHeight);
[tableView scrollPoint:CGPointMake(0, rowTwentyFiveAndAHalfY)];
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
AssertCorrectCellsVisible(25);
[self assert:2 * visibleRows equals:[allViews count] message:"only as many data views as necessary should be present (2)"];
}
- (void)testContentBinding
{
var contentBindingTable = [[CPTableView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)],
delegate = [ContentBindingTableDelegate new];
tableColumn = [[CPTableColumn alloc] initWithIdentifier:@"A"];
[contentBindingTable addTableColumn:tableColumn];
[delegate setTester:self];
[contentBindingTable setDelegate:delegate];
[delegate setTableEntries:[[@"A1", @"B1"], [@"A2", @"B2"], [@"A3", @"B3"]]];
[contentBindingTable bind:@"content" toObject:delegate withKeyPath:@"tableEntries" options:nil];
// The following should also work:
//var ac = [[CPArrayController alloc] initWithContent:[delegate tableEntries]];
//[contentBindingTable bind:@"content" toObject:ac withKeyPath:@"arrangedObjects" options:nil];
[[theWindow contentView] addSubview:contentBindingTable];
[theWindow makeFirstResponder:contentBindingTable];
// Set the model again with different values
[delegate setTableEntries:[[@"C1", @"D1"], [@"C2", @"D2"], [@"C3", @"D3"]]];
[contentBindingTable reloadData];
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
}
- (void)testColumnValueBinding
{
var table = [[CPTableView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)],
container = [TestDataSource new];
var tc = [[CPTableColumn alloc] initWithIdentifier:@"A"];
[table addTableColumn:tc];
[container setTableEntries:@[@{@"name":@"B1"}, @{@"name":@"B2"}, @{@"name":@"B3"}]];
var ac = [[CPArrayController alloc] init];
[ac bind:@"contentArray" toObject:container withKeyPath:@"tableEntries" options:nil];
[tc bind:@"value" toObject:ac withKeyPath:@"arrangedObjects.name" options:nil];
[[theWindow contentView] addSubview:table];
[theWindow makeFirstResponder:table];
// Should remove all table rows.
[self assertNoThrow:function()
{
[container setTableEntries:@[]];
}];
[container setTableEntries:@[@{@"name":@"B1"}, @{@"name":@"B2"}, @{@"name":@"B3"}]];
[self assertNoThrow:function()
{
[container setTableEntries:nil];
}];
[container setTableEntries:@[@{@"name":@"B1"}, @{@"name":@"B2"}, @{@"name":@"B3"}]];
// Change a value in row 0. The number of rows stays the same.
[container setTableEntries:@[@{@"name":@"B4"}, @{@"name":@"B2"}, @{@"name":@"B3"}]];
var enumerateViewsInRowsCall = 0;
// Checks that the displayed data matches the model data.
[table enumerateAvailableViewsUsingBlock:function(dataView, aRow, aColumn, stop)
{
enumerateViewsInRowsCall++;
var data_value = [[[container tableEntries] objectAtIndex:aRow] objectForKey:@"name"];
[self assert:[dataView objectValue] equals:data_value];
}];
[self assert:enumerateViewsInRowsCall equals:3];
// We could also check that the dataviews in rows 1 and 2 are identical to the ones before mutation.
}
- (void)testInitiallyHiddenColumns
{
var table = [[CPTableView alloc] initWithFrame:CGRectMake(0, 0, 400, 400)],
tableColumn1 = [[CPTableColumn alloc] initWithIdentifier:@"A"],
tableColumn2 = [[CPTableColumn alloc] initWithIdentifier:@"B"],
delegate = [ContentBindingTableDelegate new];
[delegate setTester:self];
[table setDelegate:delegate];
[delegate setTableEntries:[[@"A1", @"B1"], [@"A2", @"B2"], [@"A3", @"B3"]]];
[table bind:@"content" toObject:delegate withKeyPath:@"tableEntries" options:nil];
[[theWindow contentView] addSubview:table];
[tableColumn1 setHidden:YES];
[tableColumn1 setWidth:50.0];
[tableColumn2 setWidth:100.0];
[table addTableColumn:tableColumn1];
[table addTableColumn:tableColumn2];
[table reloadData];
[self assertTrue:[table bounds].size.width > 100 && [table bounds].size.width < 200];
[tableColumn1 setHidden:NO];
[tableColumn1 setWidth:100.0];
[self assertTrue:[table bounds].size.width >= 200];
}
// Test internal method - (void)getColumn:(Function)columnRef row:(Function)rowRef forView:(CPView)aView
- (void)testGetColumnAndRow
{
var table = [[CPTableView alloc] initWithFrame:CGRectMake(0, 0, 400, 400)],
tableColumn1 = [[CPTableColumn alloc] initWithIdentifier:@"A"],
tableColumn2 = [[CPTableColumn alloc] initWithIdentifier:@"B"],
delegate = [ContentBindingTableDelegate new];
[delegate setTester:self];
[table setDelegate:delegate];
[delegate setTableEntries:[[@"A1", @"B1"], [@"A2", @"B2"], [@"A3", @"B3"]]];
[table bind:@"content" toObject:delegate withKeyPath:@"tableEntries" options:nil];
[[theWindow contentView] addSubview:table];
[tableColumn1 setWidth:50.0];
[tableColumn2 setWidth:100.0];
[table addTableColumn:tableColumn1];
[table addTableColumn:tableColumn2];
var table2 = [[CPTableView alloc] initWithFrame:CGRectMake(400, 0, 400, 400)],
table2Column1 = [[CPTableColumn alloc] initWithIdentifier:@"A"],
table2Column2 = [[CPTableColumn alloc] initWithIdentifier:@"B"],
delegate = [ContentBindingTableDelegate new];
[delegate setTester:self];
[table2 setDelegate:delegate];
[delegate setTableEntries:[[@"A1", @"B1"], [@"A2", @"B2"], [@"A3", @"B3"]]];
[table2 bind:@"content" toObject:delegate withKeyPath:@"tableEntries" options:nil];
[[theWindow contentView] addSubview:table2];
[table2Column1 setWidth:50.0];
[table2Column2 setWidth:100.0];
[table2 addTableColumn:tableColumn1];
[table2 addTableColumn:tableColumn2];
var row,
column;
// get row and column for a nil view
[table getColumn:@ref(column) row:@ref(row) forView:nil];
[self assert:CPNotFound equals:column];
[self assert:CPNotFound equals:row];
// get row and column for a random view
var v = [[CPView alloc] initWithFrame:CGRectMake(0,0,100,100)];
[table getColumn:@ref(column) row:@ref(row) forView:v];
[self assert:CPNotFound equals:column];
[self assert:CPNotFound equals:row];
// get row and column for the table view
[table getColumn:@ref(column) row:@ref(row) forView:table];
[self assert:CPNotFound equals:column];
[self assert:CPNotFound equals:row];
// get row and column for a view outside a table column
[table getColumn:@ref(column) row:@ref(row) forView:[theWindow contentView]];
[self assert:CPNotFound equals:column];
[self assert:CPNotFound equals:row];
var enumerateViewsInRowsCall = 0;
// Enumerate views inside the table view and check that rows and columns are correct
[table enumerateAvailableViewsUsingBlock:function(dataView, aRow, aColumn, stop)
{
enumerateViewsInRowsCall++;
var getRow,
getColumn;
[table getColumn:@ref(getColumn) row:@ref(getRow) forView:dataView];
[self assert:aColumn equals:getColumn];
[self assert:aRow equals:getRow];
}];
[self assert:enumerateViewsInRowsCall equals:6];
enumerateViewsInRowsCall = 0;
// Enumerate views inside a different table view and check that rows and columns are not found
[table2 enumerateAvailableViewsUsingBlock:function(dataView, aRow, aColumn, stop)
{
enumerateViewsInRowsCall++;
var getRow,
getColumn;
[table getColumn:@ref(getColumn) row:@ref(getRow) forView:dataView];
[self assert:CPNotFound equals:getRow];
[self assert:CPNotFound equals:getColumn];
}];
[self assert:enumerateViewsInRowsCall equals:6];
}
-(void)testNotificationsRegistered
{
[self assert:[CPNotificationCenterHelper registeredNotificationsForObserver:tableView] equals:[@"_CPWindowDidChangeFirstResponderNotification"] message:@"Notications registered for the tableView in the notification center are wrong"];
[tableView removeFromSuperview];
[self assert:[CPNotificationCenterHelper registeredNotificationsForObserver:tableView] equals:[] message:@"Notications registered for the tableView in the notification center are wrong"];
[[theWindow contentView] addSubview:tableView];
[self assert:[CPNotificationCenterHelper registeredNotificationsForObserver:tableView] equals:[@"_CPWindowDidChangeFirstResponderNotification"] message:@"Notications registered for the tableView in the notification center are wrong"];
[[theWindow contentView] addSubview:tableView];
[self assert:[CPNotificationCenterHelper registeredNotificationsForObserver:tableView] equals:[@"_CPWindowDidChangeFirstResponderNotification"] message:@"Notications registered for the tableView in the notification center are wrong"];
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0, 0, 100.0, 100.0)],
expectedNotifications = [@"_CPWindowDidChangeFirstResponderNotification", @"CPViewFrameDidChangeNotification", @"CPViewBoundsDidChangeNotification"].sort();
[scrollView setDocumentView:tableView];
[[theWindow contentView] addSubview:scrollView];
[self assert:[CPNotificationCenterHelper registeredNotificationsForObserver:tableView] equals:expectedNotifications message:@"Notications registered for the tableView in the notification center are wrong"];
[[theWindow contentView] addSubview:scrollView];
[self assert:[CPNotificationCenterHelper registeredNotificationsForObserver:tableView] equals:expectedNotifications message:@"Notications registered for the tableView in the notification center are wrong"];
[scrollView removeFromSuperview];
[self assert:[CPNotificationCenterHelper registeredNotificationsForObserver:tableView] equals:[] message:@"Notications registered for the tableView in the notification center are wrong"];
}
- (void)testTableDataViewState
{
var textField = [[CPTextField alloc] initWithFrame:CGRectMake(110, 0, 100, 32)];
[textField setEditable:YES];
[[theWindow contentView] addSubview:textField];
var dataSource = [TestDataSource new];
[dataSource setTableEntries:["A", "B", "C"]];
[tableView setDataSource:dataSource];
[tableView setDelegate:[EditableTableDelegate new]];
[theWindow makeFirstResponder:tableView];
[tableView selectRowIndexes:[CPIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
var enumerateViewsInRowsCall = 0;
[tableView enumerateAvailableViewsUsingBlock:function(dataView, row, column, stop)
{
if (row == 0)
{
[self assertTrue:[dataView hasThemeState:CPThemeStateTableDataView] message:"CPThemeStateTableDataView should be enabled"];
[self assertTrue:[dataView hasThemeState:CPThemeStateSelectedDataView] message:"CPThemeStateSelectedDataView should be enabled"];
[self assertTrue:[dataView hasThemeState:CPThemeStateFirstResponder] message:"CPThemeStateFirstResponder should be enabled"];
}
if (row == 1)
{
[self assertTrue:[dataView hasThemeState:CPThemeStateTableDataView] message:"CPThemeStateTableDataView should be enabled"];
[self assertFalse:[dataView hasThemeState:CPThemeStateSelectedDataView] message:"CPThemeStateSelectedDataView should be disabled"];
[self assertTrue:[dataView hasThemeState:CPThemeStateFirstResponder] message:"CPThemeStateFirstResponder should be enabled"];
}
enumerateViewsInRowsCall++;
}];
[self assert:enumerateViewsInRowsCall equals:3];
[tableView selectRowIndexes:[CPIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
enumerateViewsInRowsCall = 0;
[tableView enumerateAvailableViewsUsingBlock:function(dataView, row, column, stop)
{
enumerateViewsInRowsCall++;
if (row == 0)
{
[self assertTrue:[dataView hasThemeState:CPThemeStateTableDataView] message:"CPThemeStateTableDataView should be enabled"];
[self assertFalse:[dataView hasThemeState:CPThemeStateSelectedDataView] message:"CPThemeStateSelectedDataView should be disabled"];
[self assertTrue:[dataView hasThemeState:CPThemeStateFirstResponder] message:"CPThemeStateFirstResponder should be enabled"];
}
}];
[self assert:enumerateViewsInRowsCall equals:3];
[theWindow makeFirstResponder:textField];
enumerateViewsInRowsCall = 0;
[tableView enumerateAvailableViewsUsingBlock:function(dataView, row, column, stop)
{
enumerateViewsInRowsCall++;
if (row == 0)
{
[self assertTrue:[dataView hasThemeState:CPThemeStateTableDataView] message:"CPThemeStateTableDataView should be enabled"];
[self assertFalse:[dataView hasThemeState:CPThemeStateSelectedDataView] message:"CPThemeStateSelectedDataView should be disabled"];
[self assertFalse:[dataView hasThemeState:CPThemeStateFirstResponder] message:"CPThemeStateFirstResponder should be disabled"];
}
if (row == 1)
{
[self assertTrue:[dataView hasThemeState:CPThemeStateTableDataView] message:"CPThemeStateTableDataView should be enabled"];
[self assertTrue:[dataView hasThemeState:CPThemeStateSelectedDataView] message:"CPThemeStateSelectedDataView should be enabled"];
[self assertFalse:[dataView hasThemeState:CPThemeStateFirstResponder] message:"CPThemeStateFirstResponder should be disabled"];
}
}];
[self assert:enumerateViewsInRowsCall equals:3];
}
- (void)testMethodViewAtColumnWithMakeIfNecessarySetToNo
{
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0, 0, 100.0, 100.0)];
[scrollView setDocumentView:tableView];
var dataSource = [TestDataSource new];
[tableView setDelegate:[CustomeSizeTableDelegate new]];
[dataSource setTableEntries:["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]];
[tableView setDataSource:dataSource];
var view = [tableView viewAtColumn:0 row:0 makeIfNecessary:NO];
[self assert:view equals:nil message:@"View should be equal to nil"];
// Process all events immediately to make sure table data views are reloaded.
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
var view = [tableView viewAtColumn:0 row:0 makeIfNecessary:NO];
[self assert:[view objectValue] equals:@"A" message:@"View should be equal to A"];
[self assert:[view superview] equals:tableView message:@"Superview of view should be the tableview"];
view = [tableView viewAtColumn:0 row:25 makeIfNecessary:NO];
[self assert:view equals:nil message:@"View should be equal to nil"];
}
- (void)testMethodViewAtColumnWithMakeIfNecessarySetToYes
{
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0, 0, 100.0, 100.0)];
[scrollView setDocumentView:tableView];
var dataSource = [TestDataSource new];
[tableView setDelegate:[CustomeSizeTableDelegate new]];
[dataSource setTableEntries:["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]];
[tableView setDataSource:dataSource];
var view = [tableView viewAtColumn:0 row:0 makeIfNecessary:YES];
[self assert:[view objectValue] equals:@"A" message:@"View should be equal to A"];
[self assert:[view superview] equals:tableView message:@"Superview of view should be the tableview"];
view = [tableView viewAtColumn:0 row:25 makeIfNecessary:YES];
[self assert:[view objectValue] equals:@"Z" message:@"View should be equal to Z"];
[self assert:[view superview] equals:tableView message:@"Superview of view should be the tableview"];
}
- (void)testMethodViewAtColumnException
{
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0, 0, 100.0, 100.0)];
[scrollView setDocumentView:tableView];
var dataSource = [TestDataSource new];
[tableView setDelegate:[CustomeSizeTableDelegate new]];
[dataSource setTableEntries:["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]];
[tableView setDataSource:dataSource];
var expectedMessage = @"Row 26 out of row range [0-25] for rowViewAtRow:createIfNeeded:",
exceptionMessage = @"";
try
{
[tableView viewAtColumn:0 row:26 makeIfNecessary:YES];
}
catch (e)
{
exceptionMessage = e.message;
}
[self assert:expectedMessage equals:exceptionMessage];
expectedMessage = @"Column 2 out of row range [0-0] for rowViewAtRow:createIfNeeded:";
try
{
[tableView viewAtColumn:2 row:2 makeIfNecessary:YES];
}
catch (e)
{
exceptionMessage = e.message;
}
[self assert:expectedMessage equals:exceptionMessage];
}
/*!
Test that we don't build up an array with all the value for each key path and then pick the row and
throws away all the other values. This is performance critical as this is done for every column on every row.
This is tested by setting up a key path to the name property that is combined. We count the number of times each
part in the key path is accessed with valueForKey.
*/
- (void)testLazyPrepareDataView
{
//objj_msgSend_decorate(objj_backtrace_decorator);
var frameRect = CGRectMake(0, 0, 1024, 64);
[theWindow setFrame:frameRect];
var table = [[CPTableView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)],
container = [TestDataSource new];
var tc1 = [[CPTableColumn alloc] initWithIdentifier:@"A"];
[table addTableColumn:tc1];
var tc2 = [[CPTableColumn alloc] initWithIdentifier:@"B"];
[table addTableColumn:tc2];
var scrollView = [[CPScrollView alloc] initWithFrame:frameRect];
[[theWindow contentView] addSubview:scrollView];
[scrollView setDocumentView:table];
[theWindow makeFirstResponder:table];
var i = 1;
var dataArray = @[
[ValueForKeyCountingDictionary dictionaryWithObject:[ValueForKeyCountingDictionary dictionaryWithObject:@{@"name":@"B"+i, @"longname": @"BB"+i++} forKey:@"b"] forKey:@"a"],
[ValueForKeyCountingDictionary dictionaryWithObject:[ValueForKeyCountingDictionary dictionaryWithObject:@{@"name":@"B"+i, @"longname": @"BB"+i++} forKey:@"b"] forKey:@"a"],
[ValueForKeyCountingDictionary dictionaryWithObject:[ValueForKeyCountingDictionary dictionaryWithObject:@{@"name":@"B"+i, @"longname": @"BB"+i++} forKey:@"b"] forKey:@"a"],
];
var ac = [[CPArrayController alloc] init];
[ac bind:@"contentArray" toObject:container withKeyPath:@"tableEntries" options:nil];
[tc1 bind:@"value" toObject:ac withKeyPath:@"arrangedObjects.a.b.name" options:nil];
[tc2 bind:@"value" toObject:ac withKeyPath:@"arrangedObjects.a.b.longname" options:nil];
[container setTableEntries:dataArray];
// The first two are visible so they are accessed both when setting up the observers and displaying the cell in the table view
[self assert:[[dataArray[0] countingDictionary] objectForKey:@"a"] equals:5];
[self assert:[[dataArray[1] countingDictionary] objectForKey:@"a"] equals:5];
// This is not visible so it is only accessed when setting up the observers
[self assert:[[dataArray[2] countingDictionary] objectForKey:@"a"] equals:2];
var enumerateViewsInRowsCall = 0;
// Checks that the displayed data matches the model data.
[table enumerateAvailableViewsUsingBlock:function(dataView, aRow, aColumn, stop)
{
enumerateViewsInRowsCall++;
var data_value = [[[container tableEntries] objectAtIndex:aRow] valueForKeyPath:aColumn === 0 ? @"a.b.name" : @"a.b.longname"];
[self assert:[dataView objectValue] equals:data_value];
}];
[self assert:enumerateViewsInRowsCall equals:4]; // Only 4 views visible
}
@end
@implementation FirstResponderConfigurableTableView : CPTableView
{
BOOL acceptsFirstResponder;
}
- (BOOL)acceptsFirstResponder
{
return acceptsFirstResponder;
}
@end
@implementation TestDataSource : CPObject <CPTableViewDataSource>
{
CPArray tableEntries @accessors;
}
- (int)numberOfRowsInTableView:(CPTableView)aTableView
{
return [tableEntries count];
}
- (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aColumn row:(CPInteger)aRow
{
return tableEntries[aRow];
}
- (void)tableView:(CPTableView)aTableView setObjectValue:(id)anObject forTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRow
{
tableEntries[aRow] = anObject;
}
@end
@implementation CustomeSizeTableDelegate : CPObject
{
}
- (float)tableView:(CPTableView)aTableView heightOfRow:(CPInteger)aRowIndex
{
return 200;
}
- (CPView)tableView:(CPTableView)aTableView viewForTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)aRowIndex
{
return [[CPTextField alloc] initWithFrame:CGRectMake(0,0, 100, 100)];
}
@end
@implementation EditableTableDelegate : CPObject
{
}
- (BOOL)tableView:(CPTableView)aTableView shouldEditTableColumn:(CPTableColumn)aTableColumn row:(CPInteger)anRow
{
return YES;
}
@end
@implementation ContentBindingTableDelegate : CPObject
{
// For the purpose of this test, the delegate contains the model (!).
CPArray tableEntries @accessors;
CPTableViewTest tester @accessors;
}
- (void)tableView:(CPTableView)aTableView willDisplayView:(CPView)aView forTableColumn:(CPTableColumn)tableColumn row:(CPInteger)row
{
// Make sure each view contains the full row in its objectValue
[tester assert:tableEntries[row] equals:[aView objectValue]];
}
@end
@implementation CustomTextView0 : CPTextField
@end
@implementation CustomTextView1 : CPTextField
@end
@implementation ValueForKeyCountingDictionary : CPMutableDictionary
{
CPMutableDictionary countingDictionary @accessors;
}
- (id)initWithObjects:(CPArray)objects forKeys:(CPArray)keyArray
{
self = [super initWithObjects:objects forKeys:keyArray];
if (self) {
countingDictionary = [CPDictionary new];
}
return self;
}
- (id)valueForKey:(CPString)aKey
{
var countsForKey = [countingDictionary objectForKey:aKey];
countsForKey = (countsForKey == nil ? 1 : countsForKey + 1);
//print([self UID] + @": valueForKey:" + aKey + ", countForKey: " + countsForKey);
//objj_backtrace_print(CPLog.error);
[countingDictionary setObject:countsForKey forKey:aKey];
return [super valueForKey:aKey];
}
@end
var getAllViews = function(aView)
{
var views = [aView],
subviews = [aView subviews];
for (var i = 0, count = [subviews count]; i < count; i++)
views = views.concat(getAllViews(subviews[i]));
return views;
};
var keyViewComparator = function(lhs, rhs, context)
{
var lhsBounds = [lhs convertRect:[lhs bounds] toView:nil],
rhsBounds = [rhs convertRect:[rhs bounds] toView:nil],
lhsY = CGRectGetMinY(lhsBounds),
rhsY = CGRectGetMinY(rhsBounds),
lhsX = CGRectGetMinX(lhsBounds),
rhsX = CGRectGetMinX(rhsBounds),
intersectsVertically = MIN(CGRectGetMaxY(lhsBounds), CGRectGetMaxY(rhsBounds)) - MAX(lhsY, rhsY);
// If two views are "on the same line" (intersect vertically), then rely on the x comparison.
if (intersectsVertically > 0)
{
if (lhsX < rhsX)
return CPOrderedAscending;
if (lhsX === rhsX)
return CPOrderedSame;
return CPOrderedDescending;
}
if (lhsY < rhsY)
return CPOrderedAscending;
if (lhsY === rhsY)
return CPOrderedSame;
return CPOrderedDescending;
};