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/SLMailComposeViewTest.m b/Integration Tests/Tests/SLMailComposeViewTest.m new file mode 100644 index 0000000..7c1f75e --- /dev/null +++ b/Integration Tests/Tests/SLMailComposeViewTest.m @@ -0,0 +1,316 @@ +// +// SLMailComposeViewTest.m +// Subliminal +// +// Created by Jeffrey Wear on 5/24/14. +// Copyright (c) 2014 Inkling. All rights reserved. +// + +#import "SLIntegrationTest.h" + +#import + +@interface SLMailComposeViewTest : SLIntegrationTest + +@end + +@implementation SLMailComposeViewTest { + SLMailComposeView *_composeView; + NSDictionary *_messageInfo; +} + ++ (NSString *)testCaseViewControllerClassName { + return @"SLMailComposeViewTestViewController"; +} + +- (void)setUpTest { + [super setUpTest]; + + _composeView = [SLMailComposeView currentComposeView]; +} + +- (void)setUpTestCaseWithSelector:(SEL)testCaseSelector { + [super setUpTestCaseWithSelector:testCaseSelector]; + + // The matching test must present the controller itself. + if (testCaseSelector != @selector(testCanMatchComposeView)) { + NSDictionary *messageInfo = nil; + // all "can read" cases operate with all recipient fields populated, + // to make sure that we can distinguish the fields (something of an implementation test + if ((testCaseSelector == @selector(testCanReadToRecipients)) || + (testCaseSelector == @selector(testCanReadMultipleRecipients)) || + (testCaseSelector == @selector(testCanReadCcRecipients)) || + (testCaseSelector == @selector(testCanReadBccRecipients))) { + NSArray *toRecipients; + if (testCaseSelector == @selector(testCanReadMultipleRecipients)) { + toRecipients = @[ @"foo@example.com", @"bar@example.com" ]; + } else { + toRecipients = @[ @"foo@example.com" ]; + } + messageInfo = @{ + @"toRecipients": toRecipients, + @"ccRecipients": @[ @"baz@example.com" ], + @"bccRecipients": @[ @"bee@example.com" ] + }; + } else if (testCaseSelector == @selector(testCanReadSubject)) { + messageInfo = @{ @"subject": @"This Is a Subliminal Message" }; + } else if (testCaseSelector == @selector(testCanReadBody)) { + messageInfo = @{ @"body": @"That is, a message sent by Subliminal." }; + } else if ((testCaseSelector == @selector(testCanCancelAndDeleteDraft)) || + (testCaseSelector == @selector(testCanCancelAndSaveDraft))) { + messageInfo = @{ @"body": @"A Subliminal message." }; + } else if (testCaseSelector == @selector(testCanSendMessage)) { + messageInfo = @{ + @"toRecipients": @[ @"foo@example.com" ], + @"subject": @"This is a test message", + @"body": @"test" + }; + } + _messageInfo = messageInfo; + SLAskApp1(presentComposeViewControllerWithInfo:, _messageInfo); + SLWaitUntilTrue(SLAskAppYesNo(composeViewControllerIsPresented), 2.0); + } +} + +- (void)tearDownTestCaseWithSelector:(SEL)testCaseSelector { + SLAskApp(dismissComposeViewController); + SLWaitUntilTrue(!SLAskAppYesNo(composeViewControllerIsPresented), 2.0); + _messageInfo = nil; + + [super tearDownTestCaseWithSelector:testCaseSelector]; +} + +- (void)testCanMatchComposeView { + SLAssertFalse([_composeView isValid], @"The compose view should not exist."); + + SLAskApp1(presentComposeViewControllerWithInfo:, nil); + SLAssertTrueWithTimeout([_composeView isValidAndVisible], 2.0, + @"The compose view should be visible."); +} + +#pragma mark - Message Field Test Cases + +- (void)testCanReadToRecipients { + NSArray *actualRecipients, *expectedRecipients = _messageInfo[@"toRecipients"]; + SLAssertNoThrow(actualRecipients = _composeView.toRecipients, + @"The 'to' recipients should have been able to be read."); + SLAssertTrue([actualRecipients isEqualToArray:expectedRecipients], + @"The 'to' recipients were not read as expected."); +} + +- (void)testCanSetToRecipients { + NSArray *expectedRecipients = @[ @"foo@example.com" ]; + + SLAssertFalse([_composeView.toRecipients isEqualToArray:expectedRecipients], + @"The 'to' recipients must not be pre-populated to the expected value."); + SLAssertNoThrow(_composeView.toRecipients = expectedRecipients, + @"The 'to' recipients should have been able to be set."); + SLAssertTrue([_composeView.toRecipients isEqualToArray:expectedRecipients], + @"The 'to' recipients were not set as expected."); +} + +// This is something of an implementation test--the mail compose view truncates +// the display of secondary recipients when a recipient field is not focused. +// This covers the "to", "cc", and "bcc" fields. +- (void)testCanReadMultipleRecipients { + NSArray *actualRecipients, *expectedRecipients = _messageInfo[@"toRecipients"]; + SLAssertNoThrow(actualRecipients = _composeView.toRecipients, + @"The 'to' recipients should have been able to be read."); + SLAssertTrue([actualRecipients isEqualToArray:expectedRecipients], + @"The 'to' recipients were not read as expected."); +} + +// This covers the "to", "cc", and "bcc" fields. +- (void)testCanSetMultipleRecipients { + NSArray *expectedRecipients = @[ @"foo@example.com", @"bar@example.com" ]; + + SLAssertFalse([_composeView.toRecipients isEqualToArray:expectedRecipients], + @"The 'to' recipients must not be pre-populated to the expected value."); + SLAssertNoThrow(_composeView.toRecipients = expectedRecipients, + @"The 'to' recipients should have been able to be set."); + SLAssertTrue([_composeView.toRecipients isEqualToArray:expectedRecipients], + @"The 'to' recipients were not set as expected."); +} + +- (void)testCanReadCcRecipients { + NSArray *actualRecipients, *expectedRecipients = _messageInfo[@"ccRecipients"]; + SLAssertNoThrow(actualRecipients = _composeView.ccRecipients, + @"The 'cc' recipients should have been able to be read."); + SLAssertTrue([actualRecipients isEqualToArray:expectedRecipients], + @"The 'cc' recipients were not read as expected."); +} + +- (void)testCanSetCcRecipients { + NSArray *expectedRecipients = @[ @"foo@example.com" ]; + + SLAssertFalse([_composeView.ccRecipients isEqualToArray:expectedRecipients], + @"The 'cc' recipients must not be pre-populated to the expected value."); + SLAssertNoThrow(_composeView.ccRecipients = expectedRecipients, + @"The 'cc' recipients should have been able to be set."); + SLAssertTrue([_composeView.ccRecipients isEqualToArray:expectedRecipients], + @"The 'cc' recipients were not set as expected."); +} + +- (void)testCanReadBccRecipients { + NSArray *actualRecipients, *expectedRecipients = _messageInfo[@"bccRecipients"]; + SLAssertNoThrow(actualRecipients = _composeView.bccRecipients, + @"The 'bcc' recipients should have been able to be read."); + SLAssertTrue([actualRecipients isEqualToArray:expectedRecipients], + @"The 'bcc' recipients were not read as expected."); +} + +// This is tested because the "bcc" field is collapsed into the "cc" field if empty. +- (void)testCanReadEmptyBccRecipients { + NSArray *bccRecipients; + SLAssertNoThrow(bccRecipients = _composeView.bccRecipients, + @"The 'bcc' recipients should have been able to be read."); + SLAssertFalse([bccRecipients count], @"There should be no 'bcc' recipients."); +} + +- (void)testCanSetBccRecipients { + NSArray *expectedRecipients = @[ @"foo@example.com" ]; + + SLAssertFalse([_composeView.bccRecipients isEqualToArray:expectedRecipients], + @"The 'bcc' recipients must not be pre-populated to the expected value."); + SLAssertNoThrow(_composeView.bccRecipients = expectedRecipients, + @"The 'bcc' recipients should have been able to be set."); + SLAssertTrue([_composeView.bccRecipients isEqualToArray:expectedRecipients], + @"The 'bcc' recipients were not set as expected."); +} + +- (void)testCanReadSubject { + NSString *actualSubject, *expectedSubject = _messageInfo[@"subject"]; + + SLAssertNoThrow(actualSubject = _composeView.subject, + @"The subject should have been able to be read."); + SLAssertTrue([actualSubject isEqualToString:expectedSubject], + @"The subject was not read as expected: %@", actualSubject); +} + +- (void)testCanSetSubject { + NSString *expectedSubject = @"This Is a Subliminal Message"; + + SLAssertFalse([_composeView.subject isEqualToString:expectedSubject], + @"The subject must not be pre-populated to the expected value."); + SLAssertNoThrow(_composeView.subject = expectedSubject, + @"The subject should have been able to be set."); + SLAssertTrue([_composeView.subject isEqualToString:expectedSubject], + @"The subject was not set as expected."); +} + +- (void)testCanReadBody { + NSString *actualBody, *expectedBody = _messageInfo[@"body"]; + + SLAssertNoThrow(actualBody = _composeView.body, + @"The body should have been able to be read."); + // on iOS 6.1, the body will be suffixed with the signature "Sent from my iPhone Simulator" + SLAssertTrue([actualBody hasPrefix:expectedBody], + @"The body was not read as expected: %@", actualBody); +} + +- (void)testCanSetBody { + // this test inexplicably fails sometimes on Travis + // if it does, retry + NSUInteger tryCount = 0; + const NSUInteger kMaxTryCount = 2; + NSException *failureException; + do { + tryCount++; + failureException = nil; + if (tryCount > 1) { + SLAskApp(dismissComposeViewController); + SLWaitUntilTrue(!SLAskAppYesNo(composeViewControllerIsPresented), 2.0); + + SLAskApp1(presentComposeViewControllerWithInfo:, _messageInfo); + SLWaitUntilTrue(SLAskAppYesNo(composeViewControllerIsPresented), 2.0); + } + + @try { + NSString *expectedBody = @"A message sent by Subliminal."; + + SLAssertFalse([_composeView.body isEqualToString:expectedBody], + @"The body must not be pre-populated to the expected value."); + SLAssertNoThrow(_composeView.body = expectedBody, + @"The body should have been able to be set."); + SLAssertTrue([_composeView.body isEqualToString:expectedBody], + @"The body was not set as expected."); + } + @catch (NSException *exception) { + failureException = exception; + } + + } while (failureException && (tryCount < kMaxTryCount)); + + if (failureException) @throw failureException; +} + +#pragma mark - Sending Mail Test Cases + +- (void)testCanAbortEmptyMessage { + BOOL didCancelDraft; + SLAssertNoThrow(didCancelDraft = [_composeView cancelAndDeleteDraft:NO], + @"Should have been able to attempt to cancel and save draft."); + SLAssertFalse(didCancelDraft, + @"For the purposes of this test case, there should not have been a draft in progress to cancel."); + + SLAssertTrueWithTimeout(!SLAskAppYesNo(composeViewControllerIsPresented), 2.0, + @"Compose view should have been dismissed."); + MFMailComposeResult composeFinishResult; + [SLAskApp(composeViewControllerFinishResult) getValue:&composeFinishResult]; + SLAssertTrue(composeFinishResult == MFMailComposeResultCancelled, + @"Compose view was not cancelled as expected."); +} + +- (void)testCanCancelAndDeleteDraft { + BOOL didCancelDraft; + SLAssertNoThrow(didCancelDraft = [_composeView cancelAndDeleteDraft:YES], + @"Should have been able to cancel and save draft."); + SLAssertTrue(didCancelDraft, + @"For the purposes of this test case, there should have been a draft in progress to cancel."); + + SLAssertTrueWithTimeout(!SLAskAppYesNo(composeViewControllerIsPresented), 2.0, + @"Compose view should have been dismissed."); + MFMailComposeResult composeFinishResult; + [SLAskApp(composeViewControllerFinishResult) getValue:&composeFinishResult]; + SLAssertTrue(composeFinishResult == MFMailComposeResultCancelled, + @"The draft was not cancelled as expected."); +} + +- (void)testCanCancelAndSaveDraft { + BOOL didCancelDraft; + SLAssertNoThrow(didCancelDraft = [_composeView cancelAndDeleteDraft:NO], + @"Should have been able to cancel and save draft."); + SLAssertTrue(didCancelDraft, + @"For the purposes of this test case, there should have been a draft in progress to cancel."); + + SLAssertTrueWithTimeout(!SLAskAppYesNo(composeViewControllerIsPresented), 2.0, + @"Compose view should have been dismissed."); + MFMailComposeResult composeFinishResult; + [SLAskApp(composeViewControllerFinishResult) getValue:&composeFinishResult]; + SLAssertTrue(composeFinishResult == MFMailComposeResultSaved, + @"The draft was not saved as expected."); +} + +/** + Note that this test case is about `SLMailComposeView` properly reporting that + `MFMailComposeViewController` won't let the user send an empty message, + not about Subliminal enforcing that requirement itself. + */ +- (void)testCannotSendEmptyMessage { + SLAssertFalse([_composeView sendMessage], + @"Should not be able to send an empty message."); +} + +- (void)testCanSendMessage { + SLAssertTrue([_composeView sendMessage], + @"Should have been able to send the message."); + + SLAssertTrueWithTimeout(!SLAskAppYesNo(composeViewControllerIsPresented), 2.0, + @"Compose view should have been dismissed."); + MFMailComposeResult composeFinishResult; + [SLAskApp(composeViewControllerFinishResult) getValue:&composeFinishResult]; + SLAssertTrue(composeFinishResult == MFMailComposeResultSent, + @"The message was not sent as expected."); +} + +@end diff --git a/Integration Tests/Tests/SLMailComposeViewTestViewController.m b/Integration Tests/Tests/SLMailComposeViewTestViewController.m new file mode 100644 index 0000000..247c3dd --- /dev/null +++ b/Integration Tests/Tests/SLMailComposeViewTestViewController.m @@ -0,0 +1,98 @@ +// +// SLMailComposeViewTestViewController.m +// Subliminal +// +// Created by Jeffrey Wear on 5/24/14. +// Copyright (c) 2014 Inkling. All rights reserved. +// + +#import "SLTestCaseViewController.h" + +#import +#import + +@interface SLMailComposeViewTestViewController : SLTestCaseViewController +@end + +@implementation SLMailComposeViewTestViewController { + MFMailComposeViewController *_composeViewController; + NSValue *_composeViewControllerFinishResultValue; +} + +- (void)loadViewForTestCase:(SEL)testCase { + // Since we're testing the mail compose view, + // we don't need any particular view. + [self loadGenericView]; +} + +- (instancetype)initWithTestCaseWithSelector:(SEL)testCase { + self = [super initWithTestCaseWithSelector:testCase]; + if (self) { + SLTestController *testController = [SLTestController sharedTestController]; + [testController registerTarget:self forAction:@selector(composeViewControllerIsPresented)]; + [testController registerTarget:self forAction:@selector(presentComposeViewControllerWithInfo:)]; + [testController registerTarget:self forAction:@selector(composeViewControllerFinishResult)]; + [testController registerTarget:self forAction:@selector(dismissComposeViewController)]; + } + return self; +} + +- (void)dealloc { + [[SLTestController sharedTestController] deregisterTarget:self]; +} + +#pragma mark - App hooks + +- (NSNumber *)composeViewControllerIsPresented { + return @(_composeViewController != nil); +} + +- (void)presentComposeViewControllerWithInfo:(NSDictionary *)info { + MFMailComposeViewController *composeViewController = [[MFMailComposeViewController alloc] init]; + composeViewController.mailComposeDelegate = self; + composeViewController.modalPresentationStyle = (kCFCoreFoundationVersionNumber > kCFCoreFoundationVersionNumber_iOS_6_1) ? UIModalPresentationFormSheet : UIModalPresentationFullScreen; + + if (info[@"toRecipients"]) [composeViewController setToRecipients:info[@"toRecipients"]]; + if (info[@"ccRecipients"]) [composeViewController setCcRecipients:info[@"ccRecipients"]]; + if (info[@"bccRecipients"]) [composeViewController setBccRecipients:info[@"bccRecipients"]]; + if (info[@"subject"]) [composeViewController setSubject:info[@"subject"]]; + if (info[@"body"]) [composeViewController setMessageBody:info[@"body"] isHTML:NO]; + + // Present the controller without animation just for parity with dismissal. + [self presentViewController:composeViewController animated:NO completion:^{ + // make sure that the modal view controller's fully presented + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]]; + _composeViewController = composeViewController; + }]; +} + +- (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error { + NSValue *resultValue = [NSValue valueWithBytes:&result objCType:@encode(__typeof(result))]; + [self dismissComposeViewControllerWithResultValue:resultValue]; +} + +- (NSValue *)composeViewControllerFinishResult { + return _composeViewControllerFinishResultValue; +} + +- (void)dismissComposeViewController { + [self dismissComposeViewControllerWithResultValue:nil]; +} + +- (void)dismissComposeViewControllerWithResultValue:(NSValue *)resultValue { + // check the second condition because this method may be called after the + // mail compose controller has finished but before it's fully dismissed + if (!_composeViewController || _composeViewControllerFinishResultValue) return; + _composeViewControllerFinishResultValue = resultValue; + + // Dismiss the controller without animation because sometimes it fails with animation + // --the controller just doesn't dismiss. + [self dismissViewControllerAnimated:NO completion:^{ + // make sure that the modal view controller's fully torn down before popping this controller + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]]; + _composeViewController = nil; + _composeViewControllerFinishResultValue = nil; + }]; +} + +@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/SLMailComposeView.h b/Sources/Classes/UIAutomation/User Interface Elements/SLMailComposeView.h new file mode 100644 index 0000000..6dda065 --- /dev/null +++ b/Sources/Classes/UIAutomation/User Interface Elements/SLMailComposeView.h @@ -0,0 +1,138 @@ +// +// SLMailComposeView.h +// Subliminal +// +// Created by Jeffrey Wear on 5/25/14. +// Copyright (c) 2014 Inkling. All rights reserved. +// + +#import "SLStaticElement.h" + +/** + `SLMailComposeView` allows you to manipulate mail compose views + within your application. + + "Mail compose views" are the views displayed by instances of + `MFMailComposeViewController`. + */ +@interface SLMailComposeView : SLStaticElement + +/** + Returns an object that represents the app's current mail compose view, if any. + + This element will be [valid](-[SLUIAElement isValid]) if and only if the application + is currently showing a mail compose view. + + @return An object that represents the app's current mail compose view. + */ ++ (instancetype)currentComposeView; + +#pragma mark - Reading and Setting Mail Fields +/// ------------------------------------------ +/// @name Reading and Setting Mail Fields +/// ------------------------------------------ + +/** + The recipients in the email's "To" field, as an array of `NSString` objects + representing the recipients' email addresses. + + @exception SLUIAElementInvalidException Raised if the compose view is not valid + by the end of the [default timeout](+defaultTimeout). + + @exception SLUIAElementNotTappableException Raised by the setter if the compose + view is not tappable when whatever amount of time remains of the default timeout + after the compose view becomes valid elapses. + */ +@property (nonatomic, copy) NSArray *toRecipients; + +/** + The recipients in the email's "Cc" field, as an array of `NSString` objects + representing the recipients' email addresses. + + @exception SLUIAElementInvalidException Raised if the compose view is not valid + by the end of the [default timeout](+defaultTimeout). + + @exception SLUIAElementNotTappableException Raised by the setter if the compose + view is not tappable when whatever amount of time remains of the default timeout + after the compose view becomes valid elapses. + */ +@property (nonatomic, copy) NSArray *ccRecipients; + +/** + The recipients in the email's "Bcc" field, as an array of `NSString` objects + representing the recipients' email addresses. + + @exception SLUIAElementInvalidException Raised if the compose view is not valid + by the end of the [default timeout](+defaultTimeout). + + @exception SLUIAElementNotTappableException Raised by the setter if the compose + view is not tappable when whatever amount of time remains of the default timeout + after the compose view becomes valid elapses. + */ +@property (nonatomic, copy) NSArray *bccRecipients; + +/** + The subject line of the email. + + @exception SLUIAElementInvalidException Raised if the compose view is not valid + by the end of the [default timeout](+defaultTimeout). + + @exception SLUIAElementNotTappableException Raised by the setter if the compose + view is not tappable when whatever amount of time remains of the default timeout + after the compose view becomes valid elapses. + */ +@property (nonatomic, copy) NSString *subject; + +/** + The body of the email. + + @exception SLUIAElementInvalidException Raised if the compose view is not valid + by the end of the [default timeout](+defaultTimeout). + + @exception SLUIAElementNotTappableException Raised by the setter if the compose + view is not tappable when whatever amount of time remains of the default timeout + after the compose view becomes valid elapses. + */ +@property (nonatomic, copy) NSString *body; + +#pragma mark - Sending Mail +/// ------------------------------------------ +/// @name Sending Mail +/// ------------------------------------------ + +/** + Cancels the message. + + An empty message will be discarded immediately, but if a draft is in progress, + the user will be presented with the option to save or delete the draft. + _deleteDraft_ determines the choice to take if/when the option is presented. + + @param deleteDraft `YES` to delete the draft, `NO` otherwise. + This parameter has no effect if a draft is not in progress. + @return `YES` if there was a draft in progress, `NO` otherwise. + + @exception SLUIAElementInvalidException Raised if the compose view is not valid + by the end of the [default timeout](+defaultTimeout). + + @exception SLUIAElementNotTappableException Raised by the setter if the compose + view is not tappable when whatever amount of time remains of the default timeout + after the compose view becomes valid elapses. + */ +- (BOOL)cancelAndDeleteDraft:(BOOL)deleteDraft; + +/** + Sends the message. + + @return `YES` if the message was sent, `NO` otherwise + (for instance, if the message is empty). + + @exception SLUIAElementInvalidException Raised if the compose view is not valid + by the end of the [default timeout](+defaultTimeout). + + @exception SLUIAElementNotTappableException Raised by the setter if the compose + view is not tappable when whatever amount of time remains of the default timeout + after the compose view becomes valid elapses. + */ +- (BOOL)sendMessage; + +@end diff --git a/Sources/Classes/UIAutomation/User Interface Elements/SLMailComposeView.m b/Sources/Classes/UIAutomation/User Interface Elements/SLMailComposeView.m new file mode 100644 index 0000000..ccc5e84 --- /dev/null +++ b/Sources/Classes/UIAutomation/User Interface Elements/SLMailComposeView.m @@ -0,0 +1,349 @@ +// +// SLMailComposeView.m +// Subliminal +// +// Created by Jeffrey Wear on 5/25/14. +// Copyright (c) 2014 Inkling. All rights reserved. +// + +#import "SLMailComposeView.h" +#import "SLUIAElement+Subclassing.h" + +#import "SLTerminal+ConvenienceFunctions.h" +#import "SLGeometry.h" +#import "SLKeyboard.h" +#import "SLNavigationBar.h" +#import "SLActionSheet.h" + +typedef NS_ENUM(NSUInteger, SLMailComposeViewChildType) { + SLMailComposeViewChildTypeToField, + SLMailComposeViewChildTypeCcBccLabel, + SLMailComposeViewChildTypeCcField, + SLMailComposeViewChildTypeBccField, + SLMailComposeViewChildTypeSubjectField, + SLMailComposeViewChildTypeBodyView +}; + +@implementation SLMailComposeView { + SLStaticElement *_toField; + SLStaticElement *_ccBccLabel, *_ccField, *_bccField; + SLStaticElement *_subjectField; + SLStaticElement *_bodyView; +} + ++ (NSString *)UIAChildSelectorForChildOfType:(SLMailComposeViewChildType)type { + switch (type) { + case SLMailComposeViewChildTypeToField: + return @"textFields()['toField']"; + case SLMailComposeViewChildTypeCcBccLabel: + // on iOS 6, this is a plain element; on 7, it's a static text element + return @"elements()['Cc/Bcc:']"; + case SLMailComposeViewChildTypeCcField: + return @"textFields()['ccField']"; + case SLMailComposeViewChildTypeBccField: + return @"textFields()['bccField']"; + case SLMailComposeViewChildTypeSubjectField: + return @"textFields()['subjectField']"; + case SLMailComposeViewChildTypeBodyView: + // on iOS 6, this is a text field; on 7, it's a text view + return @"elements()['Message body']"; + } +} + ++ (instancetype)currentComposeView { + /** + UIAutomation does not provide an interface to the compose view + (e.g. on `UIAApplication`, like other system views). + We identify a view as the compose view if it has the children of a compose view. + + We define the compose view getter as a standalone function + (rather than an immediately-evaluated function expression) so that + the `-description` of the compose view and its children will be more concise. + */ + static NSString *const kCurrentComposeViewFunctionName = @"SLMailComposeViewCurrentComposeView"; + [[SLTerminal sharedTerminal] loadFunctionWithName:kCurrentComposeViewFunctionName + params:nil + body:[NSString stringWithFormat:@"\ + var candidateView = UIATarget.localTarget().frontMostApp().mainWindow().scrollViews()[0];\ + if (candidateView.isValid() &&\ + candidateView.%@.isValid() &&\ + candidateView.%@.isValid() &&\ + candidateView.%@.isValid() &&\ + candidateView.%@.isValid() &&\ + candidateView.%@.isValid() &&\ + candidateView.%@.isValid()) {\ + return candidateView;\ + } 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'];\ + }", + [self UIAChildSelectorForChildOfType:SLMailComposeViewChildTypeToField], + [self UIAChildSelectorForChildOfType:SLMailComposeViewChildTypeCcBccLabel], + [self UIAChildSelectorForChildOfType:SLMailComposeViewChildTypeCcField], + [self UIAChildSelectorForChildOfType:SLMailComposeViewChildTypeBccField], + [self UIAChildSelectorForChildOfType:SLMailComposeViewChildTypeSubjectField], + [self UIAChildSelectorForChildOfType:SLMailComposeViewChildTypeBodyView], + NSStringFromClass(self), self]]; + + NSString *namespacedCurrentComposeViewFunctionName = [NSString stringWithFormat:@"%@.%@", + [[SLTerminal sharedTerminal] scriptNamespace], kCurrentComposeViewFunctionName]; + return [[self alloc] initWithUIARepresentation:[NSString stringWithFormat:@"%@()", namespacedCurrentComposeViewFunctionName]]; +} + +- (instancetype)initWithUIARepresentation:(NSString *)UIARepresentation { + self = [super initWithUIARepresentation:UIARepresentation]; + if (self) { + NSString *(^UIARepresentationForChildOfType)(SLMailComposeViewChildType) = ^(SLMailComposeViewChildType type) { + return [UIARepresentation stringByAppendingFormat:@".%@", [[self class] UIAChildSelectorForChildOfType:type]]; + }; + + _toField = [[SLStaticElement alloc] initWithUIARepresentation:UIARepresentationForChildOfType(SLMailComposeViewChildTypeToField)]; + _ccBccLabel = [[SLStaticElement alloc] initWithUIARepresentation:UIARepresentationForChildOfType(SLMailComposeViewChildTypeCcBccLabel)]; + _ccField = [[SLStaticElement alloc] initWithUIARepresentation:UIARepresentationForChildOfType(SLMailComposeViewChildTypeCcField)]; + _bccField = [[SLStaticElement alloc] initWithUIARepresentation:UIARepresentationForChildOfType(SLMailComposeViewChildTypeBccField)]; + _subjectField = [[SLStaticElement alloc] initWithUIARepresentation:UIARepresentationForChildOfType(SLMailComposeViewChildTypeSubjectField)]; + _bodyView = [[SLStaticElement alloc] initWithUIARepresentation:UIARepresentationForChildOfType(SLMailComposeViewChildTypeBodyView)]; + } + return self; +} + +#pragma mark - Reading and Setting Mail Fields + +/** + A general note on error handling in the mail field setters and getters: + these methods, like all of `SLMailComposeView`'s interface (save `+currentComposeView`), + require that the compose view be valid. + + However, it is not necessary to check `-[self isValid]` in these methods because + the mail fields are derived from the compose view--that is, their UIAutomation + representations are derived from that of the compose view. This causes them to + be valid if and only if the compose view is valid, and attempting to manipulate + the fields will cause suitable exceptions to be thrown if the compose view is not valid. + */ + +/** + When the recipient fields contain multiple recipients and don't have keyboard + focus, they collapse and truncate the display of the secondary recipients + (e.g. read "foo@example.com & 1 more..."). + + To read all recipients, a field must first be given the keyboard focus (so that + it will expand). But even when expanded, the `value()` of a field with multiple + recipients will still be truncated. So, the recipients must be read as the `name()`s + of the "recipient buttons" within the field. + + Unfortunately, on iOS 6, the buttons' `rect()`s are not actually within + the `rect()` of the field--the field comes _after_ the buttons. The buttons must + be read as those in the compose view's elements array just prior to the field. + */ +- (NSArray *)recipientsInFieldWithName:(NSString *)fieldName { + NSString *quotedFieldName = [NSString stringWithFormat:@"'%@'", [fieldName slStringByEscapingForJavaScriptLiteral]]; + NSString *recipientsJSONString = [[SLTerminal sharedTerminal] evalFunctionWithName:@"SLMailComposeViewNamesOfRecipientButtonsInFieldWithName" + params:@[ @"fieldName" ] + body:[NSString stringWithFormat:@"\ + var composeView = %@;" + // compare names rather than elements because elements aren't unique objects + @"var getName = function(element){ return element.name() };\ + var mailElementNames = composeView.elements().toArray().map(getName);\ + var mailFieldNames = composeView.textFields().toArray().map(getName);\ + var mailButtonNames = composeView.buttons().toArray().map(getName);" + + @"var fieldIndexAsElement = mailElementNames.indexOf(fieldName);\ + if (fieldIndexAsElement === -1) throw ('Field \"' + fieldName + '\" not found');" + + @"var previousFieldIndexAsElement = -1;\ + var fieldIndexAsField = mailFieldNames.indexOf(fieldName);\ + var previousFieldIndexAsField = fieldIndexAsField - 1;\ + if (previousFieldIndexAsField > -1) {\ + var previousFieldName = mailFieldNames[previousFieldIndexAsField];\ + previousFieldIndexAsElement = mailElementNames.indexOf(previousFieldName);\ + if (previousFieldIndexAsElement === -1) throw ('Field \"' + previousFieldName + '\" not found');\ + }" + + // return the name of every button element (except for "Add Contact") + // between the previous field and this field + @"var names = mailElementNames.slice(Math.max(0, previousFieldIndexAsElement), fieldIndexAsElement).filter(function(name){\ + return ((mailButtonNames.indexOf(name) !== -1) &&\ + (name !== 'Add Contact'));\ + }).sort(function(nameA, nameB){" + // filter duplicates--on iOS 7, there are text views that contain the email text + // and we may have picked those up "as" buttons + // sort first so we can use a faster filtering algorithm + @"return nameA - nameB;\ + }).reduce(function(arr, name){\ + if ((arr.length === 0) || (arr[arr.length-1] !== name)) arr.push(name);\ + return arr;\ + }, []);\ + return JSON.stringify(names);\ + ", _UIARepresentation] + withArgs:@[ quotedFieldName ]]; + NSData *recipientsJSONData = [recipientsJSONString dataUsingEncoding:NSUTF8StringEncoding]; + if (!recipientsJSONData) return nil; + + return [NSJSONSerialization JSONObjectWithData:recipientsJSONData options:0 error:NULL]; +} + +- (void)setContentsOfField:(SLUIAElement *)field toRecipients:(NSArray *)recipients { + // Bring up the keyboard + if (![field hasKeyboardFocus]) [field tap]; + + // Clear the contents of the field + [field waitUntilTappable:YES thenSendMessage:@"setValue('')"]; + + // Add the recipients + for (NSString *recipient in recipients) { + [[SLKeyboard keyboard] typeString:recipient]; + // Hitting enter confirms the recipient, turning it into a button (see `-recipientsInFieldWithName:`) + // The need to confirm is also why we can't use `setValue()`. + [[SLKeyboard keyboard] typeString:@"\n"]; + } +} + +/// See comment on `recipientsInFieldWithName:` for explanation. +- (NSArray *)toRecipients { + if (![_toField hasKeyboardFocus]) [_toField tap]; + + return [self recipientsInFieldWithName:@"toField"]; +} + +- (void)setToRecipients:(NSArray *)toRecipients { + [self setContentsOfField:_toField toRecipients:toRecipients]; +} + +/// See comment on `recipientsInFieldWithName:` for explanation. +- (NSArray *)ccRecipients { + /* + The "Cc/Bcc:" label is visible iff the "Cc:" and "Bcc:" fields are empty + (the label is shown next to a unified (collapsed) field). + This is the fastest way to determine if the "Cc:" field is empty, + plus the field isn't always tappable when collapsed (i.e. on iOS 6). + */ + if ([_ccBccLabel isValidAndVisible]) return @[]; + + if (![_ccField hasKeyboardFocus]) [_ccField tap]; + + return [self recipientsInFieldWithName:@"ccField"]; +} + +- (void)setCcRecipients:(NSArray *)ccRecipients { + /* + If the "cc" and "bcc" fields are collapsed, we need to show the "cc" field + to make it tappable for `-setContentsOfField:toRecipients:`. + Tapping the "cc/bcc" label expands the "cc" and "bcc" fields. + (The "cc" field is not tappable on iOS 6 if collapsed, fyi.) + */ + if ([_ccBccLabel isValidAndVisible]) [_ccBccLabel tap]; + + [self setContentsOfField:_ccField toRecipients:ccRecipients]; +} + +/// See comment on `recipientsInFieldWithName:` for explanation. +- (NSArray *)bccRecipients { + /* + The "Cc/Bcc:" label is visible iff the "Cc:" and "Bcc:" fields are empty + (the label is shown next to a unified (collapsed) field). + This is the fastest way to determine if the "Bcc:" field is empty, + plus the field isn't always tappable when collapsed (i.e. on iOS 6). + */ + if ([_ccBccLabel isValidAndVisible]) return @[]; + + if (![_bccField hasKeyboardFocus]) [_bccField tap]; + + return [self recipientsInFieldWithName:@"bccField"]; +} + +- (void)setBccRecipients:(NSArray *)bccRecipients { + /* + If the "cc" and "bcc" fields are collapsed, we need to show the "cc" field + to make it tappable for `-setContentsOfField:toRecipients:`. + Tapping the "cc/bcc" label expands the "cc" and "bcc" fields. + (The "bcc" field is not tappable on iOS 6 if collapsed, fyi.) + */ + if ([_ccBccLabel isValidAndVisible]) [_ccBccLabel tap]; + + [self setContentsOfField:_bccField toRecipients:bccRecipients]; +} + +- (NSString *)subject { + return [_subjectField value]; +} + +- (void)setSubject:(NSString *)subject { + /* + We don't need to use the keyboard to type the value here (as normal), + but can rather use the faster and more robust `setValue()`, + because the subject field is a private subview of the mail compose view, + so the application can't observe the text changing. It's also ok to violate + our "test like a user" mantra because we're not responsible for testing system views. + */ + [_subjectField waitUntilTappable:YES thenSendMessage:@"setValue('%@')", [subject slStringByEscapingForJavaScriptLiteral]]; +} + +- (NSString *)body { + return [_bodyView value]; +} + +- (void)setBody:(NSString *)body { + /* + We don't need to use the keyboard to type the value here (as normal), + but can rather use the faster and more robust `setValue()`, + because the body view is a private subview of the mail compose view, + so the application can't observe the text changing. It's also ok to violate + our "test like a user" mantra because we're not responsible for testing system views. + */ + [_bodyView waitUntilTappable:YES thenSendMessage:@"setValue('%@')", [body slStringByEscapingForJavaScriptLiteral]]; +} + +#pragma mark - Sending Mail + +- (BOOL)sendMessage { + __block BOOL didSendMessage = NO; + + // Use this as convenience to perform the waiting and, if necessary, exception throwing + // the sheet itself doesn't technically need to be tappable, + // but as the user is "acting upon the sheet", we pass `YES` for _waitUntilTappable_ + [self waitUntilTappable:YES thenPerformActionWithUIARepresentation:^(NSString *UIARepresentation) { + SLUIAElement *sendButton = [[SLNavigationBar currentNavigationBar] rightButton]; + if (![sendButton isEnabled]) return; + + [sendButton tap]; + didSendMessage = YES; + } timeout:[[self class] defaultTimeout]]; + + return didSendMessage; +} + +- (BOOL)cancelAndDeleteDraft:(BOOL)deleteDraft { + __block BOOL draftWasInProgress = NO; + + // Use this as convenience to perform the waiting and, if necessary, exception throwing + // the sheet itself doesn't technically need to be tappable, + // but as the user is "acting upon the sheet", we pass `YES` for _waitUntilTappable_ + [self waitUntilTappable:YES thenPerformActionWithUIARepresentation:^(NSString *UIARepresentation) { + SLUIAElement *cancelButton = [[SLNavigationBar currentNavigationBar] leftButton]; + [cancelButton tap]; + + // Wait for the action sheet to show up if it will (i.e. if there was a draft in progress) + [NSThread sleepForTimeInterval:0.3]; + + SLActionSheet *draftActionSheet = [SLActionSheet currentActionSheet]; + if ([draftActionSheet isValidAndVisible]) { + draftWasInProgress = YES; + + NSArray *draftActionButtons = [draftActionSheet buttons]; + + NSString *buttonTitle = deleteDraft ? @"Delete Draft" : @"Save Draft"; + NSUInteger buttonIndex = [draftActionButtons indexOfObjectPassingTest:^BOOL(SLUIAElement *button, NSUInteger idx, BOOL *stop) { + return [[button label] isEqualToString:buttonTitle]; + }]; + NSAssert(buttonIndex != NSNotFound, + @"Option to %@ draft not found.", deleteDraft ? @"delete" : @"cancel"); + [draftActionButtons[buttonIndex] tap]; + } + } timeout:[[self class] defaultTimeout]]; + + return draftWasInProgress; +} + +@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 c4b3f29..469845d 100644 --- a/Sources/Subliminal.h +++ b/Sources/Subliminal.h @@ -29,10 +29,13 @@ #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 "SLMailComposeView.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 8548951..9ca3ceb 100644 --- a/Subliminal.xcodeproj/project.pbxproj +++ b/Subliminal.xcodeproj/project.pbxproj @@ -108,6 +108,19 @@ 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 */; }; + F052B09819314998004606C0 /* SLMailComposeViewTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F052B09619314998004606C0 /* SLMailComposeViewTest.m */; }; + F052B09919314998004606C0 /* SLMailComposeViewTestViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F052B09719314998004606C0 /* SLMailComposeViewTestViewController.m */; }; + F052B09E19314B0C004606C0 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F052B09D19314B0C004606C0 /* MessageUI.framework */; }; + F052B0A11931D62C004606C0 /* SLMailComposeView.h in Headers */ = {isa = PBXBuildFile; fileRef = F052B09F1931D62C004606C0 /* SLMailComposeView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F052B0A21931D62C004606C0 /* SLMailComposeView.m in Sources */ = {isa = PBXBuildFile; fileRef = F052B0A01931D62C004606C0 /* SLMailComposeView.m */; }; + 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 = (); }; }; @@ -357,6 +370,19 @@ 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 = ""; }; + F052B09619314998004606C0 /* SLMailComposeViewTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLMailComposeViewTest.m; sourceTree = ""; }; + F052B09719314998004606C0 /* SLMailComposeViewTestViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLMailComposeViewTestViewController.m; sourceTree = ""; }; + F052B09D19314B0C004606C0 /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; }; + F052B09F1931D62C004606C0 /* SLMailComposeView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLMailComposeView.h; sourceTree = ""; }; + F052B0A01931D62C004606C0 /* SLMailComposeView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLMailComposeView.m; 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 = ""; }; @@ -496,6 +522,7 @@ F0AC809516BB299500C5D5C0 /* UIKit.framework in Frameworks */, F0AC809616BB299500C5D5C0 /* Foundation.framework in Frameworks */, F0AC809816BB299500C5D5C0 /* CoreGraphics.framework in Frameworks */, + F052B09E19314B0C004606C0 /* MessageUI.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -702,6 +729,15 @@ path = Terminal; sourceTree = ""; }; + F04F595C193149300074EAE2 /* SLMailComposeView Tests */ = { + isa = PBXGroup; + children = ( + F052B09619314998004606C0 /* SLMailComposeViewTest.m */, + F052B09719314998004606C0 /* SLMailComposeViewTestViewController.m */, + ); + name = "SLMailComposeView Tests"; + sourceTree = ""; + }; F05263C316D2C2CF0090174F /* SLElement Matching Tests */ = { isa = PBXGroup; children = ( @@ -716,6 +752,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 = ( @@ -744,6 +798,7 @@ F0695D8D16011515000B05D0 /* Frameworks */ = { isa = PBXGroup; children = ( + F052B09D19314B0C004606C0 /* MessageUI.framework */, F0695D8E16011515000B05D0 /* Foundation.framework */, F0695E0C16013D77000B05D0 /* UIKit.framework */, F0D240531683F7130031B67C /* SenTestingKit.framework */, @@ -847,6 +902,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 */, @@ -855,6 +912,10 @@ 622DA0B9194E2CB100EFFE05 /* SLDatePicker.m */, F0C07A511704011300C93F93 /* SLKeyboard.h */, F0C07A521704011300C93F93 /* SLKeyboard.m */, + F052B09F1931D62C004606C0 /* SLMailComposeView.h */, + F052B0A01931D62C004606C0 /* SLMailComposeView.m */, + F052B0A319343A92004606C0 /* SLNavigationBar.h */, + F052B0A419343A92004606C0 /* SLNavigationBar.m */, 622DA089194AF03E00EFFE05 /* SLPickerView.h */, 622DA08A194AF03E00EFFE05 /* SLPickerView.m */, F00800CC174C1C64001927AC /* SLPopover.h */, @@ -971,8 +1032,7 @@ F0AC80BE16BB542400C5D5C0 /* Tests */ = { isa = PBXGroup; children = ( - DB2627D817B96704009DA3A6 /* SLStatusBar Tests */, - 50A59BD317848CEE002A863A /* SLGeometry Tests */, + F052B0B019345F14004606C0 /* SLActionSheet Tests */, F07DA31F16E439B7004C2282 /* SLAlert Tests */, F00800BF174B2CB0001927AC /* SLButton Tests */, 622DA0AD194CCA7500EFFE05 /* SLDatePicker Tests */, @@ -981,11 +1041,15 @@ F077D70016D9D71E00908FF5 /* SLElement Visibility Tests */, F05263C316D2C2CF0090174F /* SLElement Matching Tests */, 0696BA5E16E013DF00DD70CF /* SLElement Gestures and Actions Tests */, + 50A59BD317848CEE002A863A /* SLGeometry Tests */, F089F9DC1745FB3800DF1F25 /* SLKeyboard Tests */, + F04F595C193149300074EAE2 /* SLMailComposeView 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 */, @@ -1096,6 +1160,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 */, @@ -1105,9 +1170,11 @@ 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 */, + F052B0A11931D62C004606C0 /* SLMailComposeView.h in Headers */, F00800CE174C1C64001927AC /* SLPopover.h in Headers */, 50F3E18C1783A5CB00C6BD1B /* SLGeometry.h in Headers */, F043469F175ACE3A00D91F7F /* NSObject+SLAccessibilityDescription.h in Headers */, @@ -1373,6 +1440,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F052B0AF193451FC004606C0 /* SLActionSheet.m in Sources */, 2CE9AA4D17E3A747007EF0B5 /* SLSwitch.m in Sources */, 62E7A634193EF84C00CB11AB /* SLStaticText.m in Sources */, 50F3E18E1783A60100C6BD1B /* SLGeometry.m in Sources */, @@ -1390,6 +1458,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 */, @@ -1402,6 +1471,7 @@ F04346A0175ACE3A00D91F7F /* NSObject+SLAccessibilityDescription.m in Sources */, F04346B0175AD63E00D91F7F /* SLAccessibilityPath.m in Sources */, F04346A8175AD10200D91F7F /* NSObject+SLVisibility.m in Sources */, + F052B0A21931D62C004606C0 /* SLMailComposeView.m in Sources */, F0A3F63517A715AE007529C3 /* SLTextView.m in Sources */, DB501DCA17B9669A001658CB /* SLStatusBar.m in Sources */, ); @@ -1423,6 +1493,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 */, @@ -1430,7 +1501,9 @@ 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 */, + F052B09919314998004606C0 /* SLMailComposeViewTestViewController.m in Sources */, F07DA32D16E43AD3004C2282 /* SLAlertTest.m in Sources */, F07DA32E16E43AD3004C2282 /* SLAlertTestViewController.m in Sources */, 2C903BC117F525E700555317 /* SLSwitchTestViewController.m in Sources */, @@ -1453,11 +1526,14 @@ F089F9E21745FB7E00DF1F25 /* SLKeyboardTest.m in Sources */, F089F9E31745FB7E00DF1F25 /* SLKeyboardTestViewController.m in Sources */, F05D2B061746B55C0089DB9E /* SLStaticElementTest.m in Sources */, + F052B0AB19343DD8004606C0 /* SLNavigationBarTestViewController.m in Sources */, + F052B09819314998004606C0 /* SLMailComposeViewTest.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 */,