/core/externals/google-toolbox-for-mac/UnitTesting/GTMAppKit+UnitTesting.m
Objective C | 583 lines | 334 code | 85 blank | 164 comment | 21 complexity | 61cdd799ac722957b60ef51b6fd7fed5 MD5 | raw file
1// 2// GTMAppKit+UnitTesting.m 3// 4// Categories for making unit testing of graphics/UI easier. 5// 6// Copyright 2006-2008 Google Inc. 7// 8// Licensed under the Apache License, Version 2.0 (the "License"); you may not 9// use this file except in compliance with the License. You may obtain a copy 10// of the License at 11// 12// http://www.apache.org/licenses/LICENSE-2.0 13// 14// Unless required by applicable law or agreed to in writing, software 15// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 16// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 17// License for the specific language governing permissions and limitations under 18// the License. 19// 20 21#import "GTMDefines.h" 22#import "GTMAppKit+UnitTesting.h" 23#import "GTMGeometryUtils.h" 24#import "GTMMethodCheck.h" 25 26#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4 27 #define ENCODE_NSINTEGER(coder, i, key) [(coder) encodeInt:(i) forKey:(key)] 28#else 29 #define ENCODE_NSINTEGER(coder, i, key) [(coder) encodeInteger:(i) forKey:(key)] 30#endif 31 32@implementation NSApplication (GTMUnitTestingAdditions) 33GTM_METHOD_CHECK(NSObject, gtm_unitTestEncodeState:); 34 35- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 36 [super gtm_unitTestEncodeState:inCoder]; 37 ENCODE_NSINTEGER(inCoder, [[self mainWindow] windowNumber], @"ApplicationMainWindow"); 38 39 // Descend down into the windows allowing them to store their state 40 NSWindow *window = nil; 41 int i = 0; 42 GTM_FOREACH_OBJECT(window, [self windows]) { 43 if ([window isVisible]) { 44 // Only record visible windows because invisible windows may be closing on us 45 // This appears to happen differently in 64 bit vs 32 bit, and items 46 // in the window may hold an extra retain count for a while until the 47 // event loop is spun. To avoid all this, we just don't record non 48 // visible windows. 49 // See rdar://5851458 for details. 50 [inCoder encodeObject:window forKey:[NSString stringWithFormat:@"Window %d", i]]; 51 i = i + 1; 52 } 53 } 54 55 // and encode the menu bar 56 NSMenu *mainMenu = [self mainMenu]; 57 if (mainMenu) { 58 [inCoder encodeObject:mainMenu forKey:@"MenuBar"]; 59 } 60} 61@end 62 63@implementation NSWindow (GTMUnitTestingAdditions) 64 65- (CGImageRef)gtm_unitTestImage { 66 return [[[self contentView] superview] gtm_unitTestImage]; 67} 68 69- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 70 [super gtm_unitTestEncodeState:inCoder]; 71 [inCoder encodeObject:[self title] forKey:@"WindowTitle"]; 72 [inCoder encodeBool:[self isVisible] forKey:@"WindowIsVisible"]; 73 // Do not record if window is key, because users running unit tests 74 // and clicking around to other apps, could change this mid test causing 75 // issues. 76 // [inCoder encodeBool:[self isKeyWindow] forKey:@"WindowIsKey"]; 77 [inCoder encodeBool:[self isMainWindow] forKey:@"WindowIsMain"]; 78 [inCoder encodeObject:[self contentView] forKey:@"WindowContent"]; 79 if ([self toolbar]) { 80 [inCoder encodeObject:[self toolbar] forKey:@"WindowToolbar"]; 81 } 82} 83 84@end 85 86@implementation NSControl (GTMUnitTestingAdditions) 87 88// Encodes the state of an object in a manner suitable for comparing 89// against a master state file so we can determine whether the 90// object is in a suitable state. 91// 92// Arguments: 93// inCoder - the coder to encode our state into 94- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 95 [super gtm_unitTestEncodeState:inCoder]; 96 [inCoder encodeObject:[self class] forKey:@"ControlType"]; 97 [inCoder encodeObject:[self objectValue] forKey:@"ControlValue"]; 98 [inCoder encodeObject:[self selectedCell] forKey:@"ControlSelectedCell"]; 99 ENCODE_NSINTEGER(inCoder, [self tag], @"ControlTag"); 100 [inCoder encodeBool:[self isEnabled] forKey:@"ControlIsEnabled"]; 101} 102 103@end 104 105@implementation NSButton (GTMUnitTestingAdditions) 106 107// Encodes the state of an object in a manner suitable for comparing 108// against a master state file so we can determine whether the 109// object is in a suitable state. 110// 111// Arguments: 112// inCoder - the coder to encode our state into 113- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 114 [super gtm_unitTestEncodeState:inCoder]; 115 NSString *alternateTitle = [self alternateTitle]; 116 if (alternateTitle) { 117 [inCoder encodeObject:alternateTitle forKey:@"ButtonAlternateTitle"]; 118 } 119} 120 121@end 122 123@implementation NSTextField (GTMUnitTestingAdditions) 124 125- (BOOL)gtm_shouldEncodeStateForSubviews { 126 return NO; 127} 128 129// Encodes the state of an object in a manner suitable for comparing 130// against a master state file so we can determine whether the 131// object is in a suitable state. 132// 133// Arguments: 134// inCoder - the coder to encode our state into 135- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 136 [super gtm_unitTestEncodeState:inCoder]; 137 id controlCell = [self cell]; 138 if ([controlCell isKindOfClass:[NSTextFieldCell class]]) { 139 NSTextFieldCell *textFieldCell = controlCell; 140 [inCoder encodeObject:[textFieldCell placeholderString] 141 forKey:@"PlaceHolderString"]; 142 } 143} 144 145@end 146 147@implementation NSCell (GTMUnitTestingAdditions) 148 149// Encodes the state of an object in a manner suitable for comparing 150// against a master state file so we can determine whether the 151// object is in a suitable state. 152// 153// Arguments: 154// inCoder - the coder to encode our state into 155- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 156 [super gtm_unitTestEncodeState:inCoder]; 157 BOOL isImageCell = NO; 158 if ([self hasValidObjectValue]) { 159 id val = [self objectValue]; 160 [inCoder encodeObject:val forKey:@"CellValue"]; 161 isImageCell = [val isKindOfClass:[NSImage class]]; 162 } 163 if (!isImageCell) { 164 // Image cells have a title that includes addresses that aren't going 165 // to be constant, so we don't encode them. All the info we need 166 // is going to be in the CellValue encoding. 167 [inCoder encodeObject:[self title] forKey:@"CellTitle"]; 168 } 169 ENCODE_NSINTEGER(inCoder, [self state], @"CellState"); 170 ENCODE_NSINTEGER(inCoder, [self tag], @"CellTag"); 171} 172 173@end 174 175@implementation NSImage (GTMUnitTestingAdditions) 176 177- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 178 [super gtm_unitTestEncodeState:inCoder]; 179 [inCoder encodeObject:NSStringFromSize([self size]) forKey:@"ImageSize"]; 180 [inCoder encodeObject:[self name] forKey:@"ImageName"]; 181} 182 183- (CGImageRef)gtm_unitTestImage { 184 // Create up a context 185 NSSize size = [self size]; 186 NSRect rect = GTMNSRectOfSize(size); 187 CGSize cgSize = GTMNSSizeToCGSize(size); 188 CGContextRef contextRef = GTMCreateUnitTestBitmapContextOfSizeWithData(cgSize, 189 NULL); 190 NSGraphicsContext *bitmapContext 191 = [NSGraphicsContext graphicsContextWithGraphicsPort:contextRef flipped:NO]; 192 _GTMDevAssert(bitmapContext, @"Couldn't create ns bitmap context"); 193 194 [NSGraphicsContext saveGraphicsState]; 195 [NSGraphicsContext setCurrentContext:bitmapContext]; 196 [self drawInRect:rect fromRect:rect operation:NSCompositeCopy fraction:1.0]; 197 198 CGImageRef image = CGBitmapContextCreateImage(contextRef); 199 CFRelease(contextRef); 200 [NSGraphicsContext restoreGraphicsState]; 201 return (CGImageRef)GTMCFAutorelease(image); 202} 203 204@end 205 206@implementation NSMenu (GTMUnitTestingAdditions) 207 208// Encodes the state of an object in a manner suitable for comparing 209// against a master state file so we can determine whether the 210// object is in a suitable state. 211// 212// Arguments: 213// inCoder - the coder to encode our state into 214- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 215 [super gtm_unitTestEncodeState:inCoder]; 216 // Hack here to work around 217 // rdar://5881796 Application menu item title wrong when accessed programatically 218 // which causes us to have different results on x86_64 vs x386. 219 // Hack is braced intentionally. We don't record the title of the 220 // "application" menu or it's menu title because they are wrong on 32 bit. 221 // They appear to work right on 64bit. 222 { 223 NSMenu *mainMenu = [NSApp mainMenu]; 224 NSMenu *appleMenu = [[mainMenu itemAtIndex:0] submenu]; 225 if (![self isEqual:appleMenu]) { 226 [inCoder encodeObject:[self title] forKey:@"MenuTitle"]; 227 } 228 } 229 // Descend down into the menuitems allowing them to store their state 230 NSMenuItem *menuItem = nil; 231 int i = 0; 232 GTM_FOREACH_OBJECT(menuItem, [self itemArray]) { 233 [inCoder encodeObject:menuItem 234 forKey:[NSString stringWithFormat:@"MenuItem %d", i]]; 235 ++i; 236 } 237} 238 239@end 240 241@implementation NSMenuItem (GTMUnitTestingAdditions) 242 243- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 244 [super gtm_unitTestEncodeState:inCoder]; 245 // Hack here to work around 246 // rdar://5881796 Application menu item title wrong when accessed programatically 247 // which causes us to have different results on x86_64 vs x386. 248 // See comment above. 249 { 250 NSMenu *mainMenu = [NSApp mainMenu]; 251 NSMenuItem *appleMenuItem = [mainMenu itemAtIndex:0]; 252 if (![self isEqual:appleMenuItem]) { 253 [inCoder encodeObject:[self title] forKey:@"MenuItemTitle"]; 254 } 255 } 256 [inCoder encodeObject:[self keyEquivalent] forKey:@"MenuItemKeyEquivalent"]; 257 [inCoder encodeBool:[self isSeparatorItem] forKey:@"MenuItemIsSeparator"]; 258 ENCODE_NSINTEGER(inCoder, [self state], @"MenuItemState"); 259 [inCoder encodeBool:[self isEnabled] forKey:@"MenuItemIsEnabled"]; 260 [inCoder encodeBool:[self isAlternate] forKey:@"MenuItemIsAlternate"]; 261 [inCoder encodeObject:[self toolTip] forKey:@"MenuItemTooltip"]; 262 ENCODE_NSINTEGER(inCoder, [self tag], @"MenuItemTag"); 263 ENCODE_NSINTEGER(inCoder, [self indentationLevel], @"MenuItemIndentationLevel"); 264 265 // Do our submenu if neccessary 266 if ([self hasSubmenu]) { 267 [inCoder encodeObject:[self submenu] forKey:@"MenuItemSubmenu"]; 268 } 269} 270 271@end 272 273@implementation NSTabView (GTMUnitTestingAdditions) 274 275// Encodes the state of an object in a manner suitable for comparing 276// against a master state file so we can determine whether the 277// object is in a suitable state. 278// 279// Arguments: 280// inCoder - the coder to encode our state into 281- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 282 [super gtm_unitTestEncodeState:inCoder]; 283 NSTabViewItem *tab = nil; 284 int i = 0; 285 GTM_FOREACH_OBJECT(tab, [self tabViewItems]) { 286 NSString *key = [NSString stringWithFormat:@"TabItem %d", i]; 287 [inCoder encodeObject:tab forKey:key]; 288 i = i + 1; 289 } 290} 291 292@end 293 294@implementation NSTabViewItem (GTMUnitTestingAdditions) 295 296// Encodes the state of an object in a manner suitable for comparing 297// against a master state file so we can determine whether the 298// object is in a suitable state. 299// 300// Arguments: 301// inCoder - the coder to encode our state into 302- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 303 [super gtm_unitTestEncodeState:inCoder]; 304 [inCoder encodeObject:[self label] forKey:@"TabLabel"]; 305 [inCoder encodeObject:[self view] forKey:@"TabView"]; 306} 307 308@end 309 310@implementation NSToolbar (GTMUnitTestingAdditions) 311 312// Encodes the state of an object in a manner suitable for comparing 313// against a master state file so we can determine whether the 314// object is in a suitable state. 315// 316// Arguments: 317// inCoder - the coder to encode our state into 318- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 319 [super gtm_unitTestEncodeState:inCoder]; 320 NSToolbarItem *item = nil; 321 NSUInteger i = 0; 322 GTM_FOREACH_OBJECT(item, [self items]) { 323 NSString *key 324 = [NSString stringWithFormat:@"ToolbarItem %lu", (unsigned long)i]; 325 [inCoder encodeObject:item forKey:key]; 326 i = i + 1; 327 } 328} 329 330@end 331 332@implementation NSToolbarItem (GTMUnitTestingAdditions) 333 334// Encodes the state of an object in a manner suitable for comparing 335// against a master state file so we can determine whether the 336// object is in a suitable state. 337// 338// Arguments: 339// inCoder - the coder to encode our state into 340- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 341 [super gtm_unitTestEncodeState:inCoder]; 342 [inCoder encodeObject:[self label] forKey:@"Label"]; 343 [inCoder encodeObject:[self paletteLabel] forKey:@"PaletteLabel"]; 344 [inCoder encodeObject:[self toolTip] forKey:@"ToolTip"]; 345 NSView *view = [self view]; 346 if (view) { 347 [inCoder encodeObject:view forKey:@"View"]; 348 } 349} 350 351@end 352 353@implementation NSMatrix (GTMUnitTestingAdditions) 354 355// Encodes the state of an object in a manner suitable for comparing 356// against a master state file so we can determine whether the 357// object is in a suitable state. 358// 359// Arguments: 360// inCoder - the coder to encode our state into 361- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 362 [super gtm_unitTestEncodeState:inCoder]; 363 364 ENCODE_NSINTEGER(inCoder, [self mode], @"MatrixMode"); 365 ENCODE_NSINTEGER(inCoder, [self numberOfRows], @"MatrixRowCount"); 366 ENCODE_NSINTEGER(inCoder, [self numberOfColumns], @"MatrixColumnCount"); 367 [inCoder encodeBool:[self allowsEmptySelection] 368 forKey:@"MatrixAllowEmptySelection"]; 369 [inCoder encodeBool:[self isSelectionByRect] forKey:@"MatrixSelectionByRect"]; 370 [inCoder encodeBool:[self autosizesCells] forKey:@"MatrixAutosizesCells"]; 371 [inCoder encodeSize:[self intercellSpacing] forKey:@"MatrixIntercellSpacing"]; 372 373 [inCoder encodeObject:[self prototype] forKey:@"MatrixCellPrototype"]; 374 375 // Dump the list of cells 376 NSCell *cell; 377 long i = 0; 378 GTM_FOREACH_OBJECT(cell, [self cells]) { 379 [inCoder encodeObject:cell 380 forKey:[NSString stringWithFormat:@"MatrixCell %ld", i]]; 381 ++i; 382 } 383} 384 385@end 386 387@implementation NSBox (GTMUnitTestingAdditions) 388 389// Encodes the state of an object in a manner suitable for comparing 390// against a master state file so we can determine whether the 391// object is in a suitable state. 392// 393// Arguments: 394// inCoder - the coder to encode our state into 395- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 396 [super gtm_unitTestEncodeState:inCoder]; 397 398 [inCoder encodeObject:[self title] forKey:@"BoxTitle"]; 399 ENCODE_NSINTEGER(inCoder, [self titlePosition], @"BoxTitlePosition"); 400 ENCODE_NSINTEGER(inCoder, [self boxType], @"BoxType"); 401 ENCODE_NSINTEGER(inCoder, [self borderType], @"BoxBorderType"); 402 // 10.5+ [inCoder encodeBool:[self isTransparent] forKey:@"BoxIsTransparent"]; 403} 404 405@end 406 407@implementation NSSegmentedControl (GTMUnitTestingAdditions) 408 409// Encodes the state of an NSSegmentedControl and all its segments. 410// 411// Arguments: 412// inCoder - the coder to encode state into 413- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 414 [super gtm_unitTestEncodeState:inCoder]; 415 416 NSInteger segmentCount = [self segmentCount]; 417 ENCODE_NSINTEGER(inCoder, segmentCount, @"SegmentCount"); 418 419 for (NSInteger i = 0; i < segmentCount; ++i) { 420 NSString *key = [NSString stringWithFormat:@"Segment %ld", (long)i]; 421 [inCoder encodeObject:[self labelForSegment:i] forKey:key]; 422 } 423} 424 425@end 426 427@implementation NSComboBox (GTMUnitTestingAdditions) 428 429- (BOOL)gtm_shouldEncodeStateForSubviews { 430 // Subclass of NSTextView, don't want subviews for the same reason. 431 return NO; 432} 433 434// Encodes the state of an NSSegmentedControl and all its segments. 435// 436// Arguments: 437// inCoder - the coder to encode state into 438- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 439 [super gtm_unitTestEncodeState:inCoder]; 440 441 NSInteger aCount = [self numberOfItems]; 442 ENCODE_NSINTEGER(inCoder, aCount, @"ComboBoxNumberOfItems"); 443 aCount = [self numberOfVisibleItems]; 444 ENCODE_NSINTEGER(inCoder, aCount, @"ComboBoxNumberOfVisibleItems"); 445 446 // Include the objectValues if it doesn't use a data source. 447 if (![self usesDataSource]) { 448 NSArray *objectValues = [self objectValues]; 449 for (NSUInteger i = 0; i < [objectValues count]; ++i) { 450 id value = [objectValues objectAtIndex:i]; 451 if ([value isKindOfClass:[NSString class]]) { 452 NSString *key = [NSString stringWithFormat:@"ComboBoxObjectValue %lu", 453 (unsigned long)i]; 454 [inCoder encodeObject:value forKey:key]; 455 } 456 } 457 } 458} 459 460@end 461 462 463// A view that allows you to delegate out drawing using the formal 464// GTMUnitTestViewDelegate protocol above. This is useful when writing up unit 465// tests for visual elements. 466// Your test will often end up looking like this: 467// - (void)testFoo { 468// GTMAssertDrawingEqualToFile(self, NSMakeSize(200, 200), @"Foo", nil, nil); 469// } 470// and your testSuite will also implement the unitTestViewDrawRect method to do 471// it's actual drawing. The above creates a view of size 200x200 that draws 472// it's content using |self|'s unitTestViewDrawRect method and compares it to 473// the contents of the file Foo.tif to make sure it's valid 474@implementation GTMUnitTestView 475 476- (id)initWithFrame:(NSRect)frame 477 drawer:(id<GTMUnitTestViewDrawer>)drawer 478 contextInfo:(void*)contextInfo { 479 self = [super initWithFrame:frame]; 480 if (self != nil) { 481 drawer_ = [drawer retain]; 482 contextInfo_ = contextInfo; 483 } 484 return self; 485} 486 487- (void)dealloc { 488 [drawer_ release]; 489 [super dealloc]; 490} 491 492 493- (void)drawRect:(NSRect)rect { 494 [drawer_ gtm_unitTestViewDrawRect:rect contextInfo:contextInfo_]; 495} 496 497 498@end 499 500@implementation NSView (GTMUnitTestingAdditions) 501 502// Returns an image containing a representation of the object 503// suitable for use in comparing against a master image. 504// Does all of it's drawing with smoothfonts and antialiasing off 505// to avoid issues with font smoothing settings and antialias differences 506// between ppc and x86. 507// 508// Returns: 509// an image of the object 510- (CGImageRef)gtm_unitTestImage { 511 // Create up a context 512 NSRect bounds = [self bounds]; 513 CGSize cgSize = GTMNSSizeToCGSize(bounds.size); 514 CGContextRef contextRef = GTMCreateUnitTestBitmapContextOfSizeWithData(cgSize, 515 NULL); 516 NSGraphicsContext *bitmapContext 517 = [NSGraphicsContext graphicsContextWithGraphicsPort:contextRef flipped:NO]; 518 _GTMDevAssert(bitmapContext, @"Couldn't create ns bitmap context"); 519 520 // Save our state and turn off font smoothing and antialias. 521 CGContextSaveGState(contextRef); 522 CGContextSetShouldSmoothFonts(contextRef, false); 523 CGContextSetShouldAntialias(contextRef, false); 524 [self displayRectIgnoringOpacity:bounds inContext:bitmapContext]; 525 526 CGImageRef image = CGBitmapContextCreateImage(contextRef); 527 CFRelease(contextRef); 528 return (CGImageRef)GTMCFAutorelease(image); 529} 530 531// Returns whether gtm_unitTestEncodeState should recurse into subviews 532// of a particular view. 533// If you have "Full keyboard access" in the 534// Keyboard & Mouse > Keyboard Shortcuts preferences pane set to "Text boxes 535// and Lists only" that Apple adds a set of subviews to NSTextFields. So in the 536// case of NSTextFields we don't want to recurse into their subviews. There may 537// be other cases like this, so instead of specializing gtm_unitTestEncodeState: to 538// look for NSTextFields, NSTextFields will just not allow us to recurse into 539// their subviews. 540// 541// Returns: 542// should gtm_unitTestEncodeState pick up subview state. 543- (BOOL)gtm_shouldEncodeStateForSubviews { 544 return YES; 545} 546 547// Encodes the state of an object in a manner suitable for comparing 548// against a master state file so we can determine whether the 549// object is in a suitable state. 550// 551// Arguments: 552// inCoder - the coder to encode our state into 553- (void)gtm_unitTestEncodeState:(NSCoder*)inCoder { 554 [super gtm_unitTestEncodeState:inCoder]; 555 [inCoder encodeBool:[self isHidden] forKey:@"ViewIsHidden"]; 556 [inCoder encodeObject:[self toolTip] forKey:@"ViewToolTip"]; 557 NSArray *supportedAttrs = [self accessibilityAttributeNames]; 558 if ([supportedAttrs containsObject:NSAccessibilityHelpAttribute]) { 559 NSString *help 560 = [self accessibilityAttributeValue:NSAccessibilityHelpAttribute]; 561 [inCoder encodeObject:help forKey:@"ViewAccessibilityHelp"]; 562 } 563 if ([supportedAttrs containsObject:NSAccessibilityDescriptionAttribute]) { 564 NSString *description 565 = [self accessibilityAttributeValue:NSAccessibilityDescriptionAttribute]; 566 [inCoder encodeObject:description forKey:@"ViewAccessibilityDescription"]; 567 } 568 NSMenu *menu = [self menu]; 569 if (menu) { 570 [inCoder encodeObject:menu forKey:@"ViewMenu"]; 571 } 572 if ([self gtm_shouldEncodeStateForSubviews]) { 573 NSView *subview = nil; 574 int i = 0; 575 GTM_FOREACH_OBJECT(subview, [self subviews]) { 576 [inCoder encodeObject:subview forKey:[NSString stringWithFormat:@"ViewSubView %d", i]]; 577 i = i + 1; 578 } 579 } 580} 581 582@end 583