diff --git a/Integration Tests/SLTestCaseViewController.h b/Integration Tests/SLTestCaseViewController.h index 9365110..1df1135 100644 --- a/Integration Tests/SLTestCaseViewController.h +++ b/Integration Tests/SLTestCaseViewController.h @@ -73,6 +73,9 @@ This method should create a view hierarchy and then assign the root view of the hierarchy to the [view](-[UIView view]) property. + + Subclasses can use the methods in the `ConvenienceViews` category + to load views for standard test scenarios. The default implementation of this method is a no-op. @@ -93,3 +96,25 @@ - (instancetype)initWithTestCaseWithSelector:(SEL)testCase; @end + + +/** + The methods in the `SLTestCaseViewController (ConvenienceViews)` category + may be used to load views for certain standard test scenarios. + + A subclass of `SLTestCaseViewController` would call one of these methods + from within its implementation of `-loadViewForTestCase:`. + */ +@interface SLTestCaseViewController (ConvenienceViews) + +/** + Creates a generic view. + + This method is to be used by test controllers that don't need to + display any particular interface, perhaps because they're testing + a system modal view/view controller presented in front of their view + or because they're testing some aspect of Subliminal unrelated to their view. + */ +- (void)loadGenericView; + +@end diff --git a/Integration Tests/SLTestCaseViewController.m b/Integration Tests/SLTestCaseViewController.m index e63d071..c46d63d 100644 --- a/Integration Tests/SLTestCaseViewController.m +++ b/Integration Tests/SLTestCaseViewController.m @@ -62,3 +62,29 @@ - (UIRectEdge)edgesForExtendedLayout { } @end + + +@implementation SLTestCaseViewController (ConvenienceViews) + +- (void)loadGenericView { + UIView *view = [[UIView alloc] initWithFrame:self.navigationController.view.bounds]; + view.backgroundColor = [UIColor whiteColor]; + + UIFont *nothingToShowHereFont = [UIFont systemFontOfSize:18.0f]; + NSString *nothingToShowHereText = @"Nothing to show here."; + CGRect nothingToShowHereBounds = CGRectIntegral((CGRect){ .size = [nothingToShowHereText sizeWithFont:nothingToShowHereFont + constrainedToSize:CGSizeMake(3 * CGRectGetWidth(view.bounds) / 4.0f, CGFLOAT_MAX)] }); + UILabel *nothingToShowHereLabel = [[UILabel alloc] initWithFrame:nothingToShowHereBounds]; + nothingToShowHereLabel.backgroundColor = view.backgroundColor; + nothingToShowHereLabel.font = nothingToShowHereFont; + nothingToShowHereLabel.numberOfLines = 0; + nothingToShowHereLabel.text = nothingToShowHereText; + nothingToShowHereLabel.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin; + + [view addSubview:nothingToShowHereLabel]; + nothingToShowHereLabel.center = CGPointMake(CGRectGetMidX(view.bounds), CGRectGetMidY(view.bounds)); + + self.view = view; +} + +@end diff --git a/Integration Tests/Tests/SLActionSheetTest.m b/Integration Tests/Tests/SLActionSheetTest.m new file mode 100644 index 0000000..1b1a38d --- /dev/null +++ b/Integration Tests/Tests/SLActionSheetTest.m @@ -0,0 +1,112 @@ +// +// SLActionSheetTest.m +// Subliminal +// +// Created by Jeffrey Wear on 5/26/14. +// Copyright (c) 2014 Inkling. All rights reserved. +// + +#import "SLIntegrationTest.h" + +@interface SLActionSheetTest : SLIntegrationTest + +@end + +@implementation SLActionSheetTest + ++ (NSString *)testCaseViewControllerClassName { + return @"SLActionSheetTestViewController"; +} + +- (void)tearDownTestCaseWithSelector:(SEL)testCaseSelector { + SLAskApp(dismissActionSheet); + + if (([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) && + ((testCaseSelector == @selector(testButtonsIncludesTheCancelButtonIfPresent)) || + (testCaseSelector == @selector(testCanMatchCancelButton)))) { + SLAskApp(dismissPopover); + } + + [super tearDownTestCaseWithSelector:testCaseSelector]; +} + +- (void)testCanMatchActionSheet { + SLAskApp1(showActionSheetWithInfo:, @{ @"title": @"Action!" }); + + CGRect actionSheetRect, expectedActionSheetRect = [SLAskApp(actionSheetFrameValue) CGRectValue]; + SLAssertNoThrow(actionSheetRect = [[SLActionSheet currentActionSheet] rect], + @"An action sheet should exist."); + SLAssertTrue(CGRectEqualToRect(actionSheetRect, expectedActionSheetRect), + @"The action sheet's frame does not match the expected frame."); +} + +- (void)testCanReadTitle { + NSString *actualTitle, *expectedTitle = @"Action!"; + SLAskApp1(showActionSheetWithInfo:, @{ @"title": expectedTitle }); + + SLAssertNoThrow(actualTitle = [[SLActionSheet currentActionSheet] title], + @"Should have been able to read the action sheet's title."); + SLAssertTrue([actualTitle isEqualToString:expectedTitle], + @"The action sheet's title was not equal to the expected value."); +} + +- (void)testCanMatchButtons { + NSArray *actualButtonTitles, *expectedButtonTitles = @[ @"Other Button", @"Other Other Button" ]; + // Note: the action sheet should not be presented with a cancel button here, + // for contrast with `-testThatButtonsIncludesTheCancelButtonIfPresent`. + SLAskApp1(showActionSheetWithInfo:, (@{ + @"otherButtonTitle1": expectedButtonTitles[0], + @"otherButtonTitle2": expectedButtonTitles[1] + })); + + SLAssertNoThrow(actualButtonTitles = [[[SLActionSheet currentActionSheet] buttons] valueForKey:@"label"], + @"Should have been able to retrieve the titles of the action sheet buttons."); + SLAssertTrue([actualButtonTitles isEqualToArray:expectedButtonTitles], + @"The titles of the action sheet buttons were not read as expected: %@", actualButtonTitles); +} + +- (void)testButtonsIncludesTheCancelButtonIfPresent { + NSArray *actualButtonTitles, *expectedButtonTitles = @[ @"Other Button", @"Other Other Button", @"Cancel" ]; + // `-testCanMatchButtons` verifies that the array will not include an invalid + // cancel button element if the cancel button isn't present. + SLAskApp1(showActionSheetWithInfo:, (@{ + @"otherButtonTitle1": expectedButtonTitles[0], + @"otherButtonTitle2": expectedButtonTitles[1], + @"cancelButtonTitle": expectedButtonTitles[2], + // On the iPad, `UIActionSheet` will not show a cancel button unless it is shown in a popover. + @"showInPopover": @([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) + })); + + SLAssertNoThrow(actualButtonTitles = [[[SLActionSheet currentActionSheet] buttons] valueForKey:@"label"], + @"Should have been able to retrieve the titles of the action sheet buttons."); + SLAssertTrue([actualButtonTitles isEqualToArray:expectedButtonTitles], + @"The titles of the action sheet buttons were not read as expected: %@", actualButtonTitles); +} + +- (void)testCanMatchCancelButton { + NSString *actualCancelButtonTitle, *expectedCancelButtonTitle = @"Get Out of Here"; + SLAskApp1(showActionSheetWithInfo:, (@{ + @"cancelButtonTitle": expectedCancelButtonTitle, + // On the iPad, `UIActionSheet` will not show a cancel button unless it is shown in a popover. + @"showInPopover": @([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) + })); + [self wait:5.0]; + + SLAssertNoThrow(actualCancelButtonTitle = [[[SLActionSheet currentActionSheet] cancelButton] label], + @"Should have been able to retrieve the title of the action sheet's cancel button."); + SLAssertTrue([actualCancelButtonTitle isEqualToString:expectedCancelButtonTitle], + @"The action sheet's cancel button did not have the expected title."); +} + +- (void)testCancelButtonIsInvalidIfThereIsNoCancelButton { + SLAskApp1(showActionSheetWithInfo:, @{ @"title": @"Action!" }); + + BOOL cancelButtonIsValid = NO; + SLAssertNoThrow(cancelButtonIsValid = [[[SLActionSheet currentActionSheet] cancelButton] isValid], + @"It should have been safe to access the action sheet's cancel button even though the button doesn't exist."); + SLAssertFalse(cancelButtonIsValid, + @"The action sheet's cancel button should be invalid."); + +} + +@end diff --git a/Integration Tests/Tests/SLActionSheetTestViewController.m b/Integration Tests/Tests/SLActionSheetTestViewController.m new file mode 100644 index 0000000..41b80df --- /dev/null +++ b/Integration Tests/Tests/SLActionSheetTestViewController.m @@ -0,0 +1,81 @@ +// +// SLActionSheetTestViewController.m +// Subliminal +// +// Created by Jeffrey Wear on 5/26/14. +// Copyright (c) 2014 Inkling. All rights reserved. +// + +#import "SLTestCaseViewController.h" + +#import + +@interface SLActionSheetTestViewController : SLTestCaseViewController + +@end + +@implementation SLActionSheetTestViewController { + UIActionSheet *_actionSheet; + UIPopoverController *_popoverController; +} + +- (void)loadViewForTestCase:(SEL)testCase { + [self loadGenericView]; +} + +- (instancetype)initWithTestCaseWithSelector:(SEL)testCase { + self = [super initWithTestCaseWithSelector:testCase]; + if (self) { + SLTestController *testController = [SLTestController sharedTestController]; + [testController registerTarget:self forAction:@selector(showActionSheetWithInfo:)]; + [testController registerTarget:self forAction:@selector(dismissActionSheet)]; + [testController registerTarget:self forAction:@selector(actionSheetFrameValue)]; + } + return self; +} + +- (void)dealloc { + [[SLTestController sharedTestController] deregisterTarget:self]; +} + +#pragma mark - App hooks + +- (void)showActionSheetWithInfo:(NSDictionary *)info { + NSAssert(!_actionSheet, @"An action sheet is already showing."); + + _actionSheet = [[UIActionSheet alloc] initWithTitle:info[@"title"] + delegate:nil + cancelButtonTitle:info[@"cancelButtonTitle"] + destructiveButtonTitle:nil + otherButtonTitles:info[@"otherButtonTitle1"], info[@"otherButtonTitle2"], nil]; + + if ([info[@"showInPopover"] boolValue]) { + SLActionSheetTestViewController *contentViewController = [[SLActionSheetTestViewController alloc] initWithTestCaseWithSelector:self.testCase]; + _popoverController = [[UIPopoverController alloc] initWithContentViewController:contentViewController]; + _popoverController.popoverContentSize = CGSizeMake(320.0f, 480.0f); + [_popoverController presentPopoverFromRect:CGRectInset((CGRect){ .origin = self.view.center }, -10.0f, -10.0f) + inView:self.view.superview + permittedArrowDirections:UIPopoverArrowDirectionAny animated:NO]; + + // register this here vs. in init so the controller we just presented doesn't steal it + [[SLTestController sharedTestController] registerTarget:self forAction:@selector(dismissPopover)]; + [_actionSheet showInView:_popoverController.contentViewController.view]; + } else { + [_actionSheet showInView:self.view]; + } +} + +- (void)dismissActionSheet { + [_actionSheet dismissWithClickedButtonIndex:0 animated:NO]; + _actionSheet = nil; +} + +- (NSValue *)actionSheetFrameValue { + return [NSValue valueWithCGRect:_actionSheet.accessibilityFrame]; +} + +- (void)dismissPopover { + [_popoverController dismissPopoverAnimated:NO]; +} + +@end diff --git a/Integration Tests/Tests/SLAlertTestViewController.m b/Integration Tests/Tests/SLAlertTestViewController.m index 427d3e1..2de62f9 100644 --- a/Integration Tests/Tests/SLAlertTestViewController.m +++ b/Integration Tests/Tests/SLAlertTestViewController.m @@ -37,24 +37,7 @@ @implementation SLAlertTestViewController { - (void)loadViewForTestCase:(SEL)testCase { // Since we're testing UIAlertViews in this test, // we don't need any particular view. - UIView *view = [[UIView alloc] initWithFrame:self.navigationController.view.bounds]; - view.backgroundColor = [UIColor whiteColor]; - - UIFont *nothingToShowHereFont = [UIFont systemFontOfSize:18.0f]; - NSString *nothingToShowHereText = @"Nothing to show here."; - CGSize nothingToShowHereSize = [nothingToShowHereText sizeWithFont:nothingToShowHereFont - constrainedToSize:CGSizeMake(3 * CGRectGetWidth(view.bounds) / 4.0f, CGFLOAT_MAX)]; - UILabel *nothingToShowHereLabel = [[UILabel alloc] initWithFrame:(CGRect){CGPointZero, nothingToShowHereSize}]; - nothingToShowHereLabel.backgroundColor = view.backgroundColor; - nothingToShowHereLabel.font = nothingToShowHereFont; - nothingToShowHereLabel.numberOfLines = 0; - nothingToShowHereLabel.text = nothingToShowHereText; - nothingToShowHereLabel.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin; - - [view addSubview:nothingToShowHereLabel]; - nothingToShowHereLabel.center = CGPointMake(CGRectGetMidX(view.bounds), CGRectGetMidY(view.bounds)); - - self.view = view; + [self loadGenericView]; } - (instancetype)initWithTestCaseWithSelector:(SEL)testCase { diff --git a/Integration Tests/Tests/SLGeometryTest.m b/Integration Tests/Tests/SLGeometryTest.m index 4a7f30e..819bf9b 100644 --- a/Integration Tests/Tests/SLGeometryTest.m +++ b/Integration Tests/Tests/SLGeometryTest.m @@ -6,10 +6,10 @@ // Copyright (c) 2013 Inkling. All rights reserved. // -#import #import "SLIntegrationTest.h" -#import "SLGeometry.h" -#import "SLTerminal.h" + +#import +#import @interface SLGeometryTest : SLIntegrationTest @@ -21,12 +21,56 @@ + (NSString *)testCaseViewControllerClassName { return @"SLGeometryTestViewController"; } -- (void)testSLCGRectFromUIARectConvertsCorrectly -{ - const CGRect UIARect = SLCGRectFromUIARect(@"UIATarget.localTarget().frontMostApp().navigationBar().rect()"); - const CGRect UIKitRect = [SLAskApp(navigationBarFrameValue) CGRectValue]; +- (void)testSLUIARectFromCGRect { + NSString *const expectedRect = @"{ origin: { x: 5.0, y: 10.0 }, size: { width: 50.0, height: 100.0 } }"; + NSString *const actualRect = SLUIARectFromCGRect(CGRectMake(5.0, 10.0, 50.0, 100.0)); + + SLAssertTrue([[[SLTerminal sharedTerminal] evalFunctionWithName:SLUIARectEqualToRectFunctionName() + withArgs:(@[ actualRect, expectedRect ])] boolValue], + @"`SLUIARectFromCGRect` did not return the expected value."); +} + +- (void)testSLCGRectFromUIARect { + const CGRect expectedRect = CGRectMake(5.0, 10.0, 50.0, 100.0); + const CGRect actualRect = SLCGRectFromUIARect(@"{ origin: { x: 5.0, y: 10.0 }, size: { width: 50.0, height: 100.0 } }"); - SLAssertTrue(CGRectEqualToRect(UIARect, UIKitRect), @"The frame of the main window should be the same when coming from UIAutomation or UIKit"); + SLAssertTrue(CGRectEqualToRect(actualRect, expectedRect), + @"`SLCGRectFromUIARect` did not return the expected value."); +} + +- (void)testSLUIARectEqualToRect { + NSString *const rect1 = @"{ origin: { x: 5.0, y: 10.0 }, size: { width: 50.0, height: 100.0 } }"; + NSString *const rect2 = @"{ origin: { x: 5.0, y: 10.0 }, size: { width: 50.0, height: 115.0 } }"; + + SLAssertTrue([[[SLTerminal sharedTerminal] evalFunctionWithName:SLUIARectEqualToRectFunctionName() + withArgs:(@[ rect1, rect1 ])] boolValue], + @"Two identical rects should be equal."); + SLAssertTrue([[[SLTerminal sharedTerminal] evalFunctionWithName:SLUIARectEqualToRectFunctionName() + withArgs:(@[ @"null", @"null" ])] boolValue], + @"Two null rects should be equal."); + SLAssertFalse([[[SLTerminal sharedTerminal] evalFunctionWithName:SLUIARectEqualToRectFunctionName() + withArgs:(@[ rect1, rect2 ])] boolValue], + @"Two different rects should not be equal."); +} + +- (void)testSLUIARectContainsRect { + NSString *const containerRect = @"{ origin: { x: 5.0, y: 10.0 }, size: { width: 50.0, height: 100.0 } }"; + NSString *const containedWithinRect = @"{ origin: { x: 10.0, y: 15.0 }, size: { width: 30.0, height: 50.0 } }"; + NSString *const intersectingRect = @"{ origin: { x: 10.0, y: 15.0 }, size: { width: 50.0, height: 100.0 } }"; + NSString *const nonIntersectingRect = @"{ origin: { x: 65.0, y: 120.0 }, size: { width: 50.0, height: 100.0 } }"; + + SLAssertTrue([[[SLTerminal sharedTerminal] evalFunctionWithName:SLUIARectContainsRectFunctionName() + withArgs:(@[ containerRect, containerRect ])] boolValue], + @"A rect should contain itself."); + SLAssertTrue([[[SLTerminal sharedTerminal] evalFunctionWithName:SLUIARectContainsRectFunctionName() + withArgs:(@[ containerRect, containedWithinRect ])] boolValue], + @"A rect should contain a rect within itself."); + SLAssertFalse([[[SLTerminal sharedTerminal] evalFunctionWithName:SLUIARectContainsRectFunctionName() + withArgs:(@[ containerRect, intersectingRect])] boolValue], + @"A rect should not contain a partially-intersecting rect."); + SLAssertFalse([[[SLTerminal sharedTerminal] evalFunctionWithName:SLUIARectContainsRectFunctionName() + withArgs:(@[ containerRect, nonIntersectingRect])] boolValue], + @"A rect should not contain a non-intersecting rect."); } @end diff --git a/Integration Tests/Tests/SLGeometryTestViewController.m b/Integration Tests/Tests/SLGeometryTestViewController.m index 544c766..7b87f47 100644 --- a/Integration Tests/Tests/SLGeometryTestViewController.m +++ b/Integration Tests/Tests/SLGeometryTestViewController.m @@ -7,58 +7,23 @@ // #import "SLTestCaseViewController.h" -#import "SLLogger.h" -#import "SLTestController.h" -#import "SLTestController+AppHooks.h" @interface SLGeometryTestViewController : SLTestCaseViewController -@property (nonatomic, strong) UIView *rectView; - @end @implementation SLGeometryTestViewController - (instancetype)initWithTestCaseWithSelector:(SEL)testCase { - self = [super initWithTestCaseWithSelector:testCase]; - if (self) { - SLTestController *testController = [SLTestController sharedTestController]; - [testController registerTarget:self forAction:@selector(navigationBarFrameValue)]; - } - return self; -} - -- (NSValue *)navigationBarFrameValue -{ - return [NSValue valueWithCGRect:self.navigationController.navigationBar.frame]; + return [super initWithTestCaseWithSelector:testCase]; } - (void)loadViewForTestCase:(SEL)testCase { - UIView *view = [[UIView alloc] initWithFrame:self.navigationController.view.bounds]; - view.backgroundColor = [UIColor whiteColor]; - - UIFont *nothingToShowHereFont = [UIFont systemFontOfSize:18.0f]; - NSString *nothingToShowHereText = @"Nothing to show here."; - CGSize nothingToShowHereSize = [nothingToShowHereText sizeWithFont:nothingToShowHereFont - constrainedToSize:CGSizeMake(3 * CGRectGetWidth(view.bounds) / 4.0f, CGFLOAT_MAX)]; - UILabel *nothingToShowHereLabel = [[UILabel alloc] initWithFrame:(CGRect){CGPointZero, nothingToShowHereSize}]; - nothingToShowHereLabel.backgroundColor = view.backgroundColor; - nothingToShowHereLabel.font = nothingToShowHereFont; - nothingToShowHereLabel.numberOfLines = 0; - nothingToShowHereLabel.text = nothingToShowHereText; - nothingToShowHereLabel.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin; - - [view addSubview:nothingToShowHereLabel]; - nothingToShowHereLabel.center = CGPointMake(CGRectGetMidX(view.bounds), CGRectGetMidY(view.bounds)); - - self.view = view; -} - -- (void)dealloc -{ - [[SLTestController sharedTestController] deregisterTarget:self]; + // Since we're just testing the geometry functions, + // we don't require any particular view. + [self loadGenericView]; } @end diff --git a/Integration Tests/Tests/SLNavigationBarTest.m b/Integration Tests/Tests/SLNavigationBarTest.m new file mode 100644 index 0000000..d6edf6a --- /dev/null +++ b/Integration Tests/Tests/SLNavigationBarTest.m @@ -0,0 +1,109 @@ +// +// SLNavigationBarTest.m +// Subliminal +// +// Created by Jeffrey Wear on 5/26/14. +// Copyright (c) 2014 Inkling. All rights reserved. +// + +#import "SLIntegrationTest.h" + +@interface SLNavigationBarTest : SLIntegrationTest + +@end + +@implementation SLNavigationBarTest { + NSString *_leftButtonTitle, *_rightButtonTitle; +} + ++ (NSString *)testCaseViewControllerClassName { + return @"SLNavigationBarTestViewController"; +} + +- (void)setUpTestCaseWithSelector:(SEL)testCaseSelector { + [super setUpTestCaseWithSelector:testCaseSelector]; + + if (testCaseSelector == @selector(testCurrentNavigationBarMatchesFrontmostNavigationBar_iPad)) { + SLAskApp(presentBarInFormSheet); + } else if (testCaseSelector == @selector(testCanMatchLeftButton)) { + // replace the nav bar back button with our own left button so we can control the title + // (the navigation bar might replace the back button's title with "Back" if space is tight) + _leftButtonTitle = @"Test"; + SLAskApp1(addLeftButtonWithTitle:, _leftButtonTitle); + } else if (testCaseSelector == @selector(testLeftButtonIsInvalidIfThereIsNoLeftButton)) { + // The regular nav bar's left button is the automatic "Back" button. + SLAskApp(presentBarWithoutLeftButton); + } else if (testCaseSelector == @selector(testCanMatchRightButton)) { + _rightButtonTitle = @"Test"; + SLAskApp1(addRightButtonWithTitle:, _rightButtonTitle); + } +} + +- (void)tearDownTestCaseWithSelector:(SEL)testCaseSelector { + if (testCaseSelector == @selector(testCurrentNavigationBarMatchesFrontmostNavigationBar_iPad)) { + SLAskApp(dismissBarInFormSheet); + } else if (testCaseSelector == @selector(testLeftButtonIsInvalidIfThereIsNoLeftButton)) { + SLAskApp(dismissBarWithoutLeftButton); + } + + [super tearDownTestCaseWithSelector:testCaseSelector]; +} + +- (void)testCanMatchNavigationBar { + CGRect navigationBarRect, expectedNavigationBarRect = [SLAskApp(navigationBarFrameValue) CGRectValue]; + SLAssertNoThrow(navigationBarRect = [[SLNavigationBar currentNavigationBar] rect], + @"The navigation bar should exist."); + SLAssertTrue(CGRectEqualToRect(navigationBarRect, expectedNavigationBarRect), + @"The navigation bar's frame does not match the expected navigation bar frame."); +} + +// it's only likely that there would be multiple navigation bars visible +// when a view controller is modally presented on the iPad +- (void)testCurrentNavigationBarMatchesFrontmostNavigationBar_iPad { + NSString *actualTitle, *expectedTitle = @"Child VC"; + SLAssertNoThrow(actualTitle = [[SLNavigationBar currentNavigationBar] title], + @"Should have been able to retrieve the title of the frontmost navigation bar."); + SLAssertTrue([actualTitle isEqualToString:expectedTitle], + @"Title of frontmost navigation bar was not equal to expected value."); +} + +- (void)testCanReadTitle { + NSString *expectedNavigationBarTitle = NSStringFromSelector(_cmd); + NSString *actualNavigationBarTitle = [[SLNavigationBar currentNavigationBar] title]; + SLAssertTrue([actualNavigationBarTitle isEqualToString:expectedNavigationBarTitle], + @"The navigation bar's title was not equal to the expected value."); +} + +- (void)testCanMatchLeftButton { + NSString *actualLeftButtonTitle, *expectedLeftButtonTitle = _leftButtonTitle; + SLAssertNoThrow(actualLeftButtonTitle = [[[SLNavigationBar currentNavigationBar] leftButton] label], + @"Should have been able to retrieve the title of the navigation bar's left button."); + SLAssertTrue([actualLeftButtonTitle isEqualToString:expectedLeftButtonTitle], + @"The navigation bar's left button did not have the expected title."); +} + +- (void)testLeftButtonIsInvalidIfThereIsNoLeftButton { + BOOL leftButtonIsValid = NO; + SLAssertNoThrow(leftButtonIsValid = [[[SLNavigationBar currentNavigationBar] leftButton] isValid], + @"It should have been safe to access the navigation bar's left button even though the button doesn't exist."); + SLAssertFalse(leftButtonIsValid, + @"The navigation bar's left button should be invalid."); +} + +- (void)testCanMatchRightButton { + NSString *actualRightButtonTitle, *expectedRightButtonTitle = _rightButtonTitle; + SLAssertNoThrow(actualRightButtonTitle = [[[SLNavigationBar currentNavigationBar] rightButton] label], + @"Should have been able to retrieve the title of the navigation bar's right button."); + SLAssertTrue([actualRightButtonTitle isEqualToString:expectedRightButtonTitle], + @"The navigation bar's right button did not have the expected title."); +} + +- (void)testRightButtonIsInvalidIfThereIsNoRightButton { + BOOL rightButtonIsValid = NO; + SLAssertNoThrow(rightButtonIsValid = [[[SLNavigationBar currentNavigationBar] rightButton] isValid], + @"It should have been safe to access the navigation bar's right button even though the button doesn't exist."); + SLAssertFalse(rightButtonIsValid, + @"The navigation bar's right button should be invalid."); +} + +@end diff --git a/Integration Tests/Tests/SLNavigationBarTestViewController.m b/Integration Tests/Tests/SLNavigationBarTestViewController.m new file mode 100644 index 0000000..59464a1 --- /dev/null +++ b/Integration Tests/Tests/SLNavigationBarTestViewController.m @@ -0,0 +1,95 @@ +// +// SLNavigationBarTestViewController.m +// Subliminal +// +// Created by Jeffrey Wear on 5/26/14. +// Copyright (c) 2014 Inkling. All rights reserved. +// + +#import "SLTestCaseViewController.h" + +#import + +@interface SLNavigationBarTestViewController : SLTestCaseViewController +@property (nonatomic, weak) SLNavigationBarTestViewController *parentVC; +@end + +@implementation SLNavigationBarTestViewController + +- (void)loadViewForTestCase:(SEL)testCase { + [self loadGenericView]; + + if (self.parentVC) { + UINavigationBar *navBar = [[UINavigationBar alloc] initWithFrame:CGRectZero]; + [navBar pushNavigationItem:[[UINavigationItem alloc] initWithTitle:@"Child VC"] animated:NO]; + + CGRect navBarFrame = (CGRect){ .size = [navBar sizeThatFits:CGSizeZero] }; + navBarFrame.origin = self.view.bounds.origin; + navBar.frame = navBarFrame; + [self.view addSubview:navBar]; + } +} + +- (instancetype)initWithTestCaseWithSelector:(SEL)testCase { + self = [super initWithTestCaseWithSelector:testCase]; + if (self) { + SLTestController *testController = [SLTestController sharedTestController]; + [testController registerTarget:self forAction:@selector(navigationBarFrameValue)]; + [testController registerTarget:self forAction:@selector(presentBarWithoutLeftButton)]; + [testController registerTarget:self forAction:@selector(presentBarInFormSheet)]; + [testController registerTarget:self forAction:@selector(addLeftButtonWithTitle:)]; + [testController registerTarget:self forAction:@selector(addRightButtonWithTitle:)]; + } + return self; +} + +- (void)dealloc { + [[SLTestController sharedTestController] deregisterTarget:self]; +} + +#pragma mark - App hooks + +- (NSValue *)navigationBarFrameValue { + return [NSValue valueWithCGRect:[self.navigationController.navigationBar accessibilityFrame]]; +} + +// there's no way to entirely remove a left (back) button from a navigation controller's nav bar, only hide it +// so we must present a new view controller with its own, unmanaged nav bar +- (void)presentBarWithoutLeftButton { + SLNavigationBarTestViewController *childVC = [[SLNavigationBarTestViewController alloc] initWithTestCaseWithSelector:self.testCase]; + childVC.parentVC = self; + [self presentViewController:childVC animated:NO completion:nil]; + + // register this here so the child controller doesn't steal it + [[SLTestController sharedTestController] registerTarget:self forAction:@selector(dismissBarWithoutLeftButton)]; +} + +- (void)dismissBarWithoutLeftButton { + [self dismissViewControllerAnimated:NO completion:nil]; +} + +- (void)presentBarInFormSheet { + SLNavigationBarTestViewController *childVC = [[SLNavigationBarTestViewController alloc] initWithTestCaseWithSelector:self.testCase]; + childVC.parentVC = self; + childVC.modalPresentationStyle = UIModalPresentationFormSheet; + [self presentViewController:childVC animated:NO completion:nil]; + + // register this here so the child controller doesn't steal it + [[SLTestController sharedTestController] registerTarget:self forAction:@selector(dismissBarInFormSheet)]; +} + +- (void)dismissBarInFormSheet { + [self dismissViewControllerAnimated:NO completion:nil]; +} + +- (void)addLeftButtonWithTitle:(NSString *)title { + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:title + style:UIBarButtonItemStylePlain target:nil action:NULL]; +} + +- (void)addRightButtonWithTitle:(NSString *)title { + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:title + style:UIBarButtonItemStylePlain target:nil action:NULL]; +} + +@end diff --git a/Integration Tests/Tests/SLPopoverTestViewController.m b/Integration Tests/Tests/SLPopoverTestViewController.m index ee6b286..b95e616 100644 --- a/Integration Tests/Tests/SLPopoverTestViewController.m +++ b/Integration Tests/Tests/SLPopoverTestViewController.m @@ -41,9 +41,9 @@ - (void)loadViewForTestCase:(SEL)testCase { UIFont *nothingToShowHereFont = [UIFont systemFontOfSize:18.0f]; NSString *nothingToShowHereText = @"Nothing to show here."; - CGSize nothingToShowHereSize = [nothingToShowHereText sizeWithFont:nothingToShowHereFont - constrainedToSize:CGSizeMake(3 * CGRectGetWidth(view.bounds) / 4.0f, CGFLOAT_MAX)]; - _label = [[UILabel alloc] initWithFrame:(CGRect){CGPointZero, nothingToShowHereSize}]; + CGRect nothingToShowHereBounds = CGRectIntegral((CGRect){ .size = [nothingToShowHereText sizeWithFont:nothingToShowHereFont + constrainedToSize:CGSizeMake(3 * CGRectGetWidth(view.bounds) / 4.0f, CGFLOAT_MAX)] }); + _label = [[UILabel alloc] initWithFrame:nothingToShowHereBounds]; _label.backgroundColor = view.backgroundColor; _label.font = nothingToShowHereFont; _label.numberOfLines = 0; diff --git a/Integration Tests/Tests/SLTerminalTest.m b/Integration Tests/Tests/SLTerminalTest.m index 6d75b8e..381a4a6 100644 --- a/Integration Tests/Tests/SLTerminalTest.m +++ b/Integration Tests/Tests/SLTerminalTest.m @@ -143,6 +143,18 @@ - (void)testCanLoadAndEvaluateFunction { SLAssertTrue([result isEqual:@12], @"Function did not evaluate to expected result."); } +- (void)testCanLoadAndEvaluateFunctionTakingNoArguments { + SLAssertNoThrow([[SLTerminal sharedTerminal] loadFunctionWithName:_functionName + params:nil + body:@"return 'Hello World';"], + @"Should not have thrown."); + id result; + SLAssertNoThrow((result = [[SLTerminal sharedTerminal] evalFunctionWithName:_functionName + withArgs:nil]), + @"Should not have thrown."); + SLAssertTrue([result isEqual:@"Hello World"], @"Function did not evaluate to expected result."); +} + - (void)testCanLoadAndEvaluateFunctionUsingConvenienceWrapper { id result; SLAssertNoThrow((result = [[SLTerminal sharedTerminal] evalFunctionWithName:_functionName diff --git a/Integration Tests/Tests/SLTerminalTestViewController.m b/Integration Tests/Tests/SLTerminalTestViewController.m index 7bccb0f..2830ebe 100644 --- a/Integration Tests/Tests/SLTerminalTestViewController.m +++ b/Integration Tests/Tests/SLTerminalTestViewController.m @@ -42,9 +42,9 @@ - (void)loadViewForTestCase:(SEL)testCase { UIFont *nothingToShowHereFont = [UIFont systemFontOfSize:18.0f]; NSString *nothingToShowHereText = @"Nothing to show here."; - CGSize nothingToShowHereSize = [nothingToShowHereText sizeWithFont:nothingToShowHereFont - constrainedToSize:CGSizeMake(3 * CGRectGetWidth(view.bounds) / 4.0f, CGFLOAT_MAX)]; - UILabel *nothingToShowHereLabel = [[UILabel alloc] initWithFrame:(CGRect){CGPointZero, nothingToShowHereSize}]; + CGRect nothingToShowHereBounds = CGRectIntegral((CGRect){ .size = [nothingToShowHereText sizeWithFont:nothingToShowHereFont + constrainedToSize:CGSizeMake(3 * CGRectGetWidth(view.bounds) / 4.0f, CGFLOAT_MAX)] }); + UILabel *nothingToShowHereLabel = [[UILabel alloc] initWithFrame:nothingToShowHereBounds]; nothingToShowHereLabel.backgroundColor = view.backgroundColor; nothingToShowHereLabel.font = nothingToShowHereFont; nothingToShowHereLabel.numberOfLines = 0; diff --git a/Integration Tests/Tests/SLWindowTestViewController.m b/Integration Tests/Tests/SLWindowTestViewController.m index 8abe220..3d9550f 100644 --- a/Integration Tests/Tests/SLWindowTestViewController.m +++ b/Integration Tests/Tests/SLWindowTestViewController.m @@ -33,24 +33,7 @@ @implementation SLWindowTestViewController - (void)loadViewForTestCase:(SEL)testCase { // Since we're testing the window in this test, // we don't need any particular view. - UIView *view = [[UIView alloc] initWithFrame:self.navigationController.view.bounds]; - view.backgroundColor = [UIColor whiteColor]; - - UIFont *nothingToShowHereFont = [UIFont systemFontOfSize:18.0f]; - NSString *nothingToShowHereText = @"Nothing to show here."; - CGSize nothingToShowHereSize = [nothingToShowHereText sizeWithFont:nothingToShowHereFont - constrainedToSize:CGSizeMake(3 * CGRectGetWidth(view.bounds) / 4.0f, CGFLOAT_MAX)]; - UILabel *nothingToShowHereLabel = [[UILabel alloc] initWithFrame:(CGRect){CGPointZero, nothingToShowHereSize}]; - nothingToShowHereLabel.backgroundColor = view.backgroundColor; - nothingToShowHereLabel.font = nothingToShowHereFont; - nothingToShowHereLabel.numberOfLines = 0; - nothingToShowHereLabel.text = nothingToShowHereText; - nothingToShowHereLabel.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin; - - [view addSubview:nothingToShowHereLabel]; - nothingToShowHereLabel.center = CGPointMake(CGRectGetMidX(view.bounds), CGRectGetMidY(view.bounds)); - - self.view = view; + [self loadGenericView]; } - (instancetype)initWithTestCaseWithSelector:(SEL)testCase { diff --git a/Sources/Classes/Internal/Terminal/SLTerminal+ConvenienceFunctions.h b/Sources/Classes/Internal/Terminal/SLTerminal+ConvenienceFunctions.h index f31f286..bfeb3c9 100644 --- a/Sources/Classes/Internal/Terminal/SLTerminal+ConvenienceFunctions.h +++ b/Sources/Classes/Internal/Terminal/SLTerminal+ConvenienceFunctions.h @@ -29,6 +29,21 @@ It is oftentimes more readable and efficient to evaluate some bit of JavaScript by defining a generic function and then calling it with specific arguments, rather than formatting and evaluating a long block of statements each time. + + It is recommended that the methods in this category be used both to define _and_ + evaluate functions. However, if this is not possible--if a function needs to + be called from some other bit of JavaScript, for instance--a function may be + referenced (once defined) by formatting together the terminal's namespace + and the name of the function: + + NSString *functionName = @"SLAddTwoNumbers"; + NSString *scriptNamespace = [[SLTerminal sharedTerminal] scriptNamespace]; + [[SLTerminal sharedTerminal] loadFunctionWithName:functionName + params:@[ @"one", @"two" ] + body:@"return one + two;"]; + // Contains @"36" + NSString *result = [[SLTerminal sharedTerminal] evalWithFormat:@"3 * %@.%@(5, 7)", scriptNamespace, functionName]; + */ @interface SLTerminal (ConvenienceFunctions) @@ -59,7 +74,8 @@ functions defined by Subliminal. Subliminal reserves the "SL" prefix. @param name The name of the function. - @param params The string names of the parameters of the function. + @param params The string names of the parameters of the function, + or `nil` if the function does not take any arguments. @param body The body of the function: one or more statements, with no function closure. @@ -92,7 +108,8 @@ After evaluation, `result` would contain `@"12"`. @param name The name of a function previously added to the terminal's namespace. - @param args The arguments to the function, as strings. + @param args The arguments to the function, as strings; + or `nil` if the function does not take any arguments. @return The result of evaluating the specified function, or `nil` if the function does not return a value. @@ -128,9 +145,11 @@ withArgs:@[ @"5", @"7" ]]; @param name The name of the function to add to the terminal's namespace, if necessary. - @param params The string names of the parameters of the function. + @param params The string names of the parameters of the function, + or `nil` if the function does not take any arguments. @param body The body of the function: one or more statements, with no function closure. - @param args The arguments to the function, as strings. + @param args The arguments to the function, as strings; + or `nil` if the function does not take any arguments. @return The result of evaluating the specified function, or `nil` if the function does not return a value. diff --git a/Sources/Classes/Internal/Terminal/SLTerminal+ConvenienceFunctions.m b/Sources/Classes/Internal/Terminal/SLTerminal+ConvenienceFunctions.m index cf0cb42..703e1f6 100644 --- a/Sources/Classes/Internal/Terminal/SLTerminal+ConvenienceFunctions.m +++ b/Sources/Classes/Internal/Terminal/SLTerminal+ConvenienceFunctions.m @@ -83,7 +83,7 @@ - (void)loadFunctionWithName:(NSString *)name params:(NSArray *)params body:(NSS return; } - NSString *paramList = [params componentsJoinedByString:@", "]; + NSString *paramList = params ? [params componentsJoinedByString:@", "] : @""; NSString *function = [NSString stringWithFormat:@"%@.%@ = function(%@){ %@ }", self.scriptNamespace, name, paramList, body]; NSString *loadedFunction = [self loadedFunctions][name]; if (!loadedFunction) { @@ -113,7 +113,7 @@ - (NSString *)evalFunctionWithName:(NSString *)name withArgs:(NSArray *)args { } NSAssert([self functionWithNameIsLoaded:name], @"No function with name %@ has been loaded.", name); - NSString *argList = [args componentsJoinedByString:@", "]; + NSString *argList = args ? [args componentsJoinedByString:@", "] : @""; return [self evalWithFormat:@"%@.%@(%@)", self.scriptNamespace, name, argList]; } diff --git a/Sources/Classes/UIAutomation/SLGeometry.h b/Sources/Classes/UIAutomation/SLGeometry.h index b4e48b8..7155055 100644 --- a/Sources/Classes/UIAutomation/SLGeometry.h +++ b/Sources/Classes/UIAutomation/SLGeometry.h @@ -9,25 +9,71 @@ #import #import -#pragma mark - CGRects +#pragma mark - Creating a JavaScript Primitive from a Struct Primitive +/// ---------------------------------------------------------------------- +/// @name Creating a JavaScript Primitive from a Struct Primitive +/// ---------------------------------------------------------------------- /** - Converts a `CGRect` to a JavaScript `Rect` object, as described in + Converts a `CGRect` to a string of JavaScript that evaluates to an equivalent + `Rect` object, as described in http://developer.apple.com/library/ios/#documentation/ToolsLanguages/Reference/UIATargetClassReference/UIATargetClass/UIATargetClass.html @param rect A non-null `CGRect`. - @return JavaScript which creates a `Rect` object. + @return JavaScript which evaluates to a `Rect` object equivalent to _rect_. @exception NSInternalInconsistencyException if `rect` is `CGRectNull`. */ NSString *SLUIARectFromCGRect(CGRect rect); +#pragma mark - Creating a Struct Primitive from a JavaScript Primitive +/// ---------------------------------------------------------------------- +/// @name Creating a Struct Primitive from a JavaScript Primitive +/// ---------------------------------------------------------------------- /** Converts a string of JavaScript (that evaluates to a `Rect` object) to a `CGRect`. - @param UIARect JavaScript which evaluates to a `Rect` object, e.g. + @param rect JavaScript which evaluates to a `Rect` object, e.g. `UIATarget.localTarget().frontMostApp().mainWindow().rect()`. - @return A `CGRect`, or `CGRectNull` if the _UIARect_ was `nil`. + @return A `CGRect`, or `CGRectNull` if _rect_ was `nil`. */ -CGRect SLCGRectFromUIARect(NSString *UIARect); +CGRect SLCGRectFromUIARect(NSString *rect); + +#pragma mark - Comparing Values +/// ------------------------------------------ +/// @name Comparing Values +/// ------------------------------------------ + +/** + Returns the name of a JavaScript function used to evaluate whether two `Rect` + objects are equal in size and position., loading it into the terminal's namespace + if necessary. + + This function accepts two parameters, _rect1_ and _rect2_, both `Rect` objects. + It returns `true` if _rect1_ and _rect2_ have equal size and origin values, or + if both rectangles are `null`; otherwise, `false`. + + @return The name of the JavaScript function used to evaluate whether two `Rect` + objects are equal in size and position. + */ +NSString *SLUIARectEqualToRectFunctionName(); + +#pragma mark - Checking for Membership +/// ------------------------------------------ +/// @name Checking for Membership +/// ------------------------------------------ + +/** + Returns the name of a JavaScript function used to evaluate whether a `Rect` + contains another `Rect`, loading it into the terminal's namespace if necessary. + + This function accepts two parameters, _rect1_ and _rect2_, both `Rect` objects. + It returns `true` if the rectangle specified by _rect2_ is contained in the + rectangle passed in _rect1_; otherwise, `false`. The first rectangle contains + the second if the union of the two rectangles is equal to the first rectangle. + + @return The name of the JavaScript function used to evaluate whether a `Rect` + contains another `Rect`. + */ +NSString *SLUIARectContainsRectFunctionName(); diff --git a/Sources/Classes/UIAutomation/SLGeometry.m b/Sources/Classes/UIAutomation/SLGeometry.m index 7704be1..67ec3bb 100644 --- a/Sources/Classes/UIAutomation/SLGeometry.m +++ b/Sources/Classes/UIAutomation/SLGeometry.m @@ -10,8 +10,7 @@ #import "SLTerminal+ConvenienceFunctions.h" -NSString *SLUIARectFromCGRect(CGRect rect) -{ +NSString *SLUIARectFromCGRect(CGRect rect) { NSCParameterAssert(!CGRectIsNull(rect)); return [NSString stringWithFormat:@"{origin:{x:%f,y:%f}, size:{width:%f, height:%f}}",rect.origin.x,rect.origin.y,rect.size.width,rect.size.height]; } @@ -26,3 +25,32 @@ CGRect SLCGRectFromUIARect(NSString *UIARect) { withArgs:@[ UIARect ]]; return ([CGRectString length] ? CGRectFromString(CGRectString) : CGRectNull); } + +NSString *SLUIARectEqualToRectFunctionName() { + static NSString *const SLUIARectEqualToRectFunctionName = @"SLUIARectEqualToRect"; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [[SLTerminal sharedTerminal] loadFunctionWithName:SLUIARectEqualToRectFunctionName + params:@[ @"rect1", @"rect2" ] + body:@"return ((!rect1 && !rect2) ||\ + ((rect2.origin.x === rect1.origin.x) &&\ + (rect2.origin.y === rect1.origin.y) &&\ + (rect2.size.width === rect1.size.width) &&\ + (rect2.size.height === rect1.size.height)));"]; + }); + return SLUIARectEqualToRectFunctionName; +} + +NSString *SLUIARectContainsRectFunctionName() { + static NSString *const SLUIARectContainsRectFunctionName = @"SLUIARectContainsRect"; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [[SLTerminal sharedTerminal] loadFunctionWithName:SLUIARectContainsRectFunctionName + params:@[ @"rect1", @"rect2" ] + body:@"return ((rect2.origin.x >= rect1.origin.x) &&\ + (rect2.origin.y >= rect1.origin.y) &&\ + ((rect2.origin.x + rect2.size.width) <= (rect1.origin.x + rect1.size.width)) &&\ + ((rect2.origin.y + rect2.size.height) <= (rect1.origin.y + rect1.size.height)));"]; + }); + return SLUIARectContainsRectFunctionName; +} diff --git a/Sources/Classes/UIAutomation/User Interface Elements/SLActionSheet.h b/Sources/Classes/UIAutomation/User Interface Elements/SLActionSheet.h new file mode 100644 index 0000000..0a60261 --- /dev/null +++ b/Sources/Classes/UIAutomation/User Interface Elements/SLActionSheet.h @@ -0,0 +1,70 @@ +// +// SLActionSheet.h +// Subliminal +// +// Created by Jeffrey Wear on 5/26/14. +// Copyright (c) 2014 Inkling. All rights reserved. +// + +#import "SLStaticElement.h" + +/** + `SLActionSheet` allows you to manipulate the buttons of action sheets + within your app. + + You can almost always access these elements directly, using instances of + `SLElement`. Assuming that an action sheet had a cancel button labeled "Cancel", + + [SLButton elementWithAccessibilityLabel:@"Cancel"] + + would return an element functionally identical to + + [[SLActionSheet currentActionSheet] cancelButton] + + However, you might use `SLActionSheet` to verify that the current action sheet's + buttons had the expected labels, or to match the action sheet of a + [remote view controller](http://oleb.net/blog/2012/10/remote-view-controllers-in-ios-6/) + (where `SLElement` could not examine the contents of the view). + */ +@interface SLActionSheet : SLStaticElement + +/** + Returns an object that represents the app's current action sheet, if any. + + This element will be [valid](-[SLUIAElement isValid]) if and only if the application + is currently showing an action sheet. + + @return An object that represents the app's current action sheet. + */ ++ (instancetype)currentActionSheet; + +/** + The string that appears in the title area of the action sheet, if any. + + @exception SLUIAElementInvalidException Raised if the action sheet is not + [valid](-[SLUIAElement isValid]) by the end of the [default timeout](+[SLUIAElement defaultTimeout]). + */ +@property (nonatomic, readonly) NSString *title; + +/** + The buttons in the action sheet, as instances of `SLUIAElement`. + + If the action sheet has a cancel button, this array will include that cancel button + as the last item in the array. + + @exception SLUIAElementInvalidException Raised if the action sheet is not + [valid](-[SLUIAElement isValid]) by the end of the [default timeout](+[SLUIAElement defaultTimeout]). + */ +@property (nonatomic, readonly) NSArray *buttons; + +/** + The cancel button in the action sheet, if any. + + If there is no cancel button, this element will not be [valid](-[SLUIAElement isValid]). + + @exception SLUIAElementInvalidException Raised if the action sheet is not + [valid](-[SLUIAElement isValid]) by the end of the [default timeout](+[SLUIAElement defaultTimeout]). + */ +@property (nonatomic, readonly) SLUIAElement *cancelButton; + +@end diff --git a/Sources/Classes/UIAutomation/User Interface Elements/SLActionSheet.m b/Sources/Classes/UIAutomation/User Interface Elements/SLActionSheet.m new file mode 100644 index 0000000..5ac1b44 --- /dev/null +++ b/Sources/Classes/UIAutomation/User Interface Elements/SLActionSheet.m @@ -0,0 +1,79 @@ +// +// SLActionSheet.m +// Subliminal +// +// Created by Jeffrey Wear on 5/26/14. +// Copyright (c) 2014 Inkling. All rights reserved. +// + +#import "SLActionSheet.h" + +#import "SLUIAElement+Subclassing.h" + +@implementation SLActionSheet { + SLStaticElement *_titleLabel; + SLStaticElement *_cancelButton; +} + ++ (instancetype)currentActionSheet { + NSString *UIARepresentation; + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { + UIARepresentation = @"UIATarget.localTarget().frontMostApp().actionSheet()"; + } else { + // on the iPad, action sheets are always presented in popovers + // (even if they're not presented from a view inside a popover) + // and `UIAApplication.actionSheet()` is nonfunctional + UIARepresentation = @"UIATarget.localTarget().frontMostApp().mainWindow().popover().actionSheet()"; + } + return [[self alloc] initWithUIARepresentation:UIARepresentation]; +} + +- (instancetype)initWithUIARepresentation:(NSString *)UIARepresentation { + self = [super initWithUIARepresentation:UIARepresentation]; + if (self) { + _titleLabel = [[SLStaticElement alloc] initWithUIARepresentation:[UIARepresentation stringByAppendingString:@".staticTexts()[0]"]]; + _cancelButton = [[SLStaticElement alloc] initWithUIARepresentation:[UIARepresentation stringByAppendingString:@".cancelButton()"]]; + } + return self; +} + +- (NSString *)title { + NSString *__block title; + + // Use this as convenience to perform the waiting and, if necessary, exception throwing + [self waitUntilTappable:NO thenPerformActionWithUIARepresentation:^(NSString *UIARepresentation) { + // The title label will not exist unless the view controller has a non-empty title. + // The default value of `-[UIActionSheet title]` is `nil`. + title = [_titleLabel isValid] ? [_titleLabel label] : nil; + } timeout:[[self class] defaultTimeout]]; + + return title; +} + +- (NSArray *)buttons { + __block NSUInteger numberOfButtons = 0; + [self waitUntilTappable:NO thenPerformActionWithUIARepresentation:^(NSString *UIARepresentation) { + numberOfButtons = [[[SLTerminal sharedTerminal] evalWithFormat:@"%@.buttons().length", UIARepresentation] unsignedIntegerValue]; + } timeout:[[self class] defaultTimeout]]; + + NSMutableArray *buttons = [[NSMutableArray alloc] initWithCapacity:numberOfButtons]; + for (NSUInteger buttonIndex = 0; buttonIndex < numberOfButtons; buttonIndex++) { + NSString *buttonRepresentation = [_UIARepresentation stringByAppendingFormat:@".buttons()[%lu]", (unsigned long)buttonIndex]; + SLStaticElement *button = [[SLStaticElement alloc] initWithUIARepresentation:buttonRepresentation]; + [buttons addObject:button]; + } + return [buttons copy]; +} + +- (SLUIAElement *)cancelButton { + SLUIAElement *__block cancelButton; + + // Use this as convenience to perform the waiting and, if necessary, exception throwing + [self waitUntilTappable:NO thenPerformActionWithUIARepresentation:^(NSString *UIARepresentation) { + cancelButton = _cancelButton; + } timeout:[[self class] defaultTimeout]]; + + return cancelButton; +} + +@end diff --git a/Sources/Classes/UIAutomation/User Interface Elements/SLNavigationBar.h b/Sources/Classes/UIAutomation/User Interface Elements/SLNavigationBar.h new file mode 100644 index 0000000..20ae8de --- /dev/null +++ b/Sources/Classes/UIAutomation/User Interface Elements/SLNavigationBar.h @@ -0,0 +1,77 @@ +// +// SLNavigationBar.h +// Subliminal +// +// Created by Jeffrey Wear on 5/26/14. +// Copyright (c) 2014 Inkling. All rights reserved. +// + +#import "SLStaticElement.h" + +/** + `SLNavigationBar` allows you to read the title of your app's navigation bar + and manipulate the navigation bar's left and right buttons. + + You can almost always access these elements directly, using instances of + `SLElement`. Assuming that the navigation bar's left button had the label "Cancel", + + [SLButton elementWithAccessibilityLabel:@"Cancel"] + + would return an element functionally identical to + + [[SLNavigationBar currentNavigationBar] leftButton] + + However, you might use `SLNavigationBar` to verify that the current navigation bar's + buttons had the expected labels, or to match the navigation bar of a + [remote view controller](http://oleb.net/blog/2012/10/remote-view-controllers-in-ios-6/) + (where `SLElement` could not examine the contents of the view). + */ +@interface SLNavigationBar : SLStaticElement + +/** + Returns an object that represents the app's current navigation bar, if any. + + This element will be [valid](-[SLUIAElement isValid]) if and only if the application + is currently showing a navigation bar. + + If there are multiple navigation bars visible onscreen, + this element will represent the frontmost bar. + + @return An object that represents the app's current navigation bar. + */ ++ (instancetype)currentNavigationBar; + +/** + The title (-[UIViewController title]) of the navigation controller's + top view controller (-[UINavigationController topViewController]). + + @exception SLUIAElementInvalidException Raised if the navigation bar is not + [valid](-[SLUIAElement isValid]) by the end of the [default timeout](+[SLUIAElement defaultTimeout]). + */ +@property (nonatomic, readonly) NSString *title; + +/** + The left button in the navigation bar, if any. + + If there is no left button, this element will not be [valid](-[SLUIAElement isValid]). + If the left button is the back button, and the back button is hidden (-[UINavigationItem hidesBackButton]), + it _may_ be invalid, depending on the platform--you should check that + [-isValidAndVisible](-[SLUIAElement isValidAndVisible]) returns `YES` before + attempting to access or manipulate the element. + + @exception SLUIAElementInvalidException Raised if the navigation bar is not + [valid](-[SLUIAElement isValid]) by the end of the [default timeout](+[SLUIAElement defaultTimeout]). + */ +@property (nonatomic, readonly) SLUIAElement *leftButton; + +/** + The right button in the navigation bar, if any. + + If there is no right button, this element will not be [valid](-[SLUIAElement isValid]). + + @exception SLUIAElementInvalidException Raised if the navigation bar is not + [valid](-[SLUIAElement isValid]) by the end of the [default timeout](+[SLUIAElement defaultTimeout]). + */ +@property (nonatomic, readonly) SLUIAElement *rightButton; + +@end diff --git a/Sources/Classes/UIAutomation/User Interface Elements/SLNavigationBar.m b/Sources/Classes/UIAutomation/User Interface Elements/SLNavigationBar.m new file mode 100644 index 0000000..6d4a233 --- /dev/null +++ b/Sources/Classes/UIAutomation/User Interface Elements/SLNavigationBar.m @@ -0,0 +1,94 @@ +// +// SLNavigationBar.m +// Subliminal +// +// Created by Jeffrey Wear on 5/26/14. +// Copyright (c) 2014 Inkling. All rights reserved. +// + +#import "SLNavigationBar.h" + +#import "SLUIAElement+Subclassing.h" + +@implementation SLNavigationBar { + SLStaticElement *_titleLabel; + SLStaticElement *_leftButton, *_rightButton; +} + ++ (instancetype)currentNavigationBar { + /* + On iOS 6, UIAutomation can get confused if there are multiple nav bars on-screen + (for instance, on the iPad with one in a modal controller and one in the background) + so rather than use `UIATarget.localTarget().frontMostApp().navigationBar()`, + we use the last (frontmost) nav bar within the main window. + + We define the navigation bar getter as a standalone function + (rather than an immediately-evaluated function expression) so that + the `-description` of the navigation bar will be more concise. + */ + static NSString *const kCurrentNavigationBarFunctionName = @"SLNavigationBarCurrentNavigationBar"; + [[SLTerminal sharedTerminal] loadFunctionWithName:kCurrentNavigationBarFunctionName + params:nil + body:[NSString stringWithFormat:@"\ + var navigationBars = UIATarget.localTarget().frontMostApp().mainWindow().navigationBars().toArray();\ + if (navigationBars.length) {\ + return navigationBars[navigationBars.length - 1];\ + } else {" + // return `UIAElementNil` + // I don't know how to create it, + // so get a reference to it by attempting to retrieve an element guaranteed not to exist + @"return UIATarget.localTarget().frontMostApp().elements()['%@: %p'];\ + }\ + ", NSStringFromClass(self), self]]; + + NSString *namespacedCurrentComposeViewFunctionName = [NSString stringWithFormat:@"%@.%@", + [[SLTerminal sharedTerminal] scriptNamespace], kCurrentNavigationBarFunctionName]; + return [[self alloc] initWithUIARepresentation:[NSString stringWithFormat:@"%@()", namespacedCurrentComposeViewFunctionName]]; +} + +- (instancetype)initWithUIARepresentation:(NSString *)UIARepresentation { + self = [super initWithUIARepresentation:UIARepresentation]; + if (self) { + _titleLabel = [[SLStaticElement alloc] initWithUIARepresentation:[UIARepresentation stringByAppendingString:@".staticTexts()[0]"]]; + _leftButton = [[SLStaticElement alloc] initWithUIARepresentation:[UIARepresentation stringByAppendingString:@".leftButton()"]]; + _rightButton = [[SLStaticElement alloc] initWithUIARepresentation:[UIARepresentation stringByAppendingString:@".rightButton()"]]; + } + return self; +} + +- (NSString *)title { + NSString *__block title; + + // Use this as convenience to perform the waiting and, if necessary, exception throwing + [self waitUntilTappable:NO thenPerformActionWithUIARepresentation:^(NSString *UIARepresentation) { + // The title label will not exist unless the view controller has a non-empty title. + // The default value of `-[UIViewController title]` is `nil`. + title = [_titleLabel isValid] ? [_titleLabel label] : nil; + } timeout:[[self class] defaultTimeout]]; + + return title; +} + +- (SLUIAElement *)leftButton { + SLUIAElement *__block leftButton; + + // Use this as convenience to perform the waiting and, if necessary, exception throwing + [self waitUntilTappable:NO thenPerformActionWithUIARepresentation:^(NSString *UIARepresentation) { + leftButton = _leftButton; + } timeout:[[self class] defaultTimeout]]; + + return leftButton; +} + +- (SLUIAElement *)rightButton { + SLUIAElement *__block rightButton; + + // Use this as convenience to perform the waiting and, if necessary, exception throwing + [self waitUntilTappable:NO thenPerformActionWithUIARepresentation:^(NSString *UIARepresentation) { + rightButton = _rightButton; + } timeout:[[self class] defaultTimeout]]; + + return rightButton; +} + +@end diff --git a/Sources/Classes/UIAutomation/User Interface Elements/SLStaticElement.h b/Sources/Classes/UIAutomation/User Interface Elements/SLStaticElement.h index 6d949c5..3ddfec2 100644 --- a/Sources/Classes/UIAutomation/User Interface Elements/SLStaticElement.h +++ b/Sources/Classes/UIAutomation/User Interface Elements/SLStaticElement.h @@ -55,7 +55,10 @@ does not have any properties that can be described by Subliminal without referencing private APIs). */ -@interface SLStaticElement : SLUIAElement +@interface SLStaticElement : SLUIAElement { + @protected + NSString *_UIARepresentation; +} /** Initializes and returns a newly allocated element with the specified diff --git a/Sources/Classes/UIAutomation/User Interface Elements/SLStaticElement.m b/Sources/Classes/UIAutomation/User Interface Elements/SLStaticElement.m index 0ffdf41..a2c1273 100644 --- a/Sources/Classes/UIAutomation/User Interface Elements/SLStaticElement.m +++ b/Sources/Classes/UIAutomation/User Interface Elements/SLStaticElement.m @@ -23,9 +23,7 @@ #import "SLStaticElement.h" #import "SLUIAElement+Subclassing.h" -@implementation SLStaticElement { - NSString *_UIARepresentation; -} +@implementation SLStaticElement - (instancetype)initWithUIARepresentation:(NSString *)UIARepresentation { NSParameterAssert([UIARepresentation length]); @@ -38,7 +36,8 @@ - (instancetype)initWithUIARepresentation:(NSString *)UIARepresentation { } - (NSString *)description { - return [NSString stringWithFormat:@"<%@>", NSStringFromClass([self class])]; + return [NSString stringWithFormat:@"<%@ UIARepresentation = '%@'>", + NSStringFromClass([self class]), [_UIARepresentation slStringByEscapingForJavaScriptLiteral]]; } - (BOOL)canDetermineTappability { diff --git a/Sources/Subliminal.h b/Sources/Subliminal.h index 33fcb34..d0136ea 100644 --- a/Sources/Subliminal.h +++ b/Sources/Subliminal.h @@ -30,10 +30,12 @@ #import "NSObject+SLAccessibilityDescription.h" #import "NSObject+SLAccessibilityHierarchy.h" #import "SLStaticElement.h" +#import "SLActionSheet.h" #import "SLAlert.h" #import "SLButton.h" #import "SLDatePicker.h" #import "SLKeyboard.h" +#import "SLNavigationBar.h" #import "SLPickerView.h" #import "SLPopover.h" #import "SLStaticText.h" diff --git a/Subliminal.xcodeproj/project.pbxproj b/Subliminal.xcodeproj/project.pbxproj index 264b91e..5002d82 100644 --- a/Subliminal.xcodeproj/project.pbxproj +++ b/Subliminal.xcodeproj/project.pbxproj @@ -110,6 +110,14 @@ F043A9BE1729CFFE00A4FD1D /* SLElementVisibilityTestElementHidden.xib in Resources */ = {isa = PBXBuildFile; fileRef = F043A9BD1729CFFE00A4FD1D /* SLElementVisibilityTestElementHidden.xib */; }; F043A9C01729EFD100A4FD1D /* SLElementVisibilityTestUserInteractionDisabled.xib in Resources */ = {isa = PBXBuildFile; fileRef = F043A9BF1729EFD100A4FD1D /* SLElementVisibilityTestUserInteractionDisabled.xib */; }; F043A9C7172A160600A4FD1D /* SLElementVisibilityTest.html in Resources */ = {isa = PBXBuildFile; fileRef = F043A9C6172A160600A4FD1D /* SLElementVisibilityTest.html */; }; + F052B0A519343A92004606C0 /* SLNavigationBar.h in Headers */ = {isa = PBXBuildFile; fileRef = F052B0A319343A92004606C0 /* SLNavigationBar.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F052B0A619343A92004606C0 /* SLNavigationBar.m in Sources */ = {isa = PBXBuildFile; fileRef = F052B0A419343A92004606C0 /* SLNavigationBar.m */; }; + F052B0AA19343DD8004606C0 /* SLNavigationBarTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F052B0A819343DD8004606C0 /* SLNavigationBarTest.m */; }; + F052B0AB19343DD8004606C0 /* SLNavigationBarTestViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F052B0A919343DD8004606C0 /* SLNavigationBarTestViewController.m */; }; + F052B0AE193451FC004606C0 /* SLActionSheet.h in Headers */ = {isa = PBXBuildFile; fileRef = F052B0AC193451FC004606C0 /* SLActionSheet.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F052B0AF193451FC004606C0 /* SLActionSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = F052B0AD193451FC004606C0 /* SLActionSheet.m */; }; + F052B0B319345F29004606C0 /* SLActionSheetTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F052B0B119345F29004606C0 /* SLActionSheetTest.m */; }; + F052B0B419345F29004606C0 /* SLActionSheetTestViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F052B0B219345F29004606C0 /* SLActionSheetTestViewController.m */; }; F05C4F90171406EF00A381BC /* SLTerminal+ConvenienceFunctions.h in Headers */ = {isa = PBXBuildFile; fileRef = F05C4F8E171406EF00A381BC /* SLTerminal+ConvenienceFunctions.h */; settings = {ATTRIBUTES = (Public, ); }; }; F05C4F91171406EF00A381BC /* SLTerminal+ConvenienceFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = F05C4F8F171406EF00A381BC /* SLTerminal+ConvenienceFunctions.m */; }; F05C51E5171C8AE000A381BC /* SLMainThreadRef.h in Headers */ = {isa = PBXBuildFile; fileRef = F05C51E3171C8AE000A381BC /* SLMainThreadRef.h */; settings = {ATTRIBUTES = (); }; }; @@ -361,6 +369,14 @@ F043A9BD1729CFFE00A4FD1D /* SLElementVisibilityTestElementHidden.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SLElementVisibilityTestElementHidden.xib; sourceTree = ""; }; F043A9BF1729EFD100A4FD1D /* SLElementVisibilityTestUserInteractionDisabled.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SLElementVisibilityTestUserInteractionDisabled.xib; sourceTree = ""; }; F043A9C6172A160600A4FD1D /* SLElementVisibilityTest.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = SLElementVisibilityTest.html; sourceTree = ""; }; + F052B0A319343A92004606C0 /* SLNavigationBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLNavigationBar.h; sourceTree = ""; }; + F052B0A419343A92004606C0 /* SLNavigationBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLNavigationBar.m; sourceTree = ""; }; + F052B0A819343DD8004606C0 /* SLNavigationBarTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLNavigationBarTest.m; sourceTree = ""; }; + F052B0A919343DD8004606C0 /* SLNavigationBarTestViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLNavigationBarTestViewController.m; sourceTree = ""; }; + F052B0AC193451FC004606C0 /* SLActionSheet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLActionSheet.h; sourceTree = ""; }; + F052B0AD193451FC004606C0 /* SLActionSheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLActionSheet.m; sourceTree = ""; }; + F052B0B119345F29004606C0 /* SLActionSheetTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLActionSheetTest.m; sourceTree = ""; }; + F052B0B219345F29004606C0 /* SLActionSheetTestViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLActionSheetTestViewController.m; sourceTree = ""; }; F05C4F8E171406EF00A381BC /* SLTerminal+ConvenienceFunctions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SLTerminal+ConvenienceFunctions.h"; sourceTree = ""; }; F05C4F8F171406EF00A381BC /* SLTerminal+ConvenienceFunctions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SLTerminal+ConvenienceFunctions.m"; sourceTree = ""; }; F05C51E3171C8AE000A381BC /* SLMainThreadRef.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLMainThreadRef.h; sourceTree = ""; }; @@ -720,6 +736,24 @@ name = "SLElement Matching Tests"; sourceTree = ""; }; + F052B0A719343DBA004606C0 /* SLNavigationBar Tests */ = { + isa = PBXGroup; + children = ( + F052B0A819343DD8004606C0 /* SLNavigationBarTest.m */, + F052B0A919343DD8004606C0 /* SLNavigationBarTestViewController.m */, + ); + name = "SLNavigationBar Tests"; + sourceTree = ""; + }; + F052B0B019345F14004606C0 /* SLActionSheet Tests */ = { + isa = PBXGroup; + children = ( + F052B0B119345F29004606C0 /* SLActionSheetTest.m */, + F052B0B219345F29004606C0 /* SLActionSheetTestViewController.m */, + ); + name = "SLActionSheet Tests"; + sourceTree = ""; + }; F0695D8016011515000B05D0 = { isa = PBXGroup; children = ( @@ -853,6 +887,8 @@ CAC3883E1643503C00F995F9 /* NSObject+SLAccessibilityHierarchy.m */, F089F98417445D9A00DF1F25 /* SLStaticElement.h */, F089F98517445D9A00DF1F25 /* SLStaticElement.m */, + F052B0AC193451FC004606C0 /* SLActionSheet.h */, + F052B0AD193451FC004606C0 /* SLActionSheet.m */, F0C07A361703F95B00C93F93 /* SLAlert.h */, F0C07A371703F95B00C93F93 /* SLAlert.m */, F0C07A451703FEF600C93F93 /* SLButton.h */, @@ -861,6 +897,8 @@ 622DA0B9194E2CB100EFFE05 /* SLDatePicker.m */, F0C07A511704011300C93F93 /* SLKeyboard.h */, F0C07A521704011300C93F93 /* SLKeyboard.m */, + F052B0A319343A92004606C0 /* SLNavigationBar.h */, + F052B0A419343A92004606C0 /* SLNavigationBar.m */, 622DA089194AF03E00EFFE05 /* SLPickerView.h */, 622DA08A194AF03E00EFFE05 /* SLPickerView.m */, F00800CC174C1C64001927AC /* SLPopover.h */, @@ -977,8 +1015,7 @@ F0AC80BE16BB542400C5D5C0 /* Tests */ = { isa = PBXGroup; children = ( - DB2627D817B96704009DA3A6 /* SLStatusBar Tests */, - 50A59BD317848CEE002A863A /* SLGeometry Tests */, + F052B0B019345F14004606C0 /* SLActionSheet Tests */, F07DA31F16E439B7004C2282 /* SLAlert Tests */, F00800BF174B2CB0001927AC /* SLButton Tests */, 622DA0AD194CCA7500EFFE05 /* SLDatePicker Tests */, @@ -987,11 +1024,14 @@ F077D70016D9D71E00908FF5 /* SLElement Visibility Tests */, F05263C316D2C2CF0090174F /* SLElement Matching Tests */, 0696BA5E16E013DF00DD70CF /* SLElement Gestures and Actions Tests */, + 50A59BD317848CEE002A863A /* SLGeometry Tests */, F089F9DC1745FB3800DF1F25 /* SLKeyboard Tests */, + F052B0A719343DBA004606C0 /* SLNavigationBar Tests */, 622DA09F194B796700EFFE05 /* SLPickerView Tests */, F00800D3174C2871001927AC /* SLPopover Tests */, F089F9FD1746B1D200DF1F25 /* SLStaticElement Tests */, 6256A2CD19476DA400C6507F /* SLStaticText Tests */, + DB2627D817B96704009DA3A6 /* SLStatusBar Tests */, 2C903BB917F525C900555317 /* SLSwitch Tests */, F01EBC951701150E00FF6A7C /* SLTextField Tests */, F0A3F63917A716FC007529C3 /* SLTextView Tests */, @@ -1102,6 +1142,7 @@ F0CEDA3116BF5FA5005FE8B9 /* SLTest+Internal.h in Headers */, F016493D16D42E3C000AEB50 /* SLTestController+Internal.h in Headers */, F0C07A381703F95B00C93F93 /* SLAlert.h in Headers */, + F052B0A519343A92004606C0 /* SLNavigationBar.h in Headers */, F0C07A3F1703F9A900C93F93 /* SLUIAElement+Subclassing.h in Headers */, F0C07A471703FEF600C93F93 /* SLButton.h in Headers */, F0C07A4B1704002100C93F93 /* SLTextField.h in Headers */, @@ -1111,6 +1152,7 @@ F05C4F90171406EF00A381BC /* SLTerminal+ConvenienceFunctions.h in Headers */, F05C51E5171C8AE000A381BC /* SLMainThreadRef.h in Headers */, F0A04E1D1749F70F002C7520 /* SLElement.h in Headers */, + F052B0AE193451FC004606C0 /* SLActionSheet.h in Headers */, 2CE9AA4C17E3A747007EF0B5 /* SLSwitch.h in Headers */, F089F98617445D9A00DF1F25 /* SLStaticElement.h in Headers */, F02DF30817EC064F00BE28BF /* UIScrollView+SLProgrammaticScrolling.h in Headers */, @@ -1381,6 +1423,7 @@ buildActionMask = 2147483647; files = ( 62009F8F196CB41E00419585 /* SLTestAssertions.m in Sources */, + F052B0AF193451FC004606C0 /* SLActionSheet.m in Sources */, 2CE9AA4D17E3A747007EF0B5 /* SLSwitch.m in Sources */, 62E7A634193EF84C00CB11AB /* SLStaticText.m in Sources */, 50F3E18E1783A60100C6BD1B /* SLGeometry.m in Sources */, @@ -1398,6 +1441,7 @@ CA75E78516697C0000D57E92 /* SLDevice.m in Sources */, F0C07A391703F95B00C93F93 /* SLAlert.m in Sources */, F0C07A481703FEF600C93F93 /* SLButton.m in Sources */, + F052B0A619343A92004606C0 /* SLNavigationBar.m in Sources */, F0C07A4C1704002100C93F93 /* SLTextField.m in Sources */, F0C07A501704009E00C93F93 /* SLWindow.m in Sources */, F0C07A541704011400C93F93 /* SLKeyboard.m in Sources */, @@ -1431,6 +1475,7 @@ F078C04A1808BF24000767D2 /* SLWebViewTestViewController.m in Sources */, 06953E3F178FDA7100B3D1B7 /* SLElementTouchAndHoldTest.m in Sources */, F01B2B6D16D2C55900DBA391 /* SLElementMatchingTest.m in Sources */, + F052B0AA19343DD8004606C0 /* SLNavigationBarTest.m in Sources */, F01B2B6E16D2C55900DBA391 /* SLElementMatchingTestViewController.m in Sources */, F0CEA00216D60EAD008A3B4A /* SLDeviceTest.m in Sources */, F0CEA00316D60EAD008A3B4A /* SLDeviceTestViewController.m in Sources */, @@ -1438,6 +1483,7 @@ 6256A2D419476E1F00C6507F /* SLStaticTextTest.m in Sources */, 0696BA5916E013D600DD70CF /* SLElementDraggingTestViewController.m in Sources */, F0AE4C8D16F7F92D00B2BB2B /* SLElementStateTest.m in Sources */, + F052B0B319345F29004606C0 /* SLActionSheetTest.m in Sources */, F0AE4C8E16F7F92D00B2BB2B /* SLElementStateTestViewController.m in Sources */, F07DA32D16E43AD3004C2282 /* SLAlertTest.m in Sources */, F07DA32E16E43AD3004C2282 /* SLAlertTestViewController.m in Sources */, @@ -1461,11 +1507,13 @@ F089F9E21745FB7E00DF1F25 /* SLKeyboardTest.m in Sources */, F089F9E31745FB7E00DF1F25 /* SLKeyboardTestViewController.m in Sources */, F05D2B061746B55C0089DB9E /* SLStaticElementTest.m in Sources */, + F052B0AB19343DD8004606C0 /* SLNavigationBarTestViewController.m in Sources */, F05D2B071746B55C0089DB9E /* SLStaticElementTestViewController.m in Sources */, F00800C7174B3349001927AC /* SLButtonTest.m in Sources */, F00800C8174B3349001927AC /* SLButtonTestViewController.m in Sources */, F00800E2174C2E0A001927AC /* SLPopoverTest.m in Sources */, F00800E3174C2E0A001927AC /* SLPopoverTestViewController.m in Sources */, + F052B0B419345F29004606C0 /* SLActionSheetTestViewController.m in Sources */, 0640035A178FDE7800479173 /* SLElementTouchAndHoldTestViewController.m in Sources */, DB2627DE17B96727009DA3A6 /* SLStatusBarTest.m in Sources */, F078C0491808BF24000767D2 /* SLWebViewTest.m in Sources */,