OLD | NEW |
(Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import <AVFoundation/AVFoundation.h> |
| 6 #import <EarlGrey/EarlGrey.h> |
| 7 #import <UIKit/UIKit.h> |
| 8 |
| 9 #import "base/mac/scoped_nsobject.h" |
| 10 #include "base/strings/sys_string_conversions.h" |
| 11 #include "base/test/scoped_command_line.h" |
| 12 #include "components/strings/grit/components_strings.h" |
| 13 #include "components/version_info/version_info.h" |
| 14 #import "ios/chrome/app/main_controller.h" |
| 15 #include "ios/chrome/browser/chrome_switches.h" |
| 16 #import "ios/chrome/browser/ui/browser_view_controller.h" |
| 17 #import "ios/chrome/browser/ui/commands/generic_chrome_command.h" |
| 18 #include "ios/chrome/browser/ui/commands/ios_command_ids.h" |
| 19 #include "ios/chrome/browser/ui/icons/chrome_icon.h" |
| 20 #include "ios/chrome/browser/ui/qr_scanner/camera_controller.h" |
| 21 #include "ios/chrome/browser/ui/qr_scanner/qr_scanner_view.h" |
| 22 #include "ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller.h" |
| 23 #include "ios/chrome/browser/ui/toolbar/web_toolbar_controller.h" |
| 24 #include "ios/chrome/grit/ios_chromium_strings.h" |
| 25 #include "ios/chrome/grit/ios_strings.h" |
| 26 #import "ios/chrome/test/app/chrome_test_util.h" |
| 27 #import "ios/chrome/test/base/scoped_block_swizzler.h" |
| 28 #import "ios/chrome/test/earl_grey/chrome_matchers.h" |
| 29 #import "ios/chrome/test/earl_grey/chrome_test_case.h" |
| 30 #import "ios/testing/earl_grey/disabled_test_macros.h" |
| 31 #include "ios/web/public/test/http_server.h" |
| 32 #include "ios/web/public/test/http_server_util.h" |
| 33 #import "third_party/ocmock/OCMock/OCMock.h" |
| 34 #import "ui/base/l10n/l10n_util.h" |
| 35 #import "ui/base/l10n/l10n_util_mac.h" |
| 36 |
| 37 using namespace chrome_test_util; |
| 38 using namespace qr_scanner; |
| 39 |
| 40 namespace { |
| 41 |
| 42 char kTestURL[] = "http://testurl"; |
| 43 char kTestURLResponse[] = "Test URL page"; |
| 44 char kTestQuery[] = "testquery"; |
| 45 char kTestQueryURL[] = "http://searchurl/testquery"; |
| 46 char kTestQueryResponse[] = "Test query page"; |
| 47 |
| 48 char kTestURLEdited[] = "http://testuredited"; |
| 49 char kTestURLEditedResponse[] = "Test URL edited page"; |
| 50 char kTestQueryEditedURL[] = "http://searchurl/testqueredited"; |
| 51 char kTestQueryEditedResponse[] = "Test query edited page"; |
| 52 |
| 53 // The GREYCondition timeout used for calls to waitWithTimeout:pollInterval:. |
| 54 CFTimeInterval kGREYConditionTimeout = 5; |
| 55 // The GREYCondition poll interval used for calls to |
| 56 // waitWithTimeout:pollInterval:. |
| 57 CFTimeInterval kGREYConditionPollInterval = 0.1; |
| 58 |
| 59 // Returns the GREYMatcher for an element which is visible, interactable, and |
| 60 // enabled. |
| 61 id<GREYMatcher> visibleInteractableEnabled() { |
| 62 return grey_allOf(grey_sufficientlyVisible(), grey_interactable(), |
| 63 grey_enabled(), nil); |
| 64 } |
| 65 |
| 66 // Returns the GREYMatcher for the button that closes the QR Scanner. |
| 67 id<GREYMatcher> qrScannerCloseButton() { |
| 68 return buttonWithAccessibilityLabel( |
| 69 [[ChromeIcon closeIcon] accessibilityLabel]); |
| 70 } |
| 71 |
| 72 // Returns the GREYMatcher for the button which indicates that torch is off and |
| 73 // which turns on the torch. |
| 74 id<GREYMatcher> qrScannerTorchOffButton() { |
| 75 return grey_allOf(grey_accessibilityLabel(l10n_util::GetNSString( |
| 76 IDS_IOS_QR_SCANNER_TORCH_BUTTON_ACCESSIBILITY_LABEL)), |
| 77 grey_accessibilityValue(l10n_util::GetNSString( |
| 78 IDS_IOS_QR_SCANNER_TORCH_OFF_ACCESSIBILITY_VALUE)), |
| 79 grey_accessibilityTrait(UIAccessibilityTraitButton), nil); |
| 80 } |
| 81 |
| 82 // Returns the GREYMatcher for the button which indicates that torch is on and |
| 83 // which turns off the torch. |
| 84 id<GREYMatcher> qrScannerTorchOnButton() { |
| 85 return grey_allOf(grey_accessibilityLabel(l10n_util::GetNSString( |
| 86 IDS_IOS_QR_SCANNER_TORCH_BUTTON_ACCESSIBILITY_LABEL)), |
| 87 grey_accessibilityValue(l10n_util::GetNSString( |
| 88 IDS_IOS_QR_SCANNER_TORCH_ON_ACCESSIBILITY_VALUE)), |
| 89 grey_accessibilityTrait(UIAccessibilityTraitButton), nil); |
| 90 } |
| 91 |
| 92 // Returns the GREYMatcher for the QR Scanner viewport caption. |
| 93 id<GREYMatcher> qrScannerViewportCaption() { |
| 94 return staticTextWithAccessibilityLabelId( |
| 95 IDS_IOS_QR_SCANNER_VIEWPORT_CAPTION); |
| 96 } |
| 97 |
| 98 // Returns the GREYMatcher for the back button in the web toolbar. |
| 99 id<GREYMatcher> webToolbarBackButton() { |
| 100 return buttonWithAccessibilityLabelId(IDS_ACCNAME_BACK); |
| 101 } |
| 102 |
| 103 // Returns the GREYMatcher for the Cancel button to dismiss a UIAlertController. |
| 104 id<GREYMatcher> dialogCancelButton() { |
| 105 return grey_allOf( |
| 106 grey_text(l10n_util::GetNSString(IDS_IOS_QR_SCANNER_ALERT_CANCEL)), |
| 107 grey_accessibilityTrait(UIAccessibilityTraitStaticText), nil); |
| 108 } |
| 109 |
| 110 // Opens the QR Scanner view using a command. |
| 111 // TODO(crbug.com/629776): Replace the command call with a UI action. |
| 112 void showQRScannerWithCommand() { |
| 113 base::scoped_nsobject<GenericChromeCommand> command( |
| 114 [[GenericChromeCommand alloc] initWithTag:IDC_SHOW_QR_SCANNER]); |
| 115 chrome_test_util::RunCommandWithActiveViewController(command); |
| 116 } |
| 117 |
| 118 // Taps the |button|. |
| 119 void tapButton(id<GREYMatcher> button) { |
| 120 [[EarlGrey selectElementWithMatcher:button] performAction:grey_tap()]; |
| 121 } |
| 122 |
| 123 // Appends the given |editText| to the |text| already in the omnibox and presses |
| 124 // the keyboard return key. |
| 125 void editOmniboxTextAndTapKeyboardReturn(std::string text, NSString* editText) { |
| 126 [[EarlGrey selectElementWithMatcher:omniboxText(text)] |
| 127 performAction:grey_typeText([editText stringByAppendingString:@"\n"])]; |
| 128 } |
| 129 |
| 130 // Presses the keyboard return key. |
| 131 void tapKeyboardReturnKeyInOmniboxWithText(std::string text) { |
| 132 [[EarlGrey selectElementWithMatcher:omniboxText(text)] |
| 133 performAction:grey_typeText(@"\n")]; |
| 134 } |
| 135 |
| 136 } // namespace |
| 137 |
| 138 @interface QRScannerViewControllerTestCase : ChromeTestCase { |
| 139 GURL _testURL; |
| 140 GURL _testURLEdited; |
| 141 GURL _testQuery; |
| 142 GURL _testQueryEdited; |
| 143 } |
| 144 |
| 145 @end |
| 146 |
| 147 @implementation QRScannerViewControllerTestCase { |
| 148 // A scoped command line to enable the QR Scanner experiment. |
| 149 std::unique_ptr<base::test::ScopedCommandLine> scoped_command_line_; |
| 150 // A swizzler for the CameraController method cameraControllerWithDelegate:. |
| 151 std::unique_ptr<ScopedBlockSwizzler> camera_controller_swizzler_; |
| 152 // A swizzler for the WebToolbarController method |
| 153 // loadGURLFromLocationBar:transition:. |
| 154 std::unique_ptr<ScopedBlockSwizzler> load_GURL_from_location_bar_swizzler_; |
| 155 } |
| 156 |
| 157 + (void)setUp { |
| 158 [super setUp]; |
| 159 std::map<GURL, std::string> responses; |
| 160 responses[web::test::HttpServer::MakeUrl(kTestURL)] = kTestURLResponse; |
| 161 responses[web::test::HttpServer::MakeUrl(kTestQueryURL)] = kTestQueryResponse; |
| 162 responses[web::test::HttpServer::MakeUrl(kTestURLEdited)] = |
| 163 kTestURLEditedResponse; |
| 164 responses[web::test::HttpServer::MakeUrl(kTestQueryEditedURL)] = |
| 165 kTestQueryEditedResponse; |
| 166 web::test::SetUpSimpleHttpServer(responses); |
| 167 } |
| 168 |
| 169 - (void)setUp { |
| 170 [super setUp]; |
| 171 _testURL = web::test::HttpServer::MakeUrl(kTestURL); |
| 172 _testURLEdited = web::test::HttpServer::MakeUrl(kTestURLEdited); |
| 173 _testQuery = web::test::HttpServer::MakeUrl(kTestQueryURL); |
| 174 _testQueryEdited = web::test::HttpServer::MakeUrl(kTestQueryEditedURL); |
| 175 |
| 176 // Enable the QR Scanner experiment. |
| 177 scoped_command_line_.reset(new base::test::ScopedCommandLine); |
| 178 scoped_command_line_->GetProcessCommandLine()->AppendSwitch( |
| 179 switches::kEnableQRScanner); |
| 180 } |
| 181 |
| 182 - (void)tearDown { |
| 183 [super tearDown]; |
| 184 load_GURL_from_location_bar_swizzler_.reset(); |
| 185 camera_controller_swizzler_.reset(); |
| 186 } |
| 187 |
| 188 // Checks that the close button is visible, interactable, and enabled. |
| 189 - (void)assertCloseButtonIsVisible { |
| 190 [[EarlGrey selectElementWithMatcher:qrScannerCloseButton()] |
| 191 assertWithMatcher:visibleInteractableEnabled()]; |
| 192 } |
| 193 |
| 194 // Checks that the close button is not visible. |
| 195 - (void)assertCloseButtonIsNotVisible { |
| 196 [[EarlGrey selectElementWithMatcher:qrScannerCloseButton()] |
| 197 assertWithMatcher:grey_notVisible()]; |
| 198 } |
| 199 |
| 200 // Checks that the torch off button is visible, interactable, and enabled, and |
| 201 // that the torch on button is not. |
| 202 - (void)assertTorchOffButtonIsVisible { |
| 203 [[EarlGrey selectElementWithMatcher:qrScannerTorchOffButton()] |
| 204 assertWithMatcher:visibleInteractableEnabled()]; |
| 205 [[EarlGrey selectElementWithMatcher:qrScannerTorchOnButton()] |
| 206 assertWithMatcher:grey_notVisible()]; |
| 207 } |
| 208 |
| 209 // Checks that the torch on button is visible, interactable, and enabled, and |
| 210 // that the torch off button is not. |
| 211 - (void)assertTorchOnButtonIsVisible { |
| 212 [[EarlGrey selectElementWithMatcher:qrScannerTorchOnButton()] |
| 213 assertWithMatcher:visibleInteractableEnabled()]; |
| 214 [[EarlGrey selectElementWithMatcher:qrScannerTorchOffButton()] |
| 215 assertWithMatcher:grey_notVisible()]; |
| 216 } |
| 217 |
| 218 // Checks that the torch off button is visible and disabled. |
| 219 - (void)assertTorchButtonIsDisabled { |
| 220 [[EarlGrey selectElementWithMatcher:qrScannerTorchOffButton()] |
| 221 assertWithMatcher:grey_allOf(grey_sufficientlyVisible(), |
| 222 grey_not(grey_enabled()), nil)]; |
| 223 } |
| 224 |
| 225 // Checks that the camera viewport caption is visible. |
| 226 - (void)assertCameraViewportCaptionIsVisible { |
| 227 [[EarlGrey selectElementWithMatcher:qrScannerViewportCaption()] |
| 228 assertWithMatcher:grey_sufficientlyVisible()]; |
| 229 } |
| 230 |
| 231 // Checks that the close button, the camera preview, and the camera viewport |
| 232 // caption are visible. If |torch| is YES, checks that the torch off button is |
| 233 // visible, otherwise checks that the torch button is disabled. If |preview| is |
| 234 // YES, checks that the preview is visible and of the same size as the QR |
| 235 // Scanner view, otherwise checks that the preview is in the view hierarchy but |
| 236 // is hidden. |
| 237 - (void)assertQRScannerUIIsVisibleWithTorch:(BOOL)torch { |
| 238 [self assertCloseButtonIsVisible]; |
| 239 [self assertCameraViewportCaptionIsVisible]; |
| 240 if (torch) { |
| 241 [self assertTorchOffButtonIsVisible]; |
| 242 } else { |
| 243 [self assertTorchButtonIsDisabled]; |
| 244 } |
| 245 } |
| 246 |
| 247 // Presents the QR Scanner with a command, waits for it to be displayed, and |
| 248 // checks if all its views and buttons are visible. Checks that no alerts are |
| 249 // presented. |
| 250 - (void)showQRScannerAndCheckLayoutWithCameraMock:(id)mock { |
| 251 UIViewController* bvc = [self currentBVC]; |
| 252 [self assertModalOfClass:[QRScannerViewController class] |
| 253 isNotPresentedBy:bvc]; |
| 254 [self assertModalOfClass:[UIAlertController class] isNotPresentedBy:bvc]; |
| 255 |
| 256 [self addCameraControllerInitializationExpectations:mock]; |
| 257 showQRScannerWithCommand(); |
| 258 [self waitForModalOfClass:[QRScannerViewController class] toAppearAbove:bvc]; |
| 259 [self assertQRScannerUIIsVisibleWithTorch:NO]; |
| 260 [self assertModalOfClass:[UIAlertController class] |
| 261 isNotPresentedBy:[bvc presentedViewController]]; |
| 262 [self assertModalOfClass:[UIAlertController class] isNotPresentedBy:bvc]; |
| 263 } |
| 264 |
| 265 // Closes the QR scanner by tapping the close button and waits for it to |
| 266 // disappear. |
| 267 - (void)closeQRScannerWithCameraMock:(id)mock { |
| 268 [self addCameraControllerDismissalExpectations:mock]; |
| 269 tapButton(qrScannerCloseButton()); |
| 270 [self waitForModalOfClass:[QRScannerViewController class] |
| 271 toDisappearFromAbove:[self currentBVC]]; |
| 272 } |
| 273 |
| 274 // Returns the current BrowserViewController. |
| 275 - (UIViewController*)currentBVC { |
| 276 // TODO(crbug.com/629516): Evaluate moving this into a common utility. |
| 277 MainController* mainController = chrome_test_util::GetMainController(); |
| 278 return [[mainController browserViewInformation] currentBVC]; |
| 279 } |
| 280 |
| 281 // Checks that the omnibox is visible and contains |text|. |
| 282 - (void)assertOmniboxIsVisibleWithText:(std::string)text { |
| 283 [[EarlGrey selectElementWithMatcher:omniboxText(text)] |
| 284 assertWithMatcher:grey_notNil()]; |
| 285 } |
| 286 |
| 287 // Checks that the page that is currently loaded contains the |response|. |
| 288 - (void)assertTestURLIsLoaded:(std::string)response { |
| 289 id<GREYMatcher> testURLResponseMatcher = |
| 290 chrome_test_util::webViewContainingText(response); |
| 291 [[EarlGrey selectElementWithMatcher:testURLResponseMatcher] |
| 292 assertWithMatcher:grey_notNil()]; |
| 293 } |
| 294 |
| 295 #pragma mark helpers for dialogs |
| 296 |
| 297 // Checks that the modal presented by |viewController| is of class |klass|. |
| 298 - (void)assertModalOfClass:(Class)klass |
| 299 isPresentedBy:(UIViewController*)viewController { |
| 300 UIViewController* modal = [viewController presentedViewController]; |
| 301 NSString* errorString = [NSString |
| 302 stringWithFormat:@"A modal of class %@ should be presented by %@.", klass, |
| 303 [viewController class]]; |
| 304 GREYAssertTrue(modal && [modal isKindOfClass:klass], errorString); |
| 305 } |
| 306 |
| 307 // Checks that the |viewController| is not presenting a modal, or that the modal |
| 308 // presented by |viewController| is not of class |klass|. |
| 309 - (void)assertModalOfClass:(Class)klass |
| 310 isNotPresentedBy:(UIViewController*)viewController { |
| 311 UIViewController* modal = [viewController presentedViewController]; |
| 312 NSString* errorString = [NSString |
| 313 stringWithFormat:@"A modal of class %@ should not be presented by %@.", |
| 314 klass, [viewController class]]; |
| 315 GREYAssertTrue(!modal || ![modal isKindOfClass:klass], errorString); |
| 316 } |
| 317 |
| 318 // Checks that the modal presented by |viewController| is of class |klass| and |
| 319 // waits for the modal's view to load. |
| 320 - (void)waitForModalOfClass:(Class)klass |
| 321 toAppearAbove:(UIViewController*)viewController { |
| 322 [self assertModalOfClass:klass isPresentedBy:viewController]; |
| 323 UIViewController* modal = [viewController presentedViewController]; |
| 324 GREYCondition* modalViewLoadedCondition = |
| 325 [GREYCondition conditionWithName:@"modalViewLoadedCondition" |
| 326 block:^BOOL { |
| 327 return [modal isViewLoaded]; |
| 328 }]; |
| 329 BOOL modalViewLoaded = |
| 330 [modalViewLoadedCondition waitWithTimeout:kGREYConditionTimeout |
| 331 pollInterval:kGREYConditionPollInterval]; |
| 332 NSString* errorString = [NSString |
| 333 stringWithFormat:@"The view of a modal of class %@ should be loaded.", |
| 334 klass]; |
| 335 GREYAssertTrue(modalViewLoaded, errorString); |
| 336 } |
| 337 |
| 338 // Checks that the |viewController| is not presenting a modal, or that the modal |
| 339 // presented by |viewController| is not of class |klass|. If a modal was |
| 340 // previously presented, waits until it is dismissed. |
| 341 - (void)waitForModalOfClass:(Class)klass |
| 342 toDisappearFromAbove:(UIViewController*)viewController { |
| 343 GREYCondition* modalViewDismissedCondition = [GREYCondition |
| 344 conditionWithName:@"modalViewDismissedCondition" |
| 345 block:^BOOL { |
| 346 UIViewController* modal = |
| 347 [viewController presentedViewController]; |
| 348 return !modal || ![modal isKindOfClass:klass]; |
| 349 }]; |
| 350 |
| 351 BOOL modalViewDismissed = |
| 352 [modalViewDismissedCondition waitWithTimeout:kGREYConditionTimeout |
| 353 pollInterval:kGREYConditionPollInterval]; |
| 354 NSString* errorString = [NSString |
| 355 stringWithFormat:@"The modal of class %@ should be loaded.", klass]; |
| 356 GREYAssertTrue(modalViewDismissed, errorString); |
| 357 } |
| 358 |
| 359 // Checks that the QRScannerViewController is presenting a UIAlertController and |
| 360 // that the title of this alert corresponds to |state|. |
| 361 - (void)assertQRScannerIsPresentingADialogForState:(CameraState)state { |
| 362 [self assertModalOfClass:[UIAlertController class] |
| 363 isPresentedBy:[[self currentBVC] presentedViewController]]; |
| 364 [[EarlGrey |
| 365 selectElementWithMatcher:grey_text([self dialogTitleForState:state])] |
| 366 assertWithMatcher:grey_notNil()]; |
| 367 } |
| 368 |
| 369 // Checks that there is no visible alert with title corresponding to |state|. |
| 370 - (void)assertQRScannerIsNotPresentingADialogForState:(CameraState)state { |
| 371 [[EarlGrey |
| 372 selectElementWithMatcher:grey_text([self dialogTitleForState:state])] |
| 373 assertWithMatcher:grey_nil()]; |
| 374 } |
| 375 |
| 376 // Returns the expected title for the dialog which is presented for |state|. |
| 377 - (NSString*)dialogTitleForState:(CameraState)state { |
| 378 base::string16 appName = base::UTF8ToUTF16(version_info::GetProductName()); |
| 379 switch (state) { |
| 380 case CAMERA_AVAILABLE: |
| 381 case CAMERA_NOT_LOADED: |
| 382 return nil; |
| 383 case CAMERA_IN_USE_BY_ANOTHER_APPLICATION: |
| 384 return l10n_util::GetNSString( |
| 385 IDS_IOS_QR_SCANNER_CAMERA_IN_USE_ALERT_TITLE); |
| 386 case CAMERA_PERMISSION_DENIED: |
| 387 return l10n_util::GetNSString( |
| 388 IDS_IOS_QR_SCANNER_CAMERA_PERMISSIONS_HELP_TITLE_GO_TO_SETTINGS); |
| 389 case CAMERA_UNAVAILABLE: |
| 390 return l10n_util::GetNSString( |
| 391 IDS_IOS_QR_SCANNER_CAMERA_UNAVAILABLE_ALERT_TITLE); |
| 392 case MULTIPLE_FOREGROUND_APPS: |
| 393 return l10n_util::GetNSString( |
| 394 IDS_IOS_QR_SCANNER_MULTIPLE_FOREGROUND_APPS_ALERT_TITLE); |
| 395 } |
| 396 } |
| 397 |
| 398 #pragma mark - |
| 399 #pragma mark Helpers for mocks |
| 400 |
| 401 // Swizzles the CameraController method cameraControllerWithDelegate: to return |
| 402 // |cameraControllerMock| instead of a new instance of CameraController. |
| 403 - (void)swizzleCameraController:(id)cameraControllerMock { |
| 404 CameraController* (^swizzleCameraControllerBlock)( |
| 405 id<CameraControllerDelegate>) = ^(id<CameraControllerDelegate> delegate) { |
| 406 // |initWithDelegate:| must return an object with a return count of 1 |
| 407 // because it is preceded by a call to |alloc|. |
| 408 return [cameraControllerMock retain]; |
| 409 }; |
| 410 |
| 411 camera_controller_swizzler_.reset(new ScopedBlockSwizzler( |
| 412 [CameraController class], @selector(initWithDelegate:), |
| 413 swizzleCameraControllerBlock)); |
| 414 } |
| 415 |
| 416 // Swizzles the WebToolbarController loadGURLFromLocationBarBlock:transition: |
| 417 // method to load |searchURL| instead of the generated search URL. |
| 418 - (void)swizzleWebToolbarControllerLoadGURLFromLocationBar: |
| 419 (const GURL&)searchURL { |
| 420 void (^loadGURLFromLocationBarBlock)(WebToolbarController*, const GURL&, |
| 421 ui::PageTransition) = |
| 422 ^void(WebToolbarController* self, const GURL& url, |
| 423 ui::PageTransition transition) { |
| 424 [self.urlLoader loadURL:searchURL |
| 425 referrer:web::Referrer() |
| 426 transition:transition |
| 427 rendererInitiated:NO]; |
| 428 [self cancelOmniboxEdit]; |
| 429 }; |
| 430 |
| 431 load_GURL_from_location_bar_swizzler_.reset( |
| 432 new ScopedBlockSwizzler([WebToolbarController class], |
| 433 @selector(loadGURLFromLocationBar:transition:), |
| 434 loadGURLFromLocationBarBlock)); |
| 435 } |
| 436 |
| 437 // Creates a new CameraController mock with camera permission granted if |
| 438 // |granted| is set to YES. |
| 439 - (id)getCameraControllerMockWithAuthorizationStatus: |
| 440 (AVAuthorizationStatus)authorizationStatus { |
| 441 id mock = [OCMockObject mockForClass:[CameraController class]]; |
| 442 [[[mock stub] andReturnValue:OCMOCK_VALUE(authorizationStatus)] |
| 443 getAuthorizationStatus]; |
| 444 return mock; |
| 445 } |
| 446 |
| 447 #pragma mark delegate calls |
| 448 |
| 449 // Calls |cameraStateChanged:| on the presented QRScannerViewController. |
| 450 - (void)callCameraStateChanged:(CameraState)state { |
| 451 QRScannerViewController* vc = |
| 452 (QRScannerViewController*)[[self currentBVC] presentedViewController]; |
| 453 [vc cameraStateChanged:state]; |
| 454 } |
| 455 |
| 456 // Calls |torchStateChanged:| on the presented QRScannerViewController. |
| 457 - (void)callTorchStateChanged:(BOOL)torchIsOn { |
| 458 QRScannerViewController* vc = |
| 459 (QRScannerViewController*)[[self currentBVC] presentedViewController]; |
| 460 [vc torchStateChanged:torchIsOn]; |
| 461 } |
| 462 |
| 463 // Calls |torchAvailabilityChanged:| on the presented QRScannerViewController. |
| 464 - (void)callTorchAvailabilityChanged:(BOOL)torchIsAvailable { |
| 465 QRScannerViewController* vc = |
| 466 (QRScannerViewController*)[[self currentBVC] presentedViewController]; |
| 467 [vc torchAvailabilityChanged:torchIsAvailable]; |
| 468 } |
| 469 |
| 470 // Calls |receiveQRScannerResult:| on the presented QRScannerViewController. |
| 471 - (void)callReceiveQRScannerResult:(NSString*)result { |
| 472 QRScannerViewController* vc = |
| 473 (QRScannerViewController*)[[self currentBVC] presentedViewController]; |
| 474 [vc receiveQRScannerResult:result loadImmediately:NO]; |
| 475 } |
| 476 |
| 477 #pragma mark expectations |
| 478 |
| 479 // Adds functions which are expected to be called when the |
| 480 // QRScannerViewController is presented to |cameraControllerMock|. |
| 481 - (void)addCameraControllerInitializationExpectations:(id)cameraControllerMock { |
| 482 [[cameraControllerMock expect] setTorchMode:AVCaptureTorchModeOff]; |
| 483 [[cameraControllerMock expect] loadCaptureSession:[OCMArg any]]; |
| 484 [[cameraControllerMock expect] startRecording]; |
| 485 } |
| 486 |
| 487 // Adds functions which are expected to be called when the |
| 488 // QRScannerViewController is dismissed to |cameraControllerMock|. |
| 489 - (void)addCameraControllerDismissalExpectations:(id)cameraControllerMock { |
| 490 [[cameraControllerMock expect] setTorchMode:AVCaptureTorchModeOff]; |
| 491 [[cameraControllerMock expect] stopRecording]; |
| 492 } |
| 493 |
| 494 // Adds functions which are expected to be called when the torch is switched on |
| 495 // to |cameraControllerMock|. |
| 496 - (void)addCameraControllerTorchOnExpectations:(id)cameraControllerMock { |
| 497 [[[cameraControllerMock expect] andReturnValue:@NO] isTorchActive]; |
| 498 [[cameraControllerMock expect] setTorchMode:AVCaptureTorchModeOn]; |
| 499 } |
| 500 |
| 501 // Adds functions which are expected to be called when the torch is switched off |
| 502 // to |cameraControllerMock|. |
| 503 - (void)addCameraControllerTorchOffExpectations:(id)cameraControllerMock { |
| 504 [[[cameraControllerMock expect] andReturnValue:@YES] isTorchActive]; |
| 505 [[cameraControllerMock expect] setTorchMode:AVCaptureTorchModeOff]; |
| 506 } |
| 507 |
| 508 #pragma mark - |
| 509 #pragma mark Tests |
| 510 |
| 511 // Tests that the close button, camera preview, viewport caption, and the torch |
| 512 // button are visible if the camera is available. The preview is delayed. |
| 513 - (void)testQRScannerUIIsShown { |
| 514 id cameraControllerMock = |
| 515 [self getCameraControllerMockWithAuthorizationStatus: |
| 516 AVAuthorizationStatusAuthorized]; |
| 517 [self swizzleCameraController:cameraControllerMock]; |
| 518 |
| 519 // Open the QR scanner. |
| 520 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; |
| 521 |
| 522 // Preview is loaded and camera is ready to be displayed. |
| 523 [self assertQRScannerUIIsVisibleWithTorch:NO]; |
| 524 |
| 525 // Close the QR scanner. |
| 526 [self closeQRScannerWithCameraMock:cameraControllerMock]; |
| 527 [cameraControllerMock verify]; |
| 528 } |
| 529 |
| 530 // Tests that the torch is switched on and off when pressing the torch button, |
| 531 // and that the button icon changes accordingly. |
| 532 - (void)testTurningTorchOnAndOff { |
| 533 id cameraControllerMock = |
| 534 [self getCameraControllerMockWithAuthorizationStatus: |
| 535 AVAuthorizationStatusAuthorized]; |
| 536 [self swizzleCameraController:cameraControllerMock]; |
| 537 |
| 538 // Open the QR scanner. |
| 539 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; |
| 540 |
| 541 // Torch becomes available. |
| 542 [self callTorchAvailabilityChanged:YES]; |
| 543 [self assertQRScannerUIIsVisibleWithTorch:YES]; |
| 544 |
| 545 // Turn torch on. |
| 546 [self addCameraControllerTorchOnExpectations:cameraControllerMock]; |
| 547 [self assertTorchOffButtonIsVisible]; |
| 548 tapButton(qrScannerTorchOffButton()); |
| 549 [self assertTorchOffButtonIsVisible]; |
| 550 |
| 551 // Torch becomes active. |
| 552 [self callTorchStateChanged:YES]; |
| 553 [self assertTorchOnButtonIsVisible]; |
| 554 |
| 555 // Turn torch off. |
| 556 [self addCameraControllerTorchOffExpectations:cameraControllerMock]; |
| 557 tapButton(qrScannerTorchOnButton()); |
| 558 [self assertTorchOnButtonIsVisible]; |
| 559 |
| 560 // Torch becomes inactive. |
| 561 [self callTorchStateChanged:NO]; |
| 562 [self assertTorchOffButtonIsVisible]; |
| 563 |
| 564 // Close the QR scanner. |
| 565 [self closeQRScannerWithCameraMock:cameraControllerMock]; |
| 566 [cameraControllerMock verify]; |
| 567 } |
| 568 |
| 569 // Tests that if the QR scanner is closed while the torch is on, the torch is |
| 570 // switched off and the correct button indicating that the torch is off is shown |
| 571 // when the scanner is opened again. |
| 572 - (void)testTorchButtonIsResetWhenQRScannerIsReopened { |
| 573 id cameraControllerMock = |
| 574 [self getCameraControllerMockWithAuthorizationStatus: |
| 575 AVAuthorizationStatusAuthorized]; |
| 576 [self swizzleCameraController:cameraControllerMock]; |
| 577 |
| 578 // Open the QR scanner. |
| 579 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; |
| 580 [self assertQRScannerUIIsVisibleWithTorch:NO]; |
| 581 [self callTorchAvailabilityChanged:YES]; |
| 582 [self assertQRScannerUIIsVisibleWithTorch:YES]; |
| 583 |
| 584 // Turn torch on. |
| 585 [self addCameraControllerTorchOnExpectations:cameraControllerMock]; |
| 586 tapButton(qrScannerTorchOffButton()); |
| 587 [self callTorchStateChanged:YES]; |
| 588 [self assertTorchOnButtonIsVisible]; |
| 589 |
| 590 // Close the QR scanner. |
| 591 [self closeQRScannerWithCameraMock:cameraControllerMock]; |
| 592 |
| 593 // Reopen the QR scanner. |
| 594 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; |
| 595 [self callTorchAvailabilityChanged:YES]; |
| 596 [self assertTorchOffButtonIsVisible]; |
| 597 |
| 598 // Close the QR scanner again. |
| 599 [self closeQRScannerWithCameraMock:cameraControllerMock]; |
| 600 [cameraControllerMock verify]; |
| 601 } |
| 602 |
| 603 // Tests that the torch button is disabled when the camera reports that torch |
| 604 // became unavailable. |
| 605 - (void)testTorchButtonIsDisabledWhenTorchBecomesUnavailable { |
| 606 id cameraControllerMock = |
| 607 [self getCameraControllerMockWithAuthorizationStatus: |
| 608 AVAuthorizationStatusAuthorized]; |
| 609 [self swizzleCameraController:cameraControllerMock]; |
| 610 |
| 611 // Open the QR scanner. |
| 612 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; |
| 613 |
| 614 // Torch becomes available. |
| 615 [self callTorchAvailabilityChanged:YES]; |
| 616 [self assertQRScannerUIIsVisibleWithTorch:YES]; |
| 617 |
| 618 // Torch becomes unavailable. |
| 619 [self callTorchAvailabilityChanged:NO]; |
| 620 [self assertQRScannerUIIsVisibleWithTorch:NO]; |
| 621 |
| 622 // Close the QR scanner. |
| 623 [self closeQRScannerWithCameraMock:cameraControllerMock]; |
| 624 [cameraControllerMock verify]; |
| 625 } |
| 626 |
| 627 #pragma mark dialogs |
| 628 |
| 629 // Tests that a UIAlertController is presented instead of the |
| 630 // QRScannerViewController if the camera is unavailable. |
| 631 - (void)testCameraUnavailableDialog { |
| 632 // TODO(crbug.com/663026): Reenable the test for devices. |
| 633 #if !TARGET_IPHONE_SIMULATOR |
| 634 EARL_GREY_TEST_DISABLED(@"Disabled for devices because existing system " |
| 635 @"alerts would prevent app alerts to present " |
| 636 @"correctly."); |
| 637 #endif |
| 638 |
| 639 UIViewController* bvc = [self currentBVC]; |
| 640 [self assertModalOfClass:[QRScannerViewController class] |
| 641 isNotPresentedBy:bvc]; |
| 642 [self assertModalOfClass:[UIAlertController class] isNotPresentedBy:bvc]; |
| 643 id cameraControllerMock = |
| 644 [self getCameraControllerMockWithAuthorizationStatus: |
| 645 AVAuthorizationStatusDenied]; |
| 646 [self swizzleCameraController:cameraControllerMock]; |
| 647 |
| 648 showQRScannerWithCommand(); |
| 649 [self assertModalOfClass:[QRScannerViewController class] |
| 650 isNotPresentedBy:bvc]; |
| 651 [self waitForModalOfClass:[UIAlertController class] toAppearAbove:bvc]; |
| 652 |
| 653 tapButton(dialogCancelButton()); |
| 654 [self waitForModalOfClass:[UIAlertController class] toDisappearFromAbove:bvc]; |
| 655 } |
| 656 |
| 657 // Tests that a UIAlertController is presented by the QRScannerViewController if |
| 658 // the camera state changes after the QRScannerViewController is presented. |
| 659 - (void)testDialogIsDisplayedIfCameraStateChanges { |
| 660 // TODO(crbug.com/663026): Reenable the test for devices. |
| 661 #if !TARGET_IPHONE_SIMULATOR |
| 662 EARL_GREY_TEST_DISABLED(@"Disabled for devices because existing system " |
| 663 @"alerts would prevent app alerts to present " |
| 664 @"correctly."); |
| 665 #endif |
| 666 |
| 667 id cameraControllerMock = |
| 668 [self getCameraControllerMockWithAuthorizationStatus: |
| 669 AVAuthorizationStatusAuthorized]; |
| 670 [self swizzleCameraController:cameraControllerMock]; |
| 671 |
| 672 std::vector<CameraState> tests{MULTIPLE_FOREGROUND_APPS, CAMERA_UNAVAILABLE, |
| 673 CAMERA_PERMISSION_DENIED, |
| 674 CAMERA_IN_USE_BY_ANOTHER_APPLICATION}; |
| 675 |
| 676 for (const CameraState& state : tests) { |
| 677 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; |
| 678 [self callCameraStateChanged:state]; |
| 679 [self assertQRScannerIsPresentingADialogForState:state]; |
| 680 |
| 681 // Close the dialog. |
| 682 [self addCameraControllerDismissalExpectations:cameraControllerMock]; |
| 683 tapButton(dialogCancelButton()); |
| 684 UIViewController* bvc = [self currentBVC]; |
| 685 [self waitForModalOfClass:[QRScannerViewController class] |
| 686 toDisappearFromAbove:bvc]; |
| 687 [self assertModalOfClass:[UIAlertController class] isNotPresentedBy:bvc]; |
| 688 } |
| 689 |
| 690 [cameraControllerMock verify]; |
| 691 } |
| 692 |
| 693 // Tests that a new dialog replaces an old dialog if the camera state changes. |
| 694 - (void)testDialogIsReplacedIfCameraStateChanges { |
| 695 // TODO(crbug.com/663026): Reenable the test for devices. |
| 696 #if !TARGET_IPHONE_SIMULATOR |
| 697 EARL_GREY_TEST_DISABLED(@"Disabled for devices because existing system " |
| 698 @"alerts would prevent app alerts to present " |
| 699 @"correctly."); |
| 700 #endif |
| 701 |
| 702 id cameraControllerMock = |
| 703 [self getCameraControllerMockWithAuthorizationStatus: |
| 704 AVAuthorizationStatusAuthorized]; |
| 705 [self swizzleCameraController:cameraControllerMock]; |
| 706 |
| 707 // Change state to CAMERA_UNAVAILABLE. |
| 708 CameraState currentState = CAMERA_UNAVAILABLE; |
| 709 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; |
| 710 [self callCameraStateChanged:currentState]; |
| 711 [self assertQRScannerIsPresentingADialogForState:currentState]; |
| 712 |
| 713 std::vector<CameraState> tests{ |
| 714 CAMERA_PERMISSION_DENIED, MULTIPLE_FOREGROUND_APPS, |
| 715 CAMERA_IN_USE_BY_ANOTHER_APPLICATION, CAMERA_UNAVAILABLE}; |
| 716 |
| 717 for (const CameraState& state : tests) { |
| 718 [self callCameraStateChanged:state]; |
| 719 [self assertQRScannerIsPresentingADialogForState:state]; |
| 720 [self assertQRScannerIsNotPresentingADialogForState:currentState]; |
| 721 currentState = state; |
| 722 } |
| 723 |
| 724 // Cancel the dialog. |
| 725 [self addCameraControllerDismissalExpectations:cameraControllerMock]; |
| 726 tapButton(dialogCancelButton()); |
| 727 [self waitForModalOfClass:[QRScannerViewController class] |
| 728 toDisappearFromAbove:[self currentBVC]]; |
| 729 [self assertModalOfClass:[UIAlertController class] |
| 730 isNotPresentedBy:[self currentBVC]]; |
| 731 |
| 732 [cameraControllerMock verify]; |
| 733 } |
| 734 |
| 735 // Tests that an error dialog is dismissed if the camera becomes available. |
| 736 - (void)testDialogDismissedIfCameraBecomesAvailable { |
| 737 id cameraControllerMock = |
| 738 [self getCameraControllerMockWithAuthorizationStatus: |
| 739 AVAuthorizationStatusAuthorized]; |
| 740 [self swizzleCameraController:cameraControllerMock]; |
| 741 |
| 742 std::vector<CameraState> tests{CAMERA_IN_USE_BY_ANOTHER_APPLICATION, |
| 743 CAMERA_UNAVAILABLE, MULTIPLE_FOREGROUND_APPS, |
| 744 CAMERA_PERMISSION_DENIED}; |
| 745 |
| 746 for (const CameraState& state : tests) { |
| 747 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; |
| 748 [self callCameraStateChanged:state]; |
| 749 [self assertQRScannerIsPresentingADialogForState:state]; |
| 750 |
| 751 // Change state to CAMERA_AVAILABLE. |
| 752 [self callCameraStateChanged:CAMERA_AVAILABLE]; |
| 753 [self assertQRScannerIsNotPresentingADialogForState:state]; |
| 754 [self closeQRScannerWithCameraMock:cameraControllerMock]; |
| 755 } |
| 756 |
| 757 [cameraControllerMock verify]; |
| 758 } |
| 759 |
| 760 #pragma mark scanned result |
| 761 |
| 762 // A helper function for testing that the view controller correctly passes the |
| 763 // received results to its delegate and that pages can be loaded. The result |
| 764 // received from the camera controller is in |result|, |response| is the |
| 765 // expected response on the loaded page, and |editString| is a nullable string |
| 766 // which can be appended to the response in the omnibox before the page is |
| 767 // loaded. |
| 768 - (void)doTestReceivingResult:(std::string)result |
| 769 response:(std::string)response |
| 770 edit:(NSString*)editString { |
| 771 id cameraControllerMock = |
| 772 [self getCameraControllerMockWithAuthorizationStatus: |
| 773 AVAuthorizationStatusAuthorized]; |
| 774 [self swizzleCameraController:cameraControllerMock]; |
| 775 |
| 776 // Open the QR scanner. |
| 777 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; |
| 778 [self callTorchAvailabilityChanged:YES]; |
| 779 [self assertQRScannerUIIsVisibleWithTorch:YES]; |
| 780 |
| 781 // Receive a scanned result from the camera. |
| 782 [self addCameraControllerDismissalExpectations:cameraControllerMock]; |
| 783 [self callReceiveQRScannerResult:base::SysUTF8ToNSString(result)]; |
| 784 |
| 785 [self waitForModalOfClass:[QRScannerViewController class] |
| 786 toDisappearFromAbove:[self currentBVC]]; |
| 787 [cameraControllerMock verify]; |
| 788 |
| 789 // Optionally edit the text in the omnibox before pressing return. |
| 790 [self assertOmniboxIsVisibleWithText:result]; |
| 791 if (editString != nil) { |
| 792 editOmniboxTextAndTapKeyboardReturn(result, editString); |
| 793 } else { |
| 794 tapKeyboardReturnKeyInOmniboxWithText(result); |
| 795 } |
| 796 [self assertTestURLIsLoaded:response]; |
| 797 |
| 798 // Press the back button to get back to the NTP. |
| 799 tapButton(webToolbarBackButton()); |
| 800 [self assertModalOfClass:[QRScannerViewController class] |
| 801 isNotPresentedBy:[self currentBVC]]; |
| 802 } |
| 803 |
| 804 // Test that the correct page is loaded if the scanner result is a URL. |
| 805 - (void)testReceivingQRScannerURLResult { |
| 806 [self doTestReceivingResult:_testURL.GetContent() |
| 807 response:kTestURLResponse |
| 808 edit:nil]; |
| 809 } |
| 810 |
| 811 // Test that the correct page is loaded if the scanner result is a URL which is |
| 812 // then manually edited. |
| 813 - (void)testReceivingQRScannerURLResultAndEditingTheURL { |
| 814 [self doTestReceivingResult:_testURL.GetContent() |
| 815 response:kTestURLEditedResponse |
| 816 edit:@"\b\bedited/"]; |
| 817 } |
| 818 |
| 819 // Test that the correct page is loaded if the scanner result is a search query. |
| 820 - (void)testReceivingQRScannerSearchQueryResult { |
| 821 [self swizzleWebToolbarControllerLoadGURLFromLocationBar:_testQuery]; |
| 822 [self doTestReceivingResult:kTestQuery response:kTestQueryResponse edit:nil]; |
| 823 } |
| 824 |
| 825 // Test that the correct page is loaded if the scanner result is a search query |
| 826 // which is then manually edited. |
| 827 - (void)testReceivingQRScannerSearchQueryResultAndEditingTheQuery { |
| 828 [self swizzleWebToolbarControllerLoadGURLFromLocationBar:_testQueryEdited]; |
| 829 [self doTestReceivingResult:kTestQuery |
| 830 response:kTestQueryEditedResponse |
| 831 edit:@"\bedited"]; |
| 832 } |
| 833 |
| 834 @end |
OLD | NEW |