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 <UIKit/UIKit.h> 7 8#include "base/ios/ios_util.h" 9#include "base/strings/stringprintf.h" 10#include "base/strings/sys_string_conversions.h" 11#import "ios/chrome/browser/ui/qr_scanner/qr_scanner_app_interface.h" 12#include "ios/chrome/browser/ui/scanner/camera_state.h" 13#include "ios/chrome/grit/ios_strings.h" 14#import "ios/chrome/test/earl_grey/chrome_earl_grey.h" 15#import "ios/chrome/test/earl_grey/chrome_matchers.h" 16#import "ios/chrome/test/earl_grey/chrome_test_case.h" 17#include "ios/chrome/test/earl_grey/earl_grey_scoped_block_swizzler.h" 18#import "ios/testing/earl_grey/earl_grey_test.h" 19#import "net/base/mac/url_conversions.h" 20#include "net/test/embedded_test_server/embedded_test_server.h" 21#include "net/test/embedded_test_server/http_request.h" 22#include "net/test/embedded_test_server/http_response.h" 23#import "third_party/ocmock/OCMock/OCMock.h" 24#import "ui/base/l10n/l10n_util.h" 25#import "ui/base/l10n/l10n_util_mac.h" 26 27#if !defined(__has_feature) || !__has_feature(objc_arc) 28#error "This file requires ARC support." 29#endif 30 31using scanner::CameraState; 32 33// Override a QRScannerViewController voice over check, simulating voice 34// over being enabled. This doesn't reset the previous value, don't use 35// nested. 36class ScopedQRScannerVoiceSearchOverride { 37 public: 38 ScopedQRScannerVoiceSearchOverride(UIViewController* scanner_view_controller) 39 : scanner_view_controller_(scanner_view_controller) { 40 [QRScannerAppInterface 41 overrideVoiceOverCheckForQRScannerViewController: 42 scanner_view_controller_ 43 isOn:YES]; 44 } 45 46 ~ScopedQRScannerVoiceSearchOverride() { 47 [QRScannerAppInterface overrideVoiceOverCheckForQRScannerViewController: 48 scanner_view_controller_ 49 isOn:NO]; 50 } 51 52 private: 53 UIViewController* scanner_view_controller_; 54 55 DISALLOW_COPY_AND_ASSIGN(ScopedQRScannerVoiceSearchOverride); 56}; 57 58// TODO(crbug.com/1015113) The EG2 macro is breaking indexing for some reason 59// without the trailing semicolon. For now, disable the extra semi warning 60// so Xcode indexing works for the egtest. 61#pragma clang diagnostic push 62#pragma clang diagnostic ignored "-Wc++98-compat-extra-semi" 63GREY_STUB_CLASS_IN_APP_MAIN_QUEUE(QRScannerAppInterface); 64 65namespace { 66 67char kTestURL[] = "/testurl"; 68char kTestURLResponse[] = "Test URL page"; 69char kTestQuery[] = "testquery"; 70char kTestQueryURL[] = "/searchurl/testquery"; 71char kTestQueryResponse[] = "Test query page"; 72 73char kTestURLEdited[] = "/testuredited"; 74char kTestURLEditedResponse[] = "Test URL edited page"; 75char kTestQueryEditedURL[] = "/searchurl/testqueredited"; 76char kTestQueryEditedResponse[] = "Test query edited page"; 77 78// The GREYCondition timeout used for calls to waitWithTimeout:pollInterval:. 79CFTimeInterval kGREYConditionTimeout = 5; 80// The GREYCondition poll interval used for calls to 81// waitWithTimeout:pollInterval:. 82CFTimeInterval kGREYConditionPollInterval = 0.1; 83 84// Returns the GREYMatcher for an element which is visible, interactable, and 85// enabled. 86id<GREYMatcher> VisibleInteractableEnabled() { 87 return grey_allOf(grey_sufficientlyVisible(), grey_interactable(), 88 grey_enabled(), nil); 89} 90 91// Returns the GREYMatcher for the button that closes the QR Scanner. 92id<GREYMatcher> QrScannerCloseButton() { 93 return grey_allOf(chrome_test_util::ButtonWithAccessibilityLabel( 94 QRScannerAppInterface.closeIconAccessibilityLabel), 95 grey_userInteractionEnabled(), nil); 96} 97 98// Returns the GREYMatcher for the button which indicates that torch is off and 99// which turns on the torch. 100id<GREYMatcher> QrScannerTorchOffButton() { 101 return grey_allOf(grey_accessibilityLabel(l10n_util::GetNSString( 102 IDS_IOS_SCANNER_TORCH_BUTTON_ACCESSIBILITY_LABEL)), 103 grey_accessibilityValue(l10n_util::GetNSString( 104 IDS_IOS_SCANNER_TORCH_OFF_ACCESSIBILITY_VALUE)), 105 grey_accessibilityTrait(UIAccessibilityTraitButton), nil); 106} 107 108// Returns the GREYMatcher for the button which indicates that torch is on and 109// which turns off the torch. 110id<GREYMatcher> QrScannerTorchOnButton() { 111 return grey_allOf(grey_accessibilityLabel(l10n_util::GetNSString( 112 IDS_IOS_SCANNER_TORCH_BUTTON_ACCESSIBILITY_LABEL)), 113 grey_accessibilityValue(l10n_util::GetNSString( 114 IDS_IOS_SCANNER_TORCH_ON_ACCESSIBILITY_VALUE)), 115 grey_accessibilityTrait(UIAccessibilityTraitButton), nil); 116} 117 118// Returns the GREYMatcher for the QR Scanner viewport caption. 119id<GREYMatcher> QrScannerViewportCaption() { 120 return chrome_test_util::StaticTextWithAccessibilityLabelId( 121 IDS_IOS_QR_SCANNER_VIEWPORT_CAPTION); 122} 123 124// Returns the GREYMatcher for the Cancel button to dismiss a UIAlertController. 125id<GREYMatcher> DialogCancelButton() { 126 return grey_allOf( 127 grey_text(l10n_util::GetNSString(IDS_IOS_QR_SCANNER_ALERT_CANCEL)), 128 grey_accessibilityTrait(UIAccessibilityTraitStaticText), 129 grey_sufficientlyVisible(), nil); 130} 131 132// Opens the QR Scanner view. 133void ShowQRScanner() { 134 // Tap the omnibox to get the keyboard accessory view to show up. 135 [[EarlGrey selectElementWithMatcher:chrome_test_util::NewTabPageOmnibox()] 136 performAction:grey_tap()]; 137 [ChromeEarlGrey 138 waitForSufficientlyVisibleElementWithMatcher:chrome_test_util::Omnibox()]; 139 140 // Tap the QR Code scanner button in the keyboard accessory view. 141 [[EarlGrey 142 selectElementWithMatcher:grey_accessibilityLabel(@"QR code Search")] 143 performAction:grey_tap()]; 144} 145 146// Taps the |button|. 147void TapButton(id<GREYMatcher> button) { 148 [[EarlGrey selectElementWithMatcher:button] performAction:grey_tap()]; 149} 150 151// Appends the given |editText| to the |text| already in the omnibox and presses 152// the keyboard return key. 153void EditOmniboxTextAndTapKeyboardReturn(std::string text, NSString* editText) { 154 [[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(text)] 155 performAction:grey_typeText([editText stringByAppendingString:@"\n"])]; 156} 157 158// Presses the keyboard return key. 159void TapKeyboardReturnKeyInOmniboxWithText(std::string text) { 160 [[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(text)] 161 performAction:grey_typeText(@"\n")]; 162} 163 164// Provides responses for the test page URLs. 165std::unique_ptr<net::test_server::HttpResponse> StandardResponse( 166 const net::test_server::HttpRequest& request) { 167 std::unique_ptr<net::test_server::BasicHttpResponse> http_response = 168 std::make_unique<net::test_server::BasicHttpResponse>(); 169 http_response->set_code(net::HTTP_OK); 170 171 char* body_content = nullptr; 172 if (base::StartsWith(request.relative_url, kTestURL, 173 base::CompareCase::SENSITIVE)) { 174 body_content = kTestURLResponse; 175 } else if (base::StartsWith(request.relative_url, kTestQueryURL, 176 base::CompareCase::SENSITIVE)) { 177 body_content = kTestQueryResponse; 178 } else if (base::StartsWith(request.relative_url, kTestURLEdited, 179 base::CompareCase::SENSITIVE)) { 180 body_content = kTestURLEditedResponse; 181 } else if (base::StartsWith(request.relative_url, kTestQueryEditedURL, 182 base::CompareCase::SENSITIVE)) { 183 body_content = kTestQueryEditedResponse; 184 } else { 185 return nullptr; 186 } 187 188 if (body_content) { 189 http_response->set_content( 190 base::StringPrintf("<html><body>%s</body></html>", body_content)); 191 } 192 193 return std::move(http_response); 194} 195 196} // namespace 197 198#pragma mark - Test Case 199 200@interface QRScannerViewControllerTestCase : ChromeTestCase { 201 GURL _testURL; 202 GURL _testURLEdited; 203 GURL _testQuery; 204 GURL _testQueryEdited; 205} 206 207@end 208 209@implementation QRScannerViewControllerTestCase { 210 // A swizzler for the CameraController method cameraControllerWithDelegate:. 211 std::unique_ptr<EarlGreyScopedBlockSwizzler> _camera_controller_swizzler; 212 // A swizzler for the LocationBarCoordinator method 213 // loadGURLFromLocationBar:transition:. 214 std::unique_ptr<EarlGreyScopedBlockSwizzler> 215 _load_GURL_from_location_bar_swizzler; 216} 217 218- (void)setUp { 219 [super setUp]; 220 221 self.testServer->RegisterRequestHandler( 222 base::BindRepeating(&StandardResponse)); 223 GREYAssertTrue(self.testServer->Start(), @"Server did not start."); 224 225 _testURL = self.testServer->GetURL(kTestURL); 226 _testURLEdited = self.testServer->GetURL(kTestURLEdited); 227 _testQuery = self.testServer->GetURL(kTestQueryURL); 228 _testQueryEdited = self.testServer->GetURL(kTestQueryEditedURL); 229} 230 231- (void)tearDown { 232 [super tearDown]; 233 _load_GURL_from_location_bar_swizzler.reset(); 234 _camera_controller_swizzler.reset(); 235} 236 237// Checks that the close button is visible, interactable, and enabled. 238- (void)assertCloseButtonIsVisible { 239 [[EarlGrey selectElementWithMatcher:QrScannerCloseButton()] 240 assertWithMatcher:VisibleInteractableEnabled()]; 241} 242 243// Checks that the torch off button is visible, interactable, and enabled, and 244// that the torch on button is not. 245- (void)assertTorchOffButtonIsVisible { 246 [[EarlGrey selectElementWithMatcher:QrScannerTorchOffButton()] 247 assertWithMatcher:VisibleInteractableEnabled()]; 248 [[EarlGrey selectElementWithMatcher:QrScannerTorchOnButton()] 249 assertWithMatcher:grey_notVisible()]; 250} 251 252// Checks that the torch on button is visible, interactable, and enabled, and 253// that the torch off button is not. 254- (void)assertTorchOnButtonIsVisible { 255 [[EarlGrey selectElementWithMatcher:QrScannerTorchOnButton()] 256 assertWithMatcher:VisibleInteractableEnabled()]; 257 [[EarlGrey selectElementWithMatcher:QrScannerTorchOffButton()] 258 assertWithMatcher:grey_notVisible()]; 259} 260 261// Checks that the torch off button is visible and disabled. 262- (void)assertTorchButtonIsDisabled { 263 [[EarlGrey selectElementWithMatcher:QrScannerTorchOffButton()] 264 assertWithMatcher:grey_allOf(grey_sufficientlyVisible(), 265 grey_not(grey_enabled()), nil)]; 266} 267 268// Checks that the camera viewport caption is visible. 269- (void)assertCameraViewportCaptionIsVisible { 270 [[EarlGrey selectElementWithMatcher:QrScannerViewportCaption()] 271 assertWithMatcher:grey_sufficientlyVisible()]; 272} 273 274// Checks that the close button, the camera preview, and the camera viewport 275// caption are visible. If |torch| is YES, checks that the torch off button is 276// visible, otherwise checks that the torch button is disabled. If |preview| is 277// YES, checks that the preview is visible and of the same size as the QR 278// Scanner view, otherwise checks that the preview is in the view hierarchy but 279// is hidden. 280- (void)assertQRScannerUIIsVisibleWithTorch:(BOOL)torch { 281 [self assertCloseButtonIsVisible]; 282 [self assertCameraViewportCaptionIsVisible]; 283 if (torch) { 284 [self assertTorchOffButtonIsVisible]; 285 } else { 286 [self assertTorchButtonIsDisabled]; 287 } 288} 289 290// Presents the QR Scanner with a command, waits for it to be displayed, and 291// checks if all its views and buttons are visible. Checks that no alerts are 292// presented. 293- (void)showQRScannerAndCheckLayoutWithCameraMock:(id)mock { 294 UIViewController* bvc = QRScannerAppInterface.currentBrowserViewController; 295 NSError* error = 296 [QRScannerAppInterface assertModalOfClass:@"QRScannerViewController" 297 isNotPresentedBy:bvc]; 298 GREYAssertNil(error, error.localizedDescription); 299 error = [QRScannerAppInterface assertModalOfClass:@"UIAlertController" 300 isNotPresentedBy:bvc]; 301 GREYAssertNil(error, error.localizedDescription); 302 303 [QRScannerAppInterface addCameraControllerInitializationExpectations:mock]; 304 ShowQRScanner(); 305 [self waitForModalOfClass:@"QRScannerViewController" toAppearAbove:bvc]; 306 [self assertQRScannerUIIsVisibleWithTorch:NO]; 307 error = 308 [QRScannerAppInterface assertModalOfClass:@"UIAlertController" 309 isNotPresentedBy:[bvc presentedViewController]]; 310 GREYAssertNil(error, error.localizedDescription); 311 error = [QRScannerAppInterface assertModalOfClass:@"UIAlertController" 312 isNotPresentedBy:bvc]; 313 GREYAssertNil(error, error.localizedDescription); 314} 315 316// Closes the QR scanner by tapping the close button and waits for it to 317// disappear. 318- (void)closeQRScannerWithCameraMock:(id)mock { 319 [QRScannerAppInterface addCameraControllerDismissalExpectations:mock]; 320 TapButton(QrScannerCloseButton()); 321 [self waitForModalOfClass:@"QRScannerViewController" 322 toDisappearFromAbove:QRScannerAppInterface.currentBrowserViewController]; 323} 324 325// Checks that the omnibox is visible and contains |text|. 326- (void)assertOmniboxIsVisibleWithText:(std::string)text { 327 [[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(text)] 328 assertWithMatcher:grey_notNil()]; 329} 330 331#pragma mark helpers for dialogs 332 333// Checks that the QRScannerViewController is presenting a UIAlertController and 334// that the title of this alert corresponds to |state|. 335- (void)assertQRScannerIsPresentingADialogForState:(CameraState)state { 336 NSError* error = [QRScannerAppInterface 337 assertModalOfClass:@"UIAlertController" 338 isPresentedBy:[QRScannerAppInterface.currentBrowserViewController 339 presentedViewController]]; 340 GREYAssertNil(error, error.localizedDescription); 341 [[EarlGrey selectElementWithMatcher:grey_text([QRScannerAppInterface 342 dialogTitleForState:state])] 343 assertWithMatcher:grey_notNil()]; 344} 345 346// Checks that there is no visible alert with title corresponding to |state|. 347- (void)assertQRScannerIsNotPresentingADialogForState:(CameraState)state { 348 [[EarlGrey selectElementWithMatcher:grey_text([QRScannerAppInterface 349 dialogTitleForState:state])] 350 assertWithMatcher:grey_nil()]; 351} 352 353#pragma mark - 354#pragma mark Helpers for mocks 355 356// Swizzles the QRScannerViewController property cameraController: to return 357// |cameraControllerMock| instead of a new instance of CameraController. 358- (void)swizzleCameraController:(id)cameraControllerMock { 359 id swizzleCameraControllerBlock = [QRScannerAppInterface 360 cameraControllerSwizzleBlockWithMock:cameraControllerMock]; 361 362 _camera_controller_swizzler = std::make_unique<EarlGreyScopedBlockSwizzler>( 363 @"QRScannerViewController", @"cameraController", 364 swizzleCameraControllerBlock); 365} 366 367// Swizzles the LocationBarCoordinator loadGURLFromLocationBarBlock:transition: 368// method to load |searchURL| instead of the generated search URL. 369- (void)swizzleLocationBarCoordinatorLoadGURLFromLocationBar: 370 (const GURL&)replacementURL { 371 NSURL* replacementNSURL = net::NSURLWithGURL(replacementURL); 372 373 id loadGURLFromLocationBarBlock = [QRScannerAppInterface 374 locationBarCoordinatorLoadGURLFromLocationBarSwizzleBlockForSearchURL: 375 replacementNSURL]; 376 _load_GURL_from_location_bar_swizzler = 377 std::make_unique<EarlGreyScopedBlockSwizzler>( 378 @"LocationBarCoordinator", 379 @"loadGURLFromLocationBar:postContent:transition:disposition:", 380 loadGURLFromLocationBarBlock); 381} 382 383// Checks that the modal presented by |viewController| is of class |klass| and 384// waits for the modal's view to load. 385- (void)waitForModalOfClass:(NSString*)klassString 386 toAppearAbove:(UIViewController*)viewController { 387 NSError* error = [QRScannerAppInterface assertModalOfClass:klassString 388 isPresentedBy:viewController]; 389 GREYAssertNil(error, error.localizedDescription); 390 UIViewController* modal = [viewController presentedViewController]; 391 GREYCondition* modalViewLoadedCondition = 392 [GREYCondition conditionWithName:@"modalViewLoadedCondition" 393 block:^BOOL { 394 return [modal isViewLoaded]; 395 }]; 396 BOOL modalViewLoaded = 397 [modalViewLoadedCondition waitWithTimeout:kGREYConditionTimeout 398 pollInterval:kGREYConditionPollInterval]; 399 NSString* errorString = [NSString 400 stringWithFormat:@"The view of a modal of class %@ should be loaded.", 401 klassString]; 402 GREYAssertTrue(modalViewLoaded, errorString); 403} 404 405// Checks that the |viewController| is not presenting a modal, or that the modal 406// presented by |viewController| is not of class |klass|. If a modal was 407// previously presented, waits until it is dismissed. 408- (void)waitForModalOfClass:(NSString*)klassString 409 toDisappearFromAbove:(UIViewController*)viewController { 410 BOOL (^waitingBlock)() = 411 [QRScannerAppInterface blockForWaitingForModalOfClass:klassString 412 toDisappearFromAbove:viewController]; 413 Class klass = NSClassFromString(klassString); 414 GREYCondition* modalViewDismissedCondition = 415 [GREYCondition conditionWithName:@"modalViewDismissedCondition" 416 block:waitingBlock]; 417 418 BOOL modalViewDismissed = 419 [modalViewDismissedCondition waitWithTimeout:kGREYConditionTimeout 420 pollInterval:kGREYConditionPollInterval]; 421 NSString* errorString = [NSString 422 stringWithFormat:@"The modal of class %@ should be loaded.", klass]; 423 GREYAssertTrue(modalViewDismissed, errorString); 424} 425 426#pragma mark - 427#pragma mark Tests 428 429// Tests that the close button, camera preview, viewport caption, and the torch 430// button are visible if the camera is available. The preview is delayed. 431- (void)testQRScannerUIIsShown { 432 id cameraControllerMock = 433 [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus: 434 AVAuthorizationStatusAuthorized]; 435 [self swizzleCameraController:cameraControllerMock]; 436 437 // Open the QR scanner. 438 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; 439 440 // Preview is loaded and camera is ready to be displayed. 441 [self assertQRScannerUIIsVisibleWithTorch:NO]; 442 443 // Close the QR scanner. 444 [self closeQRScannerWithCameraMock:cameraControllerMock]; 445 [cameraControllerMock verify]; 446} 447 448// Tests that the torch is switched on and off when pressing the torch button, 449// and that the button icon changes accordingly. 450- (void)testTurningTorchOnAndOff { 451 id cameraControllerMock = 452 [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus: 453 AVAuthorizationStatusAuthorized]; 454 [self swizzleCameraController:cameraControllerMock]; 455 456 // Open the QR scanner. 457 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; 458 459 // Torch becomes available. 460 [QRScannerAppInterface callTorchAvailabilityChanged:YES]; 461 [self assertQRScannerUIIsVisibleWithTorch:YES]; 462 463 // Turn torch on. 464 [QRScannerAppInterface 465 addCameraControllerTorchOnExpectations:cameraControllerMock]; 466 [self assertTorchOffButtonIsVisible]; 467 TapButton(QrScannerTorchOffButton()); 468 [self assertTorchOffButtonIsVisible]; 469 470 // Torch becomes active. 471 [QRScannerAppInterface callTorchStateChanged:YES]; 472 [self assertTorchOnButtonIsVisible]; 473 474 // Turn torch off. 475 [QRScannerAppInterface 476 addCameraControllerTorchOffExpectations:cameraControllerMock]; 477 TapButton(QrScannerTorchOnButton()); 478 [self assertTorchOnButtonIsVisible]; 479 480 // Torch becomes inactive. 481 [QRScannerAppInterface callTorchStateChanged:NO]; 482 [self assertTorchOffButtonIsVisible]; 483 484 // Close the QR scanner. 485 [self closeQRScannerWithCameraMock:cameraControllerMock]; 486 [cameraControllerMock verify]; 487} 488 489// Tests that if the QR scanner is closed while the torch is on, the torch is 490// switched off and the correct button indicating that the torch is off is shown 491// when the scanner is opened again. 492- (void)testTorchButtonIsResetWhenQRScannerIsReopened { 493 id cameraControllerMock = 494 [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus: 495 AVAuthorizationStatusAuthorized]; 496 [self swizzleCameraController:cameraControllerMock]; 497 498 // Open the QR scanner. 499 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; 500 [self assertQRScannerUIIsVisibleWithTorch:NO]; 501 [QRScannerAppInterface callTorchAvailabilityChanged:YES]; 502 [self assertQRScannerUIIsVisibleWithTorch:YES]; 503 504 // Turn torch on. 505 [QRScannerAppInterface 506 addCameraControllerTorchOnExpectations:cameraControllerMock]; 507 TapButton(QrScannerTorchOffButton()); 508 [QRScannerAppInterface callTorchStateChanged:YES]; 509 [self assertTorchOnButtonIsVisible]; 510 511 // Close the QR scanner. 512 [self closeQRScannerWithCameraMock:cameraControllerMock]; 513 514 // Reopen the QR scanner. 515 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; 516 [QRScannerAppInterface callTorchAvailabilityChanged:YES]; 517 [self assertTorchOffButtonIsVisible]; 518 519 // Close the QR scanner again. 520 [self closeQRScannerWithCameraMock:cameraControllerMock]; 521 [cameraControllerMock verify]; 522} 523 524// Tests that the torch button is disabled when the camera reports that torch 525// became unavailable. 526- (void)testTorchButtonIsDisabledWhenTorchBecomesUnavailable { 527 id cameraControllerMock = 528 [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus: 529 AVAuthorizationStatusAuthorized]; 530 [self swizzleCameraController:cameraControllerMock]; 531 532 // Open the QR scanner. 533 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; 534 535 // Torch becomes available. 536 [QRScannerAppInterface callTorchAvailabilityChanged:YES]; 537 [self assertQRScannerUIIsVisibleWithTorch:YES]; 538 539 // Torch becomes unavailable. 540 [QRScannerAppInterface callTorchAvailabilityChanged:NO]; 541 [self assertQRScannerUIIsVisibleWithTorch:NO]; 542 543 // Close the QR scanner. 544 [self closeQRScannerWithCameraMock:cameraControllerMock]; 545 [cameraControllerMock verify]; 546} 547 548#pragma mark dialogs 549 550// Tests that a UIAlertController is presented instead of the 551// QRScannerViewController if the camera is unavailable. 552- (void)testCameraUnavailableDialog { 553 UIViewController* bvc = QRScannerAppInterface.currentBrowserViewController; 554 NSError* error = 555 [QRScannerAppInterface assertModalOfClass:@"QRScannerViewController" 556 isNotPresentedBy:bvc]; 557 GREYAssertNil(error, error.localizedDescription); 558 error = [QRScannerAppInterface assertModalOfClass:@"UIAlertController" 559 isNotPresentedBy:bvc]; 560 GREYAssertNil(error, error.localizedDescription); 561 562 id cameraControllerMock = [QRScannerAppInterface 563 cameraControllerMockWithAuthorizationStatus:AVAuthorizationStatusDenied]; 564 [self swizzleCameraController:cameraControllerMock]; 565 566 ShowQRScanner(); 567 error = [QRScannerAppInterface assertModalOfClass:@"QRScannerViewController" 568 isNotPresentedBy:bvc]; 569 GREYAssertNil(error, error.localizedDescription); 570 571 [self waitForModalOfClass:@"UIAlertController" toAppearAbove:bvc]; 572 573 TapButton(DialogCancelButton()); 574 [self waitForModalOfClass:@"UIAlertController" toDisappearFromAbove:bvc]; 575} 576 577// Tests that a UIAlertController is presented by the QRScannerViewController if 578// the camera state changes after the QRScannerViewController is presented. 579// TODO(crbug.com/1019211): Re-enable test on iOS12. 580- (void)testDialogIsDisplayedIfCameraStateChanges { 581 id cameraControllerMock = 582 [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus: 583 AVAuthorizationStatusAuthorized]; 584 [self swizzleCameraController:cameraControllerMock]; 585 586 std::vector<CameraState> tests{scanner::MULTIPLE_FOREGROUND_APPS, 587 scanner::CAMERA_UNAVAILABLE, 588 scanner::CAMERA_PERMISSION_DENIED, 589 scanner::CAMERA_IN_USE_BY_ANOTHER_APPLICATION}; 590 591 for (const CameraState& state : tests) { 592 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; 593 [QRScannerAppInterface callCameraStateChanged:state]; 594 [self assertQRScannerIsPresentingADialogForState:state]; 595 596 // Close the dialog. 597 [QRScannerAppInterface 598 addCameraControllerDismissalExpectations:cameraControllerMock]; 599 TapButton(DialogCancelButton()); 600 UIViewController* bvc = QRScannerAppInterface.currentBrowserViewController; 601 [self waitForModalOfClass:@"QRScannerViewController" 602 toDisappearFromAbove:bvc]; 603 NSError* error = 604 [QRScannerAppInterface assertModalOfClass:@"UIAlertController" 605 isNotPresentedBy:bvc]; 606 GREYAssertNil(error, error.localizedDescription); 607 } 608 609 [cameraControllerMock verify]; 610} 611 612// Tests that a new dialog replaces an old dialog if the camera state changes. 613// TODO(crbug.com/1019211): Re-enable test on iOS12. 614- (void)testDialogIsReplacedIfCameraStateChanges { 615 id cameraControllerMock = 616 [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus: 617 AVAuthorizationStatusAuthorized]; 618 [self swizzleCameraController:cameraControllerMock]; 619 620 // Change state to CAMERA_UNAVAILABLE. 621 CameraState currentState = scanner::CAMERA_UNAVAILABLE; 622 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; 623 [QRScannerAppInterface callCameraStateChanged:currentState]; 624 [self assertQRScannerIsPresentingADialogForState:currentState]; 625 626 std::vector<CameraState> tests{scanner::CAMERA_PERMISSION_DENIED, 627 scanner::MULTIPLE_FOREGROUND_APPS, 628 scanner::CAMERA_IN_USE_BY_ANOTHER_APPLICATION, 629 scanner::CAMERA_UNAVAILABLE}; 630 631 for (const CameraState& state : tests) { 632 [QRScannerAppInterface callCameraStateChanged:state]; 633 [self assertQRScannerIsPresentingADialogForState:state]; 634 [self assertQRScannerIsNotPresentingADialogForState:currentState]; 635 currentState = state; 636 } 637 638 // Cancel the dialog. 639 [QRScannerAppInterface 640 addCameraControllerDismissalExpectations:cameraControllerMock]; 641 TapButton(DialogCancelButton()); 642 [self waitForModalOfClass:@"QRScannerViewController" 643 toDisappearFromAbove:QRScannerAppInterface.currentBrowserViewController]; 644 NSError* error = [QRScannerAppInterface 645 assertModalOfClass:@"UIAlertController" 646 isNotPresentedBy:QRScannerAppInterface.currentBrowserViewController]; 647 GREYAssertNil(error, error.localizedDescription); 648 649 [cameraControllerMock verify]; 650} 651 652// Tests that an error dialog is dismissed if the camera becomes available. 653- (void)testDialogDismissedIfCameraBecomesAvailable { 654 id cameraControllerMock = 655 [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus: 656 AVAuthorizationStatusAuthorized]; 657 [self swizzleCameraController:cameraControllerMock]; 658 659 std::vector<CameraState> tests{scanner::CAMERA_IN_USE_BY_ANOTHER_APPLICATION, 660 scanner::CAMERA_UNAVAILABLE, 661 scanner::MULTIPLE_FOREGROUND_APPS, 662 scanner::CAMERA_PERMISSION_DENIED}; 663 664 for (const CameraState& state : tests) { 665 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; 666 [QRScannerAppInterface callCameraStateChanged:state]; 667 [self assertQRScannerIsPresentingADialogForState:state]; 668 669 // Change state to CAMERA_AVAILABLE. 670 [QRScannerAppInterface callCameraStateChanged:scanner::CAMERA_AVAILABLE]; 671 [self assertQRScannerIsNotPresentingADialogForState:state]; 672 [self closeQRScannerWithCameraMock:cameraControllerMock]; 673 } 674 675 [cameraControllerMock verify]; 676} 677 678#pragma mark scanned result 679 680// A helper function for testing that the view controller correctly passes the 681// received results to its delegate and that pages can be loaded. The result 682// received from the camera controller is in |result|, |response| is the 683// expected response on the loaded page, and |editString| is a nullable string 684// which can be appended to the response in the omnibox before the page is 685// loaded. 686- (void)doTestReceivingResult:(std::string)result 687 response:(std::string)response 688 edit:(NSString*)editString { 689 id cameraControllerMock = 690 [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus: 691 AVAuthorizationStatusAuthorized]; 692 [self swizzleCameraController:cameraControllerMock]; 693 694 // Open the QR scanner. 695 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; 696 [QRScannerAppInterface callTorchAvailabilityChanged:YES]; 697 [self assertQRScannerUIIsVisibleWithTorch:YES]; 698 699 // Receive a scanned result from the camera. 700 [QRScannerAppInterface 701 addCameraControllerDismissalExpectations:cameraControllerMock]; 702 [QRScannerAppInterface 703 callReceiveQRScannerResult:base::SysUTF8ToNSString(result)]; 704 705 [self waitForModalOfClass:@"QRScannerViewController" 706 toDisappearFromAbove:QRScannerAppInterface.currentBrowserViewController]; 707 [cameraControllerMock verify]; 708 709 // Optionally edit the text in the omnibox before pressing return. 710 [self assertOmniboxIsVisibleWithText:result]; 711 if (editString != nil) { 712 EditOmniboxTextAndTapKeyboardReturn(result, editString); 713 } else { 714 TapKeyboardReturnKeyInOmniboxWithText(result); 715 } 716 [ChromeEarlGrey waitForWebStateContainingText:response]; 717 718 // Press the back button to get back to the NTP. 719 [[EarlGrey selectElementWithMatcher:chrome_test_util::BackButton()] 720 performAction:grey_tap()]; 721 NSError* error = [QRScannerAppInterface 722 assertModalOfClass:@"QRScannerViewController" 723 isNotPresentedBy:QRScannerAppInterface.currentBrowserViewController]; 724 GREYAssertNil(error, error.localizedDescription); 725} 726 727// Test that the correct page is loaded if the scanner result is a URL which is 728// then manually edited when VoiceOver is enabled. 729- (void)testReceivingQRScannerURLResultWithVoiceOver { 730 id cameraControllerMock = 731 [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus: 732 AVAuthorizationStatusAuthorized]; 733 [self swizzleCameraController:cameraControllerMock]; 734 735 // Open the QR scanner. 736 [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock]; 737 [QRScannerAppInterface callTorchAvailabilityChanged:YES]; 738 [self assertQRScannerUIIsVisibleWithTorch:YES]; 739 740 // Add override for the VoiceOver check. 741 ScopedQRScannerVoiceSearchOverride scopedOverride( 742 [QRScannerAppInterface 743 .currentBrowserViewController presentedViewController]); 744 745 // Receive a scanned result from the camera. 746 [QRScannerAppInterface 747 addCameraControllerDismissalExpectations:cameraControllerMock]; 748 [QRScannerAppInterface callReceiveQRScannerResult:base::SysUTF8ToNSString( 749 _testURL.GetContent())]; 750 751 // Fake the end of the VoiceOver announcement. 752 [QRScannerAppInterface postScanEndVoiceoverAnnouncement]; 753 754 [self waitForModalOfClass:@"QRScannerViewController" 755 toDisappearFromAbove:QRScannerAppInterface.currentBrowserViewController]; 756 [cameraControllerMock verify]; 757 758 // Optionally edit the text in the omnibox before pressing return. 759 [self assertOmniboxIsVisibleWithText:_testURL.GetContent()]; 760 TapKeyboardReturnKeyInOmniboxWithText(_testURL.GetContent()); 761 [ChromeEarlGrey waitForWebStateContainingText:kTestURLResponse]; 762} 763 764// Test that the correct page is loaded if the scanner result is a URL. 765- (void)testReceivingQRScannerURLResult { 766 [self doTestReceivingResult:_testURL.GetContent() 767 response:kTestURLResponse 768 edit:nil]; 769} 770 771// Test that the correct page is loaded if the scanner result is a URL which is 772// then manually edited. 773- (void)testReceivingQRScannerURLResultAndEditingTheURL { 774 // TODO(crbug.com/753098): Re-enable this test on iPad once grey_typeText 775 // works. 776 if ([ChromeEarlGrey isIPadIdiom]) { 777 EARL_GREY_TEST_DISABLED(@"Test disabled on iPad."); 778 } 779 780 [self doTestReceivingResult:_testURL.GetContent() 781 response:kTestURLEditedResponse 782 edit:@"\bedited/"]; 783} 784 785// Test that the correct page is loaded if the scanner result is a search query. 786- (void)testReceivingQRScannerSearchQueryResult { 787 [self swizzleLocationBarCoordinatorLoadGURLFromLocationBar:_testQuery]; 788 [self doTestReceivingResult:kTestQuery response:kTestQueryResponse edit:nil]; 789} 790 791// Test that the correct page is loaded if the scanner result is a search query 792// which is then manually edited. 793- (void)testReceivingQRScannerSearchQueryResultAndEditingTheQuery { 794 // TODO(crbug.com/753098): Re-enable this test on iPad once grey_typeText 795 // works. 796 if ([ChromeEarlGrey isIPadIdiom]) { 797 EARL_GREY_TEST_DISABLED(@"Test disabled on iPad."); 798 } 799 800 [self swizzleLocationBarCoordinatorLoadGURLFromLocationBar:_testQueryEdited]; 801 [self doTestReceivingResult:kTestQuery 802 response:kTestQueryEditedResponse 803 edit:@"\bedited"]; 804} 805 806@end 807