blob: b42a94beeb454bce7ede5c460633654e82b8ecf0 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// 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>
#include "base/ios/ios_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/ui/qr_scanner/qr_scanner_app_interface.h"
#include "ios/chrome/browser/ui/scanner/camera_state.h"
#include "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"
#include "ios/chrome/test/earl_grey/earl_grey_scoped_block_swizzler.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "net/base/mac/url_conversions.h"
#include "net/base/url_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "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"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
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() {
[QRScannerAppInterface overrideVoiceOverCheckForQRScannerViewController:
scanner_view_controller_
isOn:NO];
}
private:
UIViewController* scanner_view_controller_;
DISALLOW_COPY_AND_ASSIGN(ScopedQRScannerVoiceSearchOverride);
};
// TODO(crbug.com/1015113) The EG2 macro is breaking indexing for some reason
// without the trailing semicolon. For now, disable the extra semi warning
// so Xcode indexing works for the egtest.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wc++98-compat-extra-semi"
GREY_STUB_CLASS_IN_APP_MAIN_QUEUE(QRScannerAppInterface);
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_sufficientlyVisible(), grey_interactable(),
grey_enabled(), 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) {
[[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(text)]
performAction:grey_typeText([editText stringByAppendingString:@"\n"])];
}
// Presses the keyboard return key.
void TapKeyboardReturnKeyInOmniboxWithText(std::string text) {
[[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(text)]
performAction:grey_typeText(@"\n")];
}
// 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);
const char* body_content = nullptr;
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) {
std::string content = "Query: " + query;
body_content = content.c_str();
} else {
body_content = "No query";
}
} else {
return nullptr;
}
if (body_content) {
http_response->set_content(
base::StringPrintf("<html><body>%s</body></html>", body_content));
}
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);
[QRScannerAppInterface overrideSearchEngine:templateURL];
}
- (void)tearDown {
[super tearDown];
[QRScannerAppInterface 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_sufficientlyVisible(),
grey_not(grey_enabled()), 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);
[[EarlGrey selectElementWithMatcher:grey_text([QRScannerAppInterface
dialogTitleForState:state])]
assertWithMatcher:grey_notNil()];
}
// Checks that there is no visible alert with title corresponding to |state|.
- (void)assertQRScannerIsNotPresentingADialogForState:(CameraState)state {
[[EarlGrey selectElementWithMatcher:grey_text([QRScannerAppInterface
dialogTitleForState:state])]
assertWithMatcher:grey_nil()];
}
#pragma mark -
#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 -
#pragma mark 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 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.
// TODO(crbug.com/1019211): Re-enable test on iOS12.
- (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/1019211): Re-enable test on iOS12.
- (void)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
// 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 {
// TODO(crbug.com/753098): Re-enable this test on iPad once grey_typeText
// works.
if ([ChromeEarlGrey isIPadIdiom]) {
EARL_GREY_TEST_DISABLED(@"Test disabled on iPad.");
}
[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 {
// TODO(crbug.com/753098): Re-enable this test on iPad once grey_typeText
// works.
if ([ChromeEarlGrey isIPadIdiom]) {
EARL_GREY_TEST_DISABLED(@"Test disabled on iPad.");
}
[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