diff --git a/Integration Tests/Tests/SLEditingMenuTest.m b/Integration Tests/Tests/SLEditingMenuTest.m new file mode 100644 index 0000000..488b514 --- /dev/null +++ b/Integration Tests/Tests/SLEditingMenuTest.m @@ -0,0 +1,111 @@ +// +// SLEditingMenuTest.m +// Subliminal +// +// For details and documentation: +// http://github.com/inkling/Subliminal +// +// Copyright 2013-2014 Inkling Systems, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "SLIntegrationTest.h" + +@interface SLEditingMenuTest : SLIntegrationTest + +@end + +@implementation SLEditingMenuTest { + SLEditingMenu *_menu; + SLElement *_testText, *_otherText; + NSString *_menuItemTitle; +} + ++ (NSString *)testCaseViewControllerClassName { + return @"SLEditingMenuTestViewController"; +} + +//+ (BOOL)supportsCurrentPlatform { +// // Don't run these tests on Travis, because the editing menu +// // intermittently fails to appear on Travis, while it consistently +// // appears in other environments. +// return [super supportsCurrentPlatform] && !getenv("TRAVIS"); +//} + +- (void)setUpTest { + [super setUpTest]; + + _menu = [SLEditingMenu menu]; + + _testText = [SLElement elementMatching:^BOOL(NSObject *obj) { + return [obj.accessibilityLabel hasPrefix:@"A corner of the pub"]; + } withDescription:@"text element"]; + _otherText = [SLElement elementWithAccessibilityLabel:@"Inklings"]; + + _menuItemTitle = @"Copy"; +} + +- (void)setUpTestCaseWithSelector:(SEL)testCaseSelector { + [super setUpTestCaseWithSelector:testCaseSelector]; + + SLAssertTrueWithTimeout(SLAskAppYesNo(webViewDidFinishLoad), 5.0, @"Webview did not load test HTML."); + + if (testCaseSelector == @selector(testTapCustomMenuItem)) { + // It's important that this item have the same title as a standard item + // (see the implementation of `-[SLEditingMenuItem itemWithAccessibilityLabel:]`) + SLAskApp1(installCustomMenuItemWithTitle:, _menuItemTitle); + } +} + +- (void)tearDownTestCaseWithSelector:(SEL)testCaseSelector { + // we never need to hide the editing menu because view controllers are not reused between tests + // the view controller will automatically restore the standard menu items, too + + [super tearDownTestCaseWithSelector:testCaseSelector]; +} + +- (void)showEditingMenu { + [UIAElement(_testText) touchAndHoldWithDuration:1.0]; + + // wait for editing menu to appear before proceeding + static const NSTimeInterval kMenuAnimationDuration = 0.5; + (void)SLWaitUntilTrue([UIAElement(_menu) isValidAndVisible], kMenuAnimationDuration); +} + +- (void)testCanMatchEditingMenu { + SLAssertFalse([UIAElement(_menu) isValidAndVisible], @"The editing menu should not be visible."); + + [self showEditingMenu]; + + SLAssertTrue([UIAElement(_menu) isVisible], @"The editing menu should be visible."); +} + +- (void)testTapMenuItem { + [self showEditingMenu]; + + SLEditingMenuItem *menuItem = [SLEditingMenuItem itemWithAccessibilityLabel:_menuItemTitle]; + SLAssertTrue([UIAElement(menuItem) isVisible], nil); + SLAssertNoThrow([UIAElement(menuItem) tap], nil); + + // give a little bit of time for the responder to perform the menu item action to be received + SLAssertTrueWithTimeout(SLAskAppYesNo(menuItemWasTapped), 0.2, @"The menu item should have been tapped."); +} + +// Something of an internal test--see the implementation of `-[SLEditingMenuItem itemWithAccessibilityLabel:]`. +// It's important that the custom item have the same label as a standard item. +- (void)testTapCustomMenuItem { + SLAssertNoThrow([self testTapMenuItem], @"Tapping the custom menu item did not work as expected."); +} + +@end diff --git a/Integration Tests/Tests/SLEditingMenuTestViewController.m b/Integration Tests/Tests/SLEditingMenuTestViewController.m new file mode 100644 index 0000000..cafc4b2 --- /dev/null +++ b/Integration Tests/Tests/SLEditingMenuTestViewController.m @@ -0,0 +1,129 @@ +// +// SLEditingMenuTestViewController.m +// Subliminal +// +// Created by Jeffrey Wear on 10/11/13. +// Copyright (c) 2013 Inkling. All rights reserved. +// + +#import "SLTestCaseViewController.h" + +#import + + +@interface SLEditingMenuTestWebView : UIWebView +@property (nonatomic, readonly) BOOL copyItemWasTapped; +@property (nonatomic) BOOL standardMenuItemsDisabled; +@end + +@implementation SLEditingMenuTestWebView + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { + if (self.standardMenuItemsDisabled) return NO; + + return [super canPerformAction:action withSender:sender]; +} + +- (void)copy:(id)sender { + _copyItemWasTapped = YES; + [super copy:sender]; +} + +@end + + +@interface SLEditingMenuTestViewController : SLTestCaseViewController +@property (nonatomic) BOOL standardMenuItemsDisabled; +@end + +@implementation SLEditingMenuTestViewController { + SLEditingMenuTestWebView *_webView; + BOOL _webViewDidFinishLoad; + + NSArray *_preexistingCustomMenuItems; + UIMenuItem *_customMenuItem; + BOOL _customMenuItemWasTapped; +} + +- (void)loadViewForTestCase:(SEL)testCase { + _webView = [[SLEditingMenuTestWebView alloc] initWithFrame:CGRectZero]; + _webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.view = _webView; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + NSURL *webArchiveURL = [[NSBundle mainBundle] URLForResource:@"Inklings~iPhone" withExtension:@"webarchive"]; + NSURLRequest *webArchiveRequest = [NSURLRequest requestWithURL:webArchiveURL]; + _webView.delegate = self; + [_webView loadRequest:webArchiveRequest]; + + _webView.accessibilityLabel = @"foo"; +} + +- (instancetype)initWithTestCaseWithSelector:(SEL)testCase { + self = [super initWithTestCaseWithSelector:testCase]; + if (self) { + _preexistingCustomMenuItems = [[[UIMenuController sharedMenuController] menuItems] copy]; + + SLTestController *testController = [SLTestController sharedTestController]; + [testController registerTarget:self forAction:@selector(webViewDidFinishLoad)]; + [testController registerTarget:self forAction:@selector(menuItemWasTapped)]; + [testController registerTarget:self forAction:@selector(installCustomMenuItemWithTitle:)]; + } + return self; +} + +- (void)dealloc { + [[SLTestController sharedTestController] deregisterTarget:self]; + + [[UIMenuController sharedMenuController] setMenuItems:_preexistingCustomMenuItems]; +} + +#pragma mark - Editing menu + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { + if (self.standardMenuItemsDisabled) { + return action == _customMenuItem.action; + } else { + return [super canPerformAction:action withSender:sender]; + } +} + +- (void)slCopy:(id)sender { + [_webView copy:sender]; +} + +- (void)setStandardMenuItemsDisabled:(BOOL)standardMenuItemsDisabled { + _standardMenuItemsDisabled = standardMenuItemsDisabled; + _webView.standardMenuItemsDisabled = standardMenuItemsDisabled; +} + +- (void)installCustomMenuItemWithTitle:(NSString *)title { + self.standardMenuItemsDisabled = YES; + + UIMenuController *menuController = [UIMenuController sharedMenuController]; + // the title is for the test, to recognize and tap the item; + // the action is for the view controller, to track whether the item was tapped + _customMenuItem = [[UIMenuItem alloc] initWithTitle:title action:@selector(slCopy:)]; + menuController.menuItems = @[ _customMenuItem ]; +} + +#pragma mark - UIWebView delegate + +- (void)webViewDidFinishLoad:(UIWebView *)webView { + _webViewDidFinishLoad = YES; +} + +#pragma mark - App hooks + +- (NSNumber *)webViewDidFinishLoad { + return @(_webViewDidFinishLoad); +} + +- (NSNumber *)menuItemWasTapped { + return @(_webView.copyItemWasTapped); +} + +@end diff --git a/Sources/Classes/UIAutomation/User Interface Elements/SLEditingMenu.h b/Sources/Classes/UIAutomation/User Interface Elements/SLEditingMenu.h new file mode 100644 index 0000000..74f1628 --- /dev/null +++ b/Sources/Classes/UIAutomation/User Interface Elements/SLEditingMenu.h @@ -0,0 +1,56 @@ +// +// SLEditingMenu.h +// Subliminal +// +// For details and documentation: +// http://github.com/inkling/Subliminal +// +// Copyright 2013-2014 Inkling Systems, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "SLStaticElement.h" + +/** + The singleton `SLEditingMenu` instance allows you to manipulate your application's editing menu + --the menu that shows commands like Cut, Copy, and Paste when the user selects text. + */ +@interface SLEditingMenu : SLStaticElement + +/** + Returns an element representing the application's editing menu. + + @return An element representing the application's editing menu. + */ ++ (instancetype)menu; + +@end + + +/** + Instances of `SLEditingMenuItem` refer to items shown by the application's editing menu. + */ +@interface SLEditingMenuItem : SLStaticElement + +/** + Creates and returns an element which represents the menu item with the specified label. + + This is the designated initializer for a menu item. + + @param label The item's accessibility label. + @return A newly created element representing the menu item with the specified label. + */ ++ (instancetype)itemWithAccessibilityLabel:(NSString *)label; + +@end diff --git a/Sources/Classes/UIAutomation/User Interface Elements/SLEditingMenu.m b/Sources/Classes/UIAutomation/User Interface Elements/SLEditingMenu.m new file mode 100644 index 0000000..4161ad2 --- /dev/null +++ b/Sources/Classes/UIAutomation/User Interface Elements/SLEditingMenu.m @@ -0,0 +1,74 @@ +// +// SLEditingMenu.m +// Subliminal +// +// For details and documentation: +// http://github.com/inkling/Subliminal +// +// Copyright 2013-2014 Inkling Systems, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "SLEditingMenu.h" +#import "SLUIAElement+Subclassing.h" + +@implementation SLEditingMenu + ++ (instancetype)menu { + return [[self alloc] initWithUIARepresentation:@"UIATarget.localTarget().frontMostApp().editingMenu()"]; +} + +@end + + +@implementation SLEditingMenuItem { + NSString *_label; +} + ++ (instancetype)itemWithAccessibilityLabel:(NSString *)label { + return [[self alloc] initWithAccessibilityLabel:label]; +} + +- (instancetype)initWithAccessibilityLabel:(NSString *)label { + NSParameterAssert([label length]); + + NSString *escapedLabel = [label slStringByEscapingForJavaScriptLiteral]; + NSString *UIARepresentation = [NSString stringWithFormat:@"((function(){\ + var items = UIATarget.localTarget().frontMostApp().editingMenu().elements();" + // the editing menu may contain multiple items with the same name (one system, one custom), + // only one of which is actually visible in the menu (has a non-zero size), + // so we must search through the array rather than retrieving the item by name + @"var item = null; \ + if (items.toArray().some(function(elem) {\ + item = elem;\ + return ((elem.name() === '%@') && (elem.rect().size.width > 0));\ + })) {\ + return item;\ + } else {" + // return whatever element the menu would have returned + @"return items['%@'];\ + }\ + })())", escapedLabel, escapedLabel]; + self = [super initWithUIARepresentation:UIARepresentation]; + if (self) { + _label = [label copy]; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@ label:\"%@\">", NSStringFromClass([self class]), _label]; +} + +@end diff --git a/Sources/Subliminal.h b/Sources/Subliminal.h index b56a432..12e3488 100644 --- a/Sources/Subliminal.h +++ b/Sources/Subliminal.h @@ -31,6 +31,7 @@ #import "SLStaticElement.h" #import "SLAlert.h" #import "SLButton.h" +#import "SLEditingMenu.h" #import "SLKeyboard.h" #import "SLPopover.h" #import "SLStatusBar.h" diff --git a/Subliminal.xcodeproj/project.pbxproj b/Subliminal.xcodeproj/project.pbxproj index d291a12..efcd724 100644 --- a/Subliminal.xcodeproj/project.pbxproj +++ b/Subliminal.xcodeproj/project.pbxproj @@ -116,6 +116,10 @@ F077D70F16D9D77900908FF5 /* SLElementVisibilityTestCovered.xib in Resources */ = {isa = PBXBuildFile; fileRef = F077D70C16D9D77900908FF5 /* SLElementVisibilityTestCovered.xib */; }; F078C0491808BF24000767D2 /* SLWebViewTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F078C0471808BF24000767D2 /* SLWebViewTest.m */; }; F078C04A1808BF24000767D2 /* SLWebViewTestViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F078C0481808BF24000767D2 /* SLWebViewTestViewController.m */; }; + F078C0511808C564000767D2 /* SLEditingMenu.h in Headers */ = {isa = PBXBuildFile; fileRef = F078C04F1808C564000767D2 /* SLEditingMenu.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F078C0521808C564000767D2 /* SLEditingMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = F078C0501808C564000767D2 /* SLEditingMenu.m */; }; + F078C05E1808CF7E000767D2 /* SLEditingMenuTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F078C05C1808CF7E000767D2 /* SLEditingMenuTest.m */; }; + F078C05F1808CF7E000767D2 /* SLEditingMenuTestViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F078C05D1808CF7E000767D2 /* SLEditingMenuTestViewController.m */; }; F07DA32D16E43AD3004C2282 /* SLAlertTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F07DA32B16E43AD3004C2282 /* SLAlertTest.m */; }; F07DA32E16E43AD3004C2282 /* SLAlertTestViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F07DA32C16E43AD3004C2282 /* SLAlertTestViewController.m */; }; F08005591730762C00198F6F /* Inklings~iPhone.webarchive in Resources */ = {isa = PBXBuildFile; fileRef = F08005581730762C00198F6F /* Inklings~iPhone.webarchive */; }; @@ -349,6 +353,10 @@ F077D70C16D9D77900908FF5 /* SLElementVisibilityTestCovered.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SLElementVisibilityTestCovered.xib; sourceTree = ""; }; F078C0471808BF24000767D2 /* SLWebViewTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLWebViewTest.m; sourceTree = ""; }; F078C0481808BF24000767D2 /* SLWebViewTestViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLWebViewTestViewController.m; sourceTree = ""; }; + F078C04F1808C564000767D2 /* SLEditingMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SLEditingMenu.h; sourceTree = ""; }; + F078C0501808C564000767D2 /* SLEditingMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLEditingMenu.m; sourceTree = ""; }; + F078C05C1808CF7E000767D2 /* SLEditingMenuTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLEditingMenuTest.m; sourceTree = ""; }; + F078C05D1808CF7E000767D2 /* SLEditingMenuTestViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLEditingMenuTestViewController.m; sourceTree = ""; }; F07DA32B16E43AD3004C2282 /* SLAlertTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLAlertTest.m; sourceTree = ""; }; F07DA32C16E43AD3004C2282 /* SLAlertTestViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SLAlertTestViewController.m; sourceTree = ""; }; F08005581730762C00198F6F /* Inklings~iPhone.webarchive */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = "Inklings~iPhone.webarchive"; sourceTree = ""; }; @@ -755,6 +763,15 @@ name = "SLWebView Tests"; sourceTree = ""; }; + F078C05B1808CF59000767D2 /* SLEditingMenu Tests */ = { + isa = PBXGroup; + children = ( + F078C05C1808CF7E000767D2 /* SLEditingMenuTest.m */, + F078C05D1808CF7E000767D2 /* SLEditingMenuTestViewController.m */, + ); + name = "SLEditingMenu Tests"; + sourceTree = ""; + }; F07DA31F16E439B7004C2282 /* SLAlert Tests */ = { isa = PBXGroup; children = ( @@ -798,6 +815,8 @@ F0C07A4E1704009E00C93F93 /* SLWindow.m */, DB501DC717B9669A001658CB /* SLStatusBar.h */, DB501DC817B9669A001658CB /* SLStatusBar.m */, + F078C04F1808C564000767D2 /* SLEditingMenu.h */, + F078C0501808C564000767D2 /* SLEditingMenu.m */, ); path = "User Interface Elements"; sourceTree = ""; @@ -905,6 +924,7 @@ F077D70016D9D71E00908FF5 /* SLElement Visibility Tests */, F05263C316D2C2CF0090174F /* SLElement Matching Tests */, 0696BA5E16E013DF00DD70CF /* SLElement Gestures and Actions Tests */, + F078C05B1808CF59000767D2 /* SLEditingMenu Tests */, F089F9DC1745FB3800DF1F25 /* SLKeyboard Tests */, F00800D3174C2871001927AC /* SLPopover Tests */, F089F9FD1746B1D200DF1F25 /* SLStaticElement Tests */, @@ -1025,6 +1045,7 @@ F05C4F90171406EF00A381BC /* SLTerminal+ConvenienceFunctions.h in Headers */, F05C51E5171C8AE000A381BC /* SLMainThreadRef.h in Headers */, F0A04E1D1749F70F002C7520 /* SLElement.h in Headers */, + F078C0511808C564000767D2 /* SLEditingMenu.h in Headers */, 2CE9AA4C17E3A747007EF0B5 /* SLSwitch.h in Headers */, F089F98617445D9A00DF1F25 /* SLStaticElement.h in Headers */, F02DF30817EC064F00BE28BF /* UIScrollView+SLProgrammaticScrolling.h in Headers */, @@ -1298,6 +1319,7 @@ F02DF30917EC064F00BE28BF /* UIScrollView+SLProgrammaticScrolling.m in Sources */, CAC388061641CD7500F995F9 /* SLStringUtilities.m in Sources */, CAC388401643503C00F995F9 /* NSObject+SLAccessibilityHierarchy.m in Sources */, + F078C0521808C564000767D2 /* SLEditingMenu.m in Sources */, CA75E78516697C0000D57E92 /* SLDevice.m in Sources */, F0C07A391703F95B00C93F93 /* SLAlert.m in Sources */, F0C07A481703FEF600C93F93 /* SLButton.m in Sources */, @@ -1340,6 +1362,7 @@ 0696BA5916E013D600DD70CF /* SLElementDraggingTestViewController.m in Sources */, F0AE4C8D16F7F92D00B2BB2B /* SLElementStateTest.m in Sources */, F0AE4C8E16F7F92D00B2BB2B /* SLElementStateTestViewController.m in Sources */, + F078C05F1808CF7E000767D2 /* SLEditingMenuTestViewController.m in Sources */, F07DA32D16E43AD3004C2282 /* SLAlertTest.m in Sources */, F07DA32E16E43AD3004C2282 /* SLAlertTestViewController.m in Sources */, 2C903BC117F525E700555317 /* SLSwitchTestViewController.m in Sources */, @@ -1347,6 +1370,7 @@ F01EBC9C170115B100FF6A7C /* SLTextFieldTestViewController.m in Sources */, F077D70D16D9D77900908FF5 /* SLElementVisibilityTest.m in Sources */, F077D70E16D9D77900908FF5 /* SLElementVisibilityTestViewController.m in Sources */, + F078C05E1808CF7E000767D2 /* SLEditingMenuTest.m in Sources */, F0B868EC1740A146008BDA80 /* SLTerminalTestViewController.m in Sources */, F0CC759A173B097800E8F94A /* SLElementTapTest.m in Sources */, F0CC759B173B097800E8F94A /* SLElementTapTestViewController.m in Sources */,