chromium/ios/chrome/browser/qr_scanner/ui_bundled/qr_scanner_view_controller_egtest.mm

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <AVFoundation/AVFoundation.h>
#import <UIKit/UIKit.h>

#import "base/ios/ios_util.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/qr_scanner/ui_bundled/qr_scanner_app_interface.h"
#import "ios/chrome/browser/ui/scanner/camera_state.h"
#import "ios/chrome/browser/ui/settings/settings_app_interface.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/chrome/test/earl_grey/earl_grey_scoped_block_swizzler.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "net/base/apple/url_conversions.h"
#import "net/base/url_util.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "net/test/embedded_test_server/http_request.h"
#import "net/test/embedded_test_server/http_response.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"

using scanner::CameraState;

// Override a QRScannerViewController voice over check, simulating voice
// over being enabled. This doesn't reset the previous value, don't use
// nested.
class ScopedQRScannerVoiceSearchOverride {
 public:
  ScopedQRScannerVoiceSearchOverride(UIViewController* scanner_view_controller)
      : scanner_view_controller_(scanner_view_controller) {
    [QRScannerAppInterface
        overrideVoiceOverCheckForQRScannerViewController:
            scanner_view_controller_
                                                    isOn:YES];
  }

  ScopedQRScannerVoiceSearchOverride(
      const ScopedQRScannerVoiceSearchOverride&) = delete;
  ScopedQRScannerVoiceSearchOverride& operator=(
      const ScopedQRScannerVoiceSearchOverride&) = delete;

  ~ScopedQRScannerVoiceSearchOverride() {
    [QRScannerAppInterface overrideVoiceOverCheckForQRScannerViewController:
                               scanner_view_controller_
                                                                       isOn:NO];
  }

 private:
  UIViewController* scanner_view_controller_;
};

namespace {

char kTestURL[] = "/testurl";
char kTestURLResponse[] = "Test URL page";
char kTestURLEdited[] = "/testuredited";
char kTestURLEditedResponse[] = "Test URL edited page";

char kTestQuery[] = "testquery";
char kTestQueryURL[] = "/search";
char kTestQueryURLParams[] = "?q={searchTerms}";
char kTestQueryResponse[] = "Query: testquery";
char kTestQueryEditedResponse[] = "Query: testqueredited";
char kTestURLForbiddenCharacters[] = "test\u2028\u2029\u0085url";

char kTestDataURL[] = "data:dataURL";
char kTestSanitizedDataURL[] = "\"data:dataURL\"";
char kTestDataURLResponse[] = "Query: \"data:dataURL\"";

// The GREYCondition timeout used for calls to waitWithTimeout:pollInterval:.
CFTimeInterval kGREYConditionTimeout = 5;
// The GREYCondition poll interval used for calls to
// waitWithTimeout:pollInterval:.
CFTimeInterval kGREYConditionPollInterval = 0.1;

// Returns the GREYMatcher for an element which is visible, interactable, and
// enabled.
id<GREYMatcher> VisibleInteractableEnabled() {
  return grey_allOf(grey_enabled(), grey_sufficientlyVisible(), nil);
}

// Returns the GREYMatcher for the button that closes the QR Scanner.
id<GREYMatcher> QrScannerCloseButton() {
  return grey_allOf(chrome_test_util::ButtonWithAccessibilityLabel(
                        QRScannerAppInterface.closeIconAccessibilityLabel),
                    grey_userInteractionEnabled(), nil);
}

// Returns the GREYMatcher for the button which indicates that torch is off and
// which turns on the torch.
id<GREYMatcher> QrScannerTorchOffButton() {
  return grey_allOf(grey_accessibilityLabel(l10n_util::GetNSString(
                        IDS_IOS_SCANNER_TORCH_BUTTON_ACCESSIBILITY_LABEL)),
                    grey_accessibilityValue(l10n_util::GetNSString(
                        IDS_IOS_SCANNER_TORCH_OFF_ACCESSIBILITY_VALUE)),
                    grey_accessibilityTrait(UIAccessibilityTraitButton), nil);
}

// Returns the GREYMatcher for the button which indicates that torch is on and
// which turns off the torch.
id<GREYMatcher> QrScannerTorchOnButton() {
  return grey_allOf(grey_accessibilityLabel(l10n_util::GetNSString(
                        IDS_IOS_SCANNER_TORCH_BUTTON_ACCESSIBILITY_LABEL)),
                    grey_accessibilityValue(l10n_util::GetNSString(
                        IDS_IOS_SCANNER_TORCH_ON_ACCESSIBILITY_VALUE)),
                    grey_accessibilityTrait(UIAccessibilityTraitButton), nil);
}

// Returns the GREYMatcher for the QR Scanner viewport caption.
id<GREYMatcher> QrScannerViewportCaption() {
  return chrome_test_util::StaticTextWithAccessibilityLabelId(
      IDS_IOS_QR_SCANNER_VIEWPORT_CAPTION);
}

// Returns the GREYMatcher for the Cancel button to dismiss a UIAlertController.
id<GREYMatcher> DialogCancelButton() {
  return grey_allOf(
      grey_text(l10n_util::GetNSString(IDS_IOS_QR_SCANNER_ALERT_CANCEL)),
      grey_accessibilityTrait(UIAccessibilityTraitStaticText),
      grey_sufficientlyVisible(), nil);
}

// Opens the QR Scanner view.
void ShowQRScanner() {
  // Tap the omnibox to get the keyboard accessory view to show up.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::NewTabPageOmnibox()]
      performAction:grey_tap()];
  [ChromeEarlGrey
      waitForSufficientlyVisibleElementWithMatcher:chrome_test_util::Omnibox()];

  // Tap the QR Code scanner button in the keyboard accessory view.
  [[EarlGrey
      selectElementWithMatcher:grey_accessibilityLabel(@"QR code Search")]
      performAction:grey_tap()];
}

// Taps the `button`.
void TapButton(id<GREYMatcher> button) {
  [[EarlGrey selectElementWithMatcher:button] performAction:grey_tap()];
}

// Appends the given `editText` to the `text` already in the omnibox and presses
// the keyboard return key.
void EditOmniboxTextAndTapKeyboardReturn(std::string text, NSString* editText) {
  // TODO(crbug.com/40916974): Use simulatePhysicalKeyboardEvent until
  // replaceText can properly handle \n.
  [ChromeEarlGrey simulatePhysicalKeyboardEvent:editText flags:0];
  [ChromeEarlGrey simulatePhysicalKeyboardEvent:@"\n" flags:0];
}

// Presses the keyboard return key.
void TapKeyboardReturnKeyInOmniboxWithText(std::string text) {
  // TODO(crbug.com/40916974): Use simulatePhysicalKeyboardEvent until
  // replaceText can properly handle \n.
  [ChromeEarlGrey simulatePhysicalKeyboardEvent:@"\n" flags:0];
}

// Provides responses for the test page URLs.
std::unique_ptr<net::test_server::HttpResponse> StandardResponse(
    const net::test_server::HttpRequest& request) {
  std::unique_ptr<net::test_server::BasicHttpResponse> http_response =
      std::make_unique<net::test_server::BasicHttpResponse>();
  http_response->set_code(net::HTTP_OK);

  std::string body_content;
  if (base::StartsWith(request.relative_url, kTestURL,
                       base::CompareCase::SENSITIVE)) {
    body_content = kTestURLResponse;
  } else if (base::StartsWith(request.relative_url, kTestURLEdited,
                              base::CompareCase::SENSITIVE)) {
    body_content = kTestURLEditedResponse;
  } else if (base::StartsWith(request.relative_url, kTestQueryURL,
                              base::CompareCase::SENSITIVE)) {
    GURL url = request.GetURL();
    std::string query;
    bool found = net::GetValueForKeyInQuery(url, "q", &query);
    if (found) {
      body_content = "Query: " + query;
    } else {
      body_content = "No query";
    }
  } else {
    return nullptr;
  }

  http_response->set_content(
      base::StringPrintf("<html><body>%s</body></html>", body_content.c_str()));

  return std::move(http_response);
}

}  // namespace

#pragma mark - Test Case

@interface QRScannerViewControllerTestCase : ChromeTestCase {
  GURL _testURL;
  GURL _testURLEdited;
  GURL _testQuery;
}

@end

@implementation QRScannerViewControllerTestCase {
  // A swizzler for the CameraController method cameraControllerWithDelegate:.
  std::unique_ptr<EarlGreyScopedBlockSwizzler> _camera_controller_swizzler;
}

- (void)setUp {
  [super setUp];

  self.testServer->RegisterRequestHandler(
      base::BindRepeating(&StandardResponse));
  GREYAssertTrue(self.testServer->Start(), @"Server did not start.");

  _testURL = self.testServer->GetURL(kTestURL);
  _testURLEdited = self.testServer->GetURL(kTestURLEdited);
  _testQuery = self.testServer->GetURL(kTestQueryURL);

  NSString* templateURL =
      base::SysUTF8ToNSString(_testQuery.spec() + kTestQueryURLParams);
  [SettingsAppInterface overrideSearchEngineWithURL:templateURL];
}

- (void)tearDown {
  [super tearDown];
  [SettingsAppInterface resetSearchEngine];
  _camera_controller_swizzler.reset();
}

// Checks that the close button is visible, interactable, and enabled.
- (void)assertCloseButtonIsVisible {
  [[EarlGrey selectElementWithMatcher:QrScannerCloseButton()]
      assertWithMatcher:VisibleInteractableEnabled()];
}

// Checks that the torch off button is visible, interactable, and enabled, and
// that the torch on button is not.
- (void)assertTorchOffButtonIsVisible {
  [[EarlGrey selectElementWithMatcher:QrScannerTorchOffButton()]
      assertWithMatcher:VisibleInteractableEnabled()];
  [[EarlGrey selectElementWithMatcher:QrScannerTorchOnButton()]
      assertWithMatcher:grey_notVisible()];
}

// Checks that the torch on button is visible, interactable, and enabled, and
// that the torch off button is not.
- (void)assertTorchOnButtonIsVisible {
  [[EarlGrey selectElementWithMatcher:QrScannerTorchOnButton()]
      assertWithMatcher:VisibleInteractableEnabled()];
  [[EarlGrey selectElementWithMatcher:QrScannerTorchOffButton()]
      assertWithMatcher:grey_notVisible()];
}

// Checks that the torch off button is visible and disabled.
- (void)assertTorchButtonIsDisabled {
  [[EarlGrey selectElementWithMatcher:QrScannerTorchOffButton()]
      assertWithMatcher:grey_allOf(grey_not(grey_enabled()),
                                   grey_sufficientlyVisible(), nil)];
}

// Checks that the camera viewport caption is visible.
- (void)assertCameraViewportCaptionIsVisible {
  [[EarlGrey selectElementWithMatcher:QrScannerViewportCaption()]
      assertWithMatcher:grey_sufficientlyVisible()];
}

// Checks that the close button, the camera preview, and the camera viewport
// caption are visible. If `torch` is YES, checks that the torch off button is
// visible, otherwise checks that the torch button is disabled. If `preview` is
// YES, checks that the preview is visible and of the same size as the QR
// Scanner view, otherwise checks that the preview is in the view hierarchy but
// is hidden.
- (void)assertQRScannerUIIsVisibleWithTorch:(BOOL)torch {
  [self assertCloseButtonIsVisible];
  [self assertCameraViewportCaptionIsVisible];
  if (torch) {
    [self assertTorchOffButtonIsVisible];
  } else {
    [self assertTorchButtonIsDisabled];
  }
}

// Presents the QR Scanner with a command, waits for it to be displayed, and
// checks if all its views and buttons are visible. Checks that no alerts are
// presented.
- (void)showQRScannerAndCheckLayoutWithCameraMock:(id)mock {
  UIViewController* bvc = QRScannerAppInterface.currentBrowserViewController;
  NSError* error =
      [QRScannerAppInterface assertModalOfClass:@"QRScannerViewController"
                               isNotPresentedBy:bvc];
  GREYAssertNil(error, error.localizedDescription);
  error = [QRScannerAppInterface assertModalOfClass:@"UIAlertController"
                                   isNotPresentedBy:bvc];
  GREYAssertNil(error, error.localizedDescription);

  [QRScannerAppInterface addCameraControllerInitializationExpectations:mock];
  ShowQRScanner();
  [self waitForModalOfClass:@"QRScannerViewController" toAppearAbove:bvc];
  [self assertQRScannerUIIsVisibleWithTorch:NO];
  error =
      [QRScannerAppInterface assertModalOfClass:@"UIAlertController"
                               isNotPresentedBy:[bvc presentedViewController]];
  GREYAssertNil(error, error.localizedDescription);
  error = [QRScannerAppInterface assertModalOfClass:@"UIAlertController"
                                   isNotPresentedBy:bvc];
  GREYAssertNil(error, error.localizedDescription);
}

// Closes the QR scanner by tapping the close button and waits for it to
// disappear.
- (void)closeQRScannerWithCameraMock:(id)mock {
  [QRScannerAppInterface addCameraControllerDismissalExpectations:mock];
  TapButton(QrScannerCloseButton());
  [self waitForModalOfClass:@"QRScannerViewController"
       toDisappearFromAbove:QRScannerAppInterface.currentBrowserViewController];
}

// Checks that the omnibox is visible and contains `text`.
- (void)assertOmniboxIsVisibleWithText:(std::string)text {
  [[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(text)]
      assertWithMatcher:grey_notNil()];
}

#pragma mark - Helpers for Dialogs

// Checks that the QRScannerViewController is presenting a UIAlertController and
// that the title of this alert corresponds to `state`.
- (void)assertQRScannerIsPresentingADialogForState:(CameraState)state {
  NSError* error = [QRScannerAppInterface
      assertModalOfClass:@"UIAlertController"
           isPresentedBy:[QRScannerAppInterface.currentBrowserViewController
                                 presentedViewController]];
  GREYAssertNil(error, error.localizedDescription);
  [ChromeEarlGrey
      waitForUIElementToAppearWithMatcher:grey_text([QRScannerAppInterface
                                              dialogTitleForState:state])];
}

// Checks that there is no visible alert with title corresponding to `state`.
- (void)assertQRScannerIsNotPresentingADialogForState:(CameraState)state {
  [ChromeEarlGrey
      waitForUIElementToDisappearWithMatcher:grey_text([QRScannerAppInterface
                                                 dialogTitleForState:state])];
}

#pragma mark - Helpers for mocks

// Swizzles the QRScannerViewController property cameraController: to return
// `cameraControllerMock` instead of a new instance of CameraController.
- (void)swizzleCameraController:(id)cameraControllerMock {
  id swizzleCameraControllerBlock = [QRScannerAppInterface
      cameraControllerSwizzleBlockWithMock:cameraControllerMock];

  _camera_controller_swizzler = std::make_unique<EarlGreyScopedBlockSwizzler>(
      @"QRScannerViewController", @"cameraController",
      swizzleCameraControllerBlock);
}

// Checks that the modal presented by `viewController` is of class `klass` and
// waits for the modal's view to load.
- (void)waitForModalOfClass:(NSString*)klassString
              toAppearAbove:(UIViewController*)viewController {
  NSError* error = [QRScannerAppInterface assertModalOfClass:klassString
                                               isPresentedBy:viewController];
  GREYAssertNil(error, error.localizedDescription);
  UIViewController* modal = [viewController presentedViewController];
  GREYCondition* modalViewLoadedCondition =
      [GREYCondition conditionWithName:@"modalViewLoadedCondition"
                                 block:^BOOL {
                                   return [modal isViewLoaded];
                                 }];
  BOOL modalViewLoaded =
      [modalViewLoadedCondition waitWithTimeout:kGREYConditionTimeout
                                   pollInterval:kGREYConditionPollInterval];
  NSString* errorString = [NSString
      stringWithFormat:@"The view of a modal of class %@ should be loaded.",
                       klassString];
  GREYAssertTrue(modalViewLoaded, errorString);
}

// Checks that the `viewController` is not presenting a modal, or that the modal
// presented by `viewController` is not of class `klass`. If a modal was
// previously presented, waits until it is dismissed.
- (void)waitForModalOfClass:(NSString*)klassString
       toDisappearFromAbove:(UIViewController*)viewController {
  BOOL (^waitingBlock)() =
      [QRScannerAppInterface blockForWaitingForModalOfClass:klassString
                                       toDisappearFromAbove:viewController];
  Class klass = NSClassFromString(klassString);
  GREYCondition* modalViewDismissedCondition =
      [GREYCondition conditionWithName:@"modalViewDismissedCondition"
                                 block:waitingBlock];

  BOOL modalViewDismissed =
      [modalViewDismissedCondition waitWithTimeout:kGREYConditionTimeout
                                      pollInterval:kGREYConditionPollInterval];
  NSString* errorString = [NSString
      stringWithFormat:@"The modal of class %@ should be loaded.", klass];
  GREYAssertTrue(modalViewDismissed, errorString);
}

#pragma mark - Appearance Tests

// Tests that the close button, camera preview, viewport caption, and the torch
// button are visible if the camera is available. The preview is delayed.
- (void)testQRScannerUIIsShown {
  id cameraControllerMock =
      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
                                 AVAuthorizationStatusAuthorized];
  [self swizzleCameraController:cameraControllerMock];

  // Open the QR scanner.
  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];

  // Preview is loaded and camera is ready to be displayed.
  [self assertQRScannerUIIsVisibleWithTorch:NO];

  // Close the QR scanner.
  [self closeQRScannerWithCameraMock:cameraControllerMock];
  [cameraControllerMock verify];
}

// Tests that the torch is switched on and off when pressing the torch button,
// and that the button icon changes accordingly.
- (void)testTurningTorchOnAndOff {
  id cameraControllerMock =
      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
                                 AVAuthorizationStatusAuthorized];
  [self swizzleCameraController:cameraControllerMock];

  // Open the QR scanner.
  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];

  // Torch becomes available.
  [QRScannerAppInterface callTorchAvailabilityChanged:YES];
  [self assertQRScannerUIIsVisibleWithTorch:YES];

  // Turn torch on.
  [QRScannerAppInterface
      addCameraControllerTorchOnExpectations:cameraControllerMock];
  [self assertTorchOffButtonIsVisible];
  TapButton(QrScannerTorchOffButton());
  [self assertTorchOffButtonIsVisible];

  // Torch becomes active.
  [QRScannerAppInterface callTorchStateChanged:YES];
  [self assertTorchOnButtonIsVisible];

  // Turn torch off.
  [QRScannerAppInterface
      addCameraControllerTorchOffExpectations:cameraControllerMock];
  TapButton(QrScannerTorchOnButton());
  [self assertTorchOnButtonIsVisible];

  // Torch becomes inactive.
  [QRScannerAppInterface callTorchStateChanged:NO];
  [self assertTorchOffButtonIsVisible];

  // Close the QR scanner.
  [self closeQRScannerWithCameraMock:cameraControllerMock];
  [cameraControllerMock verify];
}

// Tests that if the QR scanner is closed while the torch is on, the torch is
// switched off and the correct button indicating that the torch is off is shown
// when the scanner is opened again.
- (void)testTorchButtonIsResetWhenQRScannerIsReopened {
  id cameraControllerMock =
      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
                                 AVAuthorizationStatusAuthorized];
  [self swizzleCameraController:cameraControllerMock];

  // Open the QR scanner.
  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
  [self assertQRScannerUIIsVisibleWithTorch:NO];
  [QRScannerAppInterface callTorchAvailabilityChanged:YES];
  [self assertQRScannerUIIsVisibleWithTorch:YES];

  // Turn torch on.
  [QRScannerAppInterface
      addCameraControllerTorchOnExpectations:cameraControllerMock];
  TapButton(QrScannerTorchOffButton());
  [QRScannerAppInterface callTorchStateChanged:YES];
  [self assertTorchOnButtonIsVisible];

  // Close the QR scanner.
  [self closeQRScannerWithCameraMock:cameraControllerMock];

  // Reopen the QR scanner.
  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
  [QRScannerAppInterface callTorchAvailabilityChanged:YES];
  [self assertTorchOffButtonIsVisible];

  // Close the QR scanner again.
  [self closeQRScannerWithCameraMock:cameraControllerMock];
  [cameraControllerMock verify];
}

// Tests that the torch button is disabled when the camera reports that torch
// became unavailable.
- (void)testTorchButtonIsDisabledWhenTorchBecomesUnavailable {
  id cameraControllerMock =
      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
                                 AVAuthorizationStatusAuthorized];
  [self swizzleCameraController:cameraControllerMock];

  // Open the QR scanner.
  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];

  // Torch becomes available.
  [QRScannerAppInterface callTorchAvailabilityChanged:YES];
  [self assertQRScannerUIIsVisibleWithTorch:YES];

  // Torch becomes unavailable.
  [QRScannerAppInterface callTorchAvailabilityChanged:NO];
  [self assertQRScannerUIIsVisibleWithTorch:NO];

  // Close the QR scanner.
  [self closeQRScannerWithCameraMock:cameraControllerMock];
  [cameraControllerMock verify];
}

#pragma mark - Dialogs Tests

// Tests that a UIAlertController is presented instead of the
// QRScannerViewController if the camera is unavailable.
- (void)testCameraUnavailableDialog {
  UIViewController* bvc = QRScannerAppInterface.currentBrowserViewController;
  NSError* error =
      [QRScannerAppInterface assertModalOfClass:@"QRScannerViewController"
                               isNotPresentedBy:bvc];
  GREYAssertNil(error, error.localizedDescription);
  error = [QRScannerAppInterface assertModalOfClass:@"UIAlertController"
                                   isNotPresentedBy:bvc];
  GREYAssertNil(error, error.localizedDescription);

  id cameraControllerMock = [QRScannerAppInterface
      cameraControllerMockWithAuthorizationStatus:AVAuthorizationStatusDenied];
  [self swizzleCameraController:cameraControllerMock];

  ShowQRScanner();
  error = [QRScannerAppInterface assertModalOfClass:@"QRScannerViewController"
                                   isNotPresentedBy:bvc];
  GREYAssertNil(error, error.localizedDescription);

  [self waitForModalOfClass:@"UIAlertController" toAppearAbove:bvc];

  TapButton(DialogCancelButton());
  [self waitForModalOfClass:@"UIAlertController" toDisappearFromAbove:bvc];
}

// Tests that a UIAlertController is presented by the QRScannerViewController if
// the camera state changes after the QRScannerViewController is presented.
- (void)testDialogIsDisplayedIfCameraStateChanges {
  id cameraControllerMock =
      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
                                 AVAuthorizationStatusAuthorized];
  [self swizzleCameraController:cameraControllerMock];

  std::vector<CameraState> tests{scanner::MULTIPLE_FOREGROUND_APPS,
                                 scanner::CAMERA_UNAVAILABLE,
                                 scanner::CAMERA_PERMISSION_DENIED,
                                 scanner::CAMERA_IN_USE_BY_ANOTHER_APPLICATION};

  for (const CameraState& state : tests) {
    [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
    [QRScannerAppInterface callCameraStateChanged:state];
    [self assertQRScannerIsPresentingADialogForState:state];

    // Close the dialog.
    [QRScannerAppInterface
        addCameraControllerDismissalExpectations:cameraControllerMock];
    TapButton(DialogCancelButton());
    UIViewController* bvc = QRScannerAppInterface.currentBrowserViewController;
    [self waitForModalOfClass:@"QRScannerViewController"
         toDisappearFromAbove:bvc];
    NSError* error =
        [QRScannerAppInterface assertModalOfClass:@"UIAlertController"
                                 isNotPresentedBy:bvc];
    GREYAssertNil(error, error.localizedDescription);
  }

  [cameraControllerMock verify];
}

// Tests that a new dialog replaces an old dialog if the camera state changes.
// TODO(crbug.com/40105250): Re-enable test on iOS12.
#if TARGET_IPHONE_SIMULATOR
#define MAYBE_testDialogIsReplacedIfCameraStateChanges \
  testDialogIsReplacedIfCameraStateChanges
#else
#define MAYBE_testDialogIsReplacedIfCameraStateChanges \
  DISABLED_testDialogIsReplacedIfCameraStateChanges
#endif
- (void)MAYBE_testDialogIsReplacedIfCameraStateChanges {
  id cameraControllerMock =
      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
                                 AVAuthorizationStatusAuthorized];
  [self swizzleCameraController:cameraControllerMock];

  // Change state to CAMERA_UNAVAILABLE.
  CameraState currentState = scanner::CAMERA_UNAVAILABLE;
  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
  [QRScannerAppInterface callCameraStateChanged:currentState];
  [self assertQRScannerIsPresentingADialogForState:currentState];

  std::vector<CameraState> tests{scanner::CAMERA_PERMISSION_DENIED,
                                 scanner::MULTIPLE_FOREGROUND_APPS,
                                 scanner::CAMERA_IN_USE_BY_ANOTHER_APPLICATION,
                                 scanner::CAMERA_UNAVAILABLE};

  for (const CameraState& state : tests) {
    [QRScannerAppInterface callCameraStateChanged:state];
    [self assertQRScannerIsPresentingADialogForState:state];
    [self assertQRScannerIsNotPresentingADialogForState:currentState];
    currentState = state;
  }

  // Cancel the dialog.
  [QRScannerAppInterface
      addCameraControllerDismissalExpectations:cameraControllerMock];
  TapButton(DialogCancelButton());
  [self waitForModalOfClass:@"QRScannerViewController"
       toDisappearFromAbove:QRScannerAppInterface.currentBrowserViewController];
  NSError* error = [QRScannerAppInterface
      assertModalOfClass:@"UIAlertController"
        isNotPresentedBy:QRScannerAppInterface.currentBrowserViewController];
  GREYAssertNil(error, error.localizedDescription);

  [cameraControllerMock verify];
}

// Tests that an error dialog is dismissed if the camera becomes available.
- (void)testDialogDismissedIfCameraBecomesAvailable {
  id cameraControllerMock =
      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
                                 AVAuthorizationStatusAuthorized];
  [self swizzleCameraController:cameraControllerMock];

  std::vector<CameraState> tests{scanner::CAMERA_IN_USE_BY_ANOTHER_APPLICATION,
                                 scanner::CAMERA_UNAVAILABLE,
                                 scanner::MULTIPLE_FOREGROUND_APPS,
                                 scanner::CAMERA_PERMISSION_DENIED};

  for (const CameraState& state : tests) {
    [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
    [QRScannerAppInterface callCameraStateChanged:state];
    [self assertQRScannerIsPresentingADialogForState:state];

    // Change state to CAMERA_AVAILABLE.
    [QRScannerAppInterface callCameraStateChanged:scanner::CAMERA_AVAILABLE];
    [self assertQRScannerIsNotPresentingADialogForState:state];
    [self closeQRScannerWithCameraMock:cameraControllerMock];
  }

  [cameraControllerMock verify];
}

#pragma mark - Scanned Result Tests

// A helper function for testing that the view controller correctly passes the
// received results to its delegate and that pages can be loaded. The result
// received from the camera controller is in `result`, `response` is the
// expected response on the loaded page, and `editString` is a nullable string
// which can be appended to the response in the omnibox before the page is
// loaded.
- (void)doTestReceivingResult:(std::string)result
              sanitizedResult:(std::string)sanitizedResult
                     response:(std::string)response
                         edit:(NSString*)editString {
  id cameraControllerMock =
      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
                                 AVAuthorizationStatusAuthorized];
  [self swizzleCameraController:cameraControllerMock];

  // Open the QR scanner.
  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
  [QRScannerAppInterface callTorchAvailabilityChanged:YES];
  [self assertQRScannerUIIsVisibleWithTorch:YES];

  // Receive a scanned result from the camera.
  [QRScannerAppInterface
      addCameraControllerDismissalExpectations:cameraControllerMock];
  [QRScannerAppInterface
      callReceiveQRScannerResult:base::SysUTF8ToNSString(result)];

  [self waitForModalOfClass:@"QRScannerViewController"
       toDisappearFromAbove:QRScannerAppInterface.currentBrowserViewController];
  [cameraControllerMock verify];

  // Optionally edit the text in the omnibox before pressing return.
  [self assertOmniboxIsVisibleWithText:sanitizedResult];
  if (editString != nil) {
    EditOmniboxTextAndTapKeyboardReturn(sanitizedResult, editString);
  } else {
    TapKeyboardReturnKeyInOmniboxWithText(sanitizedResult);
  }
  [ChromeEarlGrey waitForWebStateContainingText:response];

  // Press the back button to get back to the NTP.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::BackButton()]
      performAction:grey_tap()];
  NSError* error = [QRScannerAppInterface
      assertModalOfClass:@"QRScannerViewController"
        isNotPresentedBy:QRScannerAppInterface.currentBrowserViewController];
  GREYAssertNil(error, error.localizedDescription);
}

- (void)doTestReceivingResult:(std::string)result
                     response:(std::string)response
                         edit:(NSString*)editString {
  [self doTestReceivingResult:result
              sanitizedResult:result
                     response:response
                         edit:editString];
}

// Test that the correct page is loaded if the scanner result is a URL which is
// then manually edited when VoiceOver is enabled.
- (void)testReceivingQRScannerURLResultWithVoiceOver {
  id cameraControllerMock =
      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
                                 AVAuthorizationStatusAuthorized];
  [self swizzleCameraController:cameraControllerMock];

  // Open the QR scanner.
  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
  [QRScannerAppInterface callTorchAvailabilityChanged:YES];
  [self assertQRScannerUIIsVisibleWithTorch:YES];

  // Add override for the VoiceOver check.
  ScopedQRScannerVoiceSearchOverride scopedOverride(
      [QRScannerAppInterface
              .currentBrowserViewController presentedViewController]);

  // Receive a scanned result from the camera.
  [QRScannerAppInterface
      addCameraControllerDismissalExpectations:cameraControllerMock];
  [QRScannerAppInterface callReceiveQRScannerResult:base::SysUTF8ToNSString(
                                                        _testURL.GetContent())];

  // Fake the end of the VoiceOver announcement.
  [QRScannerAppInterface postScanEndVoiceoverAnnouncement];

  [self waitForModalOfClass:@"QRScannerViewController"
       toDisappearFromAbove:QRScannerAppInterface.currentBrowserViewController];
  [cameraControllerMock verify];

  // Optionally edit the text in the omnibox before pressing return.
  [self assertOmniboxIsVisibleWithText:_testURL.GetContent()];
  TapKeyboardReturnKeyInOmniboxWithText(_testURL.GetContent());
  [ChromeEarlGrey waitForWebStateContainingText:kTestURLResponse];
}

// Test that the correct page is loaded if the scanner result is a URL.
- (void)testReceivingQRScannerURLResult {
  [self doTestReceivingResult:_testURL.GetContent()
                     response:kTestURLResponse
                         edit:nil];
}

// Test that the URL is sanitized and the correct page is loaded if the scanner
// result is a URL with forbidden characters.
- (void)testForbiddenCharactersRemoved {
  [self doTestReceivingResult:self.testServer->base_url().GetContent() +
                              kTestURLForbiddenCharacters
              sanitizedResult:_testURL.GetContent()
                     response:kTestURLResponse
                         edit:nil];
}

// Test that the correct page is loaded if the scanner result is a URL which is
// then manually edited.
- (void)testReceivingQRScannerURLResultAndEditingTheURL {
  [self doTestReceivingResult:_testURL.GetContent()
                     response:kTestURLEditedResponse
                         edit:@"\bedited/"];
}

// Test that the correct page is loaded if the scanner result is a search query.
- (void)testReceivingQRScannerSearchQueryResult {
  [self doTestReceivingResult:kTestQuery response:kTestQueryResponse edit:nil];
}

// Test that the correct page is loaded if the scanner result is a search query
// which is then manually edited.
- (void)testReceivingQRScannerSearchQueryResultAndEditingTheQuery {
  [self doTestReceivingResult:kTestQuery
                     response:kTestQueryEditedResponse
                         edit:@"\bedited"];
}

// Test that the correct page is loaded if the scanner result is a not supported
// URL.
- (void)testReceivingQRScannerLoadDataResult {
  [self doTestReceivingResult:kTestDataURL
              sanitizedResult:kTestSanitizedDataURL
                     response:kTestDataURLResponse
                         edit:nil];
}

@end