blob: 97e40f8867e16f391c4ae866e86ff068b4276da9 [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 <EarlGrey/EarlGrey.h>
#import <UIKit/UIKit.h>
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "components/strings/grit/components_strings.h"
#include "components/version_info/version_info.h"
#import "ios/chrome/app/main_controller.h"
#include "ios/chrome/browser/chrome_switches.h"
#import "ios/chrome/browser/ui/browser_view_controller.h"
#import "ios/chrome/browser/ui/commands/generic_chrome_command.h"
#include "ios/chrome/browser/ui/commands/ios_command_ids.h"
#include "ios/chrome/browser/ui/icons/chrome_icon.h"
#include "ios/chrome/browser/ui/qr_scanner/camera_controller.h"
#include "ios/chrome/browser/ui/qr_scanner/qr_scanner_view.h"
#include "ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller.h"
#include "ios/chrome/browser/ui/toolbar/web_toolbar_controller.h"
#include "ios/chrome/grit/ios_chromium_strings.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/app/chrome_test_util.h"
#import "ios/chrome/test/base/scoped_block_swizzler.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#include "ios/shared/chrome/browser/ui/omnibox/location_bar_delegate.h"
#import "ios/web/public/test/http_server/http_server.h"
#include "ios/web/public/test/http_server/http_server_util.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 namespace chrome_test_util;
using namespace qr_scanner;
namespace {
char kTestURL[] = "http://testurl";
char kTestURLResponse[] = "Test URL page";
char kTestQuery[] = "testquery";
char kTestQueryURL[] = "http://searchurl/testquery";
char kTestQueryResponse[] = "Test query page";
char kTestURLEdited[] = "http://testuredited";
char kTestURLEditedResponse[] = "Test URL edited page";
char kTestQueryEditedURL[] = "http://searchurl/testqueredited";
char kTestQueryEditedResponse[] = "Test query edited page";
// 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 ButtonWithAccessibilityLabel(
[[ChromeIcon closeIcon] accessibilityLabel]);
}
// 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_QR_SCANNER_TORCH_BUTTON_ACCESSIBILITY_LABEL)),
grey_accessibilityValue(l10n_util::GetNSString(
IDS_IOS_QR_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_QR_SCANNER_TORCH_BUTTON_ACCESSIBILITY_LABEL)),
grey_accessibilityValue(l10n_util::GetNSString(
IDS_IOS_QR_SCANNER_TORCH_ON_ACCESSIBILITY_VALUE)),
grey_accessibilityTrait(UIAccessibilityTraitButton), nil);
}
// Returns the GREYMatcher for the QR Scanner viewport caption.
id<GREYMatcher> QrScannerViewportCaption() {
return StaticTextWithAccessibilityLabelId(
IDS_IOS_QR_SCANNER_VIEWPORT_CAPTION);
}
// Returns the GREYMatcher for the back button in the web toolbar.
id<GREYMatcher> WebToolbarBackButton() {
return ButtonWithAccessibilityLabelId(IDS_ACCNAME_BACK);
}
// 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), nil);
}
// Opens the QR Scanner view using a command.
// TODO(crbug.com/629776): Replace the command call with a UI action.
void ShowQRScannerWithCommand() {
GenericChromeCommand* command =
[[GenericChromeCommand alloc] initWithTag:IDC_SHOW_QR_SCANNER];
chrome_test_util::RunCommandWithActiveViewController(command);
}
// 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:OmniboxText(text)]
performAction:grey_typeText([editText stringByAppendingString:@"\n"])];
}
// Presses the keyboard return key.
void TapKeyboardReturnKeyInOmniboxWithText(std::string text) {
[[EarlGrey selectElementWithMatcher:OmniboxText(text)]
performAction:grey_typeText(@"\n")];
}
} // namespace
@interface QRScannerViewControllerTestCase : ChromeTestCase {
GURL _testURL;
GURL _testURLEdited;
GURL _testQuery;
GURL _testQueryEdited;
}
@end
@implementation QRScannerViewControllerTestCase {
// A swizzler for the CameraController method cameraControllerWithDelegate:.
std::unique_ptr<ScopedBlockSwizzler> camera_controller_swizzler_;
// A swizzler for the WebToolbarController method
// loadGURLFromLocationBar:transition:.
std::unique_ptr<ScopedBlockSwizzler> load_GURL_from_location_bar_swizzler_;
}
+ (void)setUp {
[super setUp];
std::map<GURL, std::string> responses;
responses[web::test::HttpServer::MakeUrl(kTestURL)] = kTestURLResponse;
responses[web::test::HttpServer::MakeUrl(kTestQueryURL)] = kTestQueryResponse;
responses[web::test::HttpServer::MakeUrl(kTestURLEdited)] =
kTestURLEditedResponse;
responses[web::test::HttpServer::MakeUrl(kTestQueryEditedURL)] =
kTestQueryEditedResponse;
web::test::SetUpSimpleHttpServer(responses);
}
- (void)setUp {
[super setUp];
_testURL = web::test::HttpServer::MakeUrl(kTestURL);
_testURLEdited = web::test::HttpServer::MakeUrl(kTestURLEdited);
_testQuery = web::test::HttpServer::MakeUrl(kTestQueryURL);
_testQueryEdited = web::test::HttpServer::MakeUrl(kTestQueryEditedURL);
}
- (void)tearDown {
[super tearDown];
load_GURL_from_location_bar_swizzler_.reset();
camera_controller_swizzler_.reset();
}
// Checks that the close button is visible, interactable, and enabled.
- (void)assertCloseButtonIsVisible {
[[EarlGrey selectElementWithMatcher:QrScannerCloseButton()]
assertWithMatcher:VisibleInteractableEnabled()];
}
// Checks that the close button is not visible.
- (void)assertCloseButtonIsNotVisible {
[[EarlGrey selectElementWithMatcher:QrScannerCloseButton()]
assertWithMatcher:grey_notVisible()];
}
// 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 = [self currentBVC];
[self assertModalOfClass:[QRScannerViewController class]
isNotPresentedBy:bvc];
[self assertModalOfClass:[UIAlertController class] isNotPresentedBy:bvc];
[self addCameraControllerInitializationExpectations:mock];
ShowQRScannerWithCommand();
[self waitForModalOfClass:[QRScannerViewController class] toAppearAbove:bvc];
[self assertQRScannerUIIsVisibleWithTorch:NO];
[self assertModalOfClass:[UIAlertController class]
isNotPresentedBy:[bvc presentedViewController]];
[self assertModalOfClass:[UIAlertController class] isNotPresentedBy:bvc];
}
// Closes the QR scanner by tapping the close button and waits for it to
// disappear.
- (void)closeQRScannerWithCameraMock:(id)mock {
[self addCameraControllerDismissalExpectations:mock];
TapButton(QrScannerCloseButton());
[self waitForModalOfClass:[QRScannerViewController class]
toDisappearFromAbove:[self currentBVC]];
}
// Returns the current BrowserViewController.
- (UIViewController*)currentBVC {
// TODO(crbug.com/629516): Evaluate moving this into a common utility.
MainController* mainController = chrome_test_util::GetMainController();
return [[mainController browserViewInformation] currentBVC];
}
// Checks that the omnibox is visible and contains |text|.
- (void)assertOmniboxIsVisibleWithText:(std::string)text {
[[EarlGrey selectElementWithMatcher:OmniboxText(text)]
assertWithMatcher:grey_notNil()];
}
// Checks that the page that is currently loaded contains the |response|.
- (void)assertTestURLIsLoaded:(std::string)response {
id<GREYMatcher> testURLResponseMatcher =
chrome_test_util::WebViewContainingText(response);
[[EarlGrey selectElementWithMatcher:testURLResponseMatcher]
assertWithMatcher:grey_notNil()];
}
#pragma mark helpers for dialogs
// Checks that the modal presented by |viewController| is of class |klass|.
- (void)assertModalOfClass:(Class)klass
isPresentedBy:(UIViewController*)viewController {
UIViewController* modal = [viewController presentedViewController];
NSString* errorString = [NSString
stringWithFormat:@"A modal of class %@ should be presented by %@.", klass,
[viewController class]];
GREYAssertTrue(modal && [modal isKindOfClass:klass], errorString);
}
// Checks that the |viewController| is not presenting a modal, or that the modal
// presented by |viewController| is not of class |klass|.
- (void)assertModalOfClass:(Class)klass
isNotPresentedBy:(UIViewController*)viewController {
UIViewController* modal = [viewController presentedViewController];
NSString* errorString = [NSString
stringWithFormat:@"A modal of class %@ should not be presented by %@.",
klass, [viewController class]];
GREYAssertTrue(!modal || ![modal isKindOfClass:klass], errorString);
}
// Checks that the modal presented by |viewController| is of class |klass| and
// waits for the modal's view to load.
- (void)waitForModalOfClass:(Class)klass
toAppearAbove:(UIViewController*)viewController {
[self assertModalOfClass:klass isPresentedBy:viewController];
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.",
klass];
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:(Class)klass
toDisappearFromAbove:(UIViewController*)viewController {
GREYCondition* modalViewDismissedCondition = [GREYCondition
conditionWithName:@"modalViewDismissedCondition"
block:^BOOL {
UIViewController* modal =
[viewController presentedViewController];
return !modal || ![modal isKindOfClass:klass];
}];
BOOL modalViewDismissed =
[modalViewDismissedCondition waitWithTimeout:kGREYConditionTimeout
pollInterval:kGREYConditionPollInterval];
NSString* errorString = [NSString
stringWithFormat:@"The modal of class %@ should be loaded.", klass];
GREYAssertTrue(modalViewDismissed, errorString);
}
// Checks that the QRScannerViewController is presenting a UIAlertController and
// that the title of this alert corresponds to |state|.
- (void)assertQRScannerIsPresentingADialogForState:(CameraState)state {
[self assertModalOfClass:[UIAlertController class]
isPresentedBy:[[self currentBVC] presentedViewController]];
[[EarlGrey
selectElementWithMatcher:grey_text([self 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([self dialogTitleForState:state])]
assertWithMatcher:grey_nil()];
}
// Returns the expected title for the dialog which is presented for |state|.
- (NSString*)dialogTitleForState:(CameraState)state {
base::string16 appName = base::UTF8ToUTF16(version_info::GetProductName());
switch (state) {
case CAMERA_AVAILABLE:
case CAMERA_NOT_LOADED:
return nil;
case CAMERA_IN_USE_BY_ANOTHER_APPLICATION:
return l10n_util::GetNSString(
IDS_IOS_QR_SCANNER_CAMERA_IN_USE_ALERT_TITLE);
case CAMERA_PERMISSION_DENIED:
return l10n_util::GetNSString(
IDS_IOS_QR_SCANNER_CAMERA_PERMISSIONS_HELP_TITLE_GO_TO_SETTINGS);
case CAMERA_UNAVAILABLE:
return l10n_util::GetNSString(
IDS_IOS_QR_SCANNER_CAMERA_UNAVAILABLE_ALERT_TITLE);
case MULTIPLE_FOREGROUND_APPS:
return l10n_util::GetNSString(
IDS_IOS_QR_SCANNER_MULTIPLE_FOREGROUND_APPS_ALERT_TITLE);
}
}
#pragma mark -
#pragma mark Helpers for mocks
// Swizzles the CameraController method cameraControllerWithDelegate: to return
// |cameraControllerMock| instead of a new instance of CameraController.
- (void)swizzleCameraController:(id)cameraControllerMock {
CameraController* (^swizzleCameraControllerBlock)(
id<CameraControllerDelegate>) = ^(id<CameraControllerDelegate> delegate) {
return cameraControllerMock;
};
camera_controller_swizzler_.reset(new ScopedBlockSwizzler(
[CameraController class], @selector(cameraControllerWithDelegate:),
swizzleCameraControllerBlock));
}
// Swizzles the WebToolbarController loadGURLFromLocationBarBlock:transition:
// method to load |searchURL| instead of the generated search URL.
- (void)swizzleWebToolbarControllerLoadGURLFromLocationBar:
(const GURL&)searchURL {
void (^loadGURLFromLocationBarBlock)(WebToolbarController*, const GURL&,
ui::PageTransition) =
^void(WebToolbarController* self, const GURL& url,
ui::PageTransition transition) {
[self.urlLoader loadURL:searchURL
referrer:web::Referrer()
transition:transition
rendererInitiated:NO];
[self cancelOmniboxEdit];
};
load_GURL_from_location_bar_swizzler_.reset(
new ScopedBlockSwizzler([WebToolbarController class],
@selector(loadGURLFromLocationBar:transition:),
loadGURLFromLocationBarBlock));
}
// Creates a new CameraController mock with camera permission granted if
// |granted| is set to YES.
- (id)getCameraControllerMockWithAuthorizationStatus:
(AVAuthorizationStatus)authorizationStatus {
id mock = [OCMockObject mockForClass:[CameraController class]];
[[[mock stub] andReturnValue:OCMOCK_VALUE(authorizationStatus)]
getAuthorizationStatus];
return mock;
}
#pragma mark delegate calls
// Calls |cameraStateChanged:| on the presented QRScannerViewController.
- (void)callCameraStateChanged:(CameraState)state {
QRScannerViewController* vc =
(QRScannerViewController*)[[self currentBVC] presentedViewController];
[vc cameraStateChanged:state];
}
// Calls |torchStateChanged:| on the presented QRScannerViewController.
- (void)callTorchStateChanged:(BOOL)torchIsOn {
QRScannerViewController* vc =
(QRScannerViewController*)[[self currentBVC] presentedViewController];
[vc torchStateChanged:torchIsOn];
}
// Calls |torchAvailabilityChanged:| on the presented QRScannerViewController.
- (void)callTorchAvailabilityChanged:(BOOL)torchIsAvailable {
QRScannerViewController* vc =
(QRScannerViewController*)[[self currentBVC] presentedViewController];
[vc torchAvailabilityChanged:torchIsAvailable];
}
// Calls |receiveQRScannerResult:| on the presented QRScannerViewController.
- (void)callReceiveQRScannerResult:(NSString*)result {
QRScannerViewController* vc =
(QRScannerViewController*)[[self currentBVC] presentedViewController];
[vc receiveQRScannerResult:result loadImmediately:NO];
}
#pragma mark expectations
// Adds functions which are expected to be called when the
// QRScannerViewController is presented to |cameraControllerMock|.
- (void)addCameraControllerInitializationExpectations:(id)cameraControllerMock {
[[cameraControllerMock expect] setTorchMode:AVCaptureTorchModeOff];
[[cameraControllerMock expect] loadCaptureSession:[OCMArg any]];
[[cameraControllerMock expect] startRecording];
}
// Adds functions which are expected to be called when the
// QRScannerViewController is dismissed to |cameraControllerMock|.
- (void)addCameraControllerDismissalExpectations:(id)cameraControllerMock {
[[cameraControllerMock expect] setTorchMode:AVCaptureTorchModeOff];
[[cameraControllerMock expect] stopRecording];
}
// Adds functions which are expected to be called when the torch is switched on
// to |cameraControllerMock|.
- (void)addCameraControllerTorchOnExpectations:(id)cameraControllerMock {
[[[cameraControllerMock expect] andReturnValue:@NO] isTorchActive];
[[cameraControllerMock expect] setTorchMode:AVCaptureTorchModeOn];
}
// Adds functions which are expected to be called when the torch is switched off
// to |cameraControllerMock|.
- (void)addCameraControllerTorchOffExpectations:(id)cameraControllerMock {
[[[cameraControllerMock expect] andReturnValue:@YES] isTorchActive];
[[cameraControllerMock expect] setTorchMode:AVCaptureTorchModeOff];
}
#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 =
[self getCameraControllerMockWithAuthorizationStatus:
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 =
[self getCameraControllerMockWithAuthorizationStatus:
AVAuthorizationStatusAuthorized];
[self swizzleCameraController:cameraControllerMock];
// Open the QR scanner.
[self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
// Torch becomes available.
[self callTorchAvailabilityChanged:YES];
[self assertQRScannerUIIsVisibleWithTorch:YES];
// Turn torch on.
[self addCameraControllerTorchOnExpectations:cameraControllerMock];
[self assertTorchOffButtonIsVisible];
TapButton(QrScannerTorchOffButton());
[self assertTorchOffButtonIsVisible];
// Torch becomes active.
[self callTorchStateChanged:YES];
[self assertTorchOnButtonIsVisible];
// Turn torch off.
[self addCameraControllerTorchOffExpectations:cameraControllerMock];
TapButton(QrScannerTorchOnButton());
[self assertTorchOnButtonIsVisible];
// Torch becomes inactive.
[self 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 =
[self getCameraControllerMockWithAuthorizationStatus:
AVAuthorizationStatusAuthorized];
[self swizzleCameraController:cameraControllerMock];
// Open the QR scanner.
[self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
[self assertQRScannerUIIsVisibleWithTorch:NO];
[self callTorchAvailabilityChanged:YES];
[self assertQRScannerUIIsVisibleWithTorch:YES];
// Turn torch on.
[self addCameraControllerTorchOnExpectations:cameraControllerMock];
TapButton(QrScannerTorchOffButton());
[self callTorchStateChanged:YES];
[self assertTorchOnButtonIsVisible];
// Close the QR scanner.
[self closeQRScannerWithCameraMock:cameraControllerMock];
// Reopen the QR scanner.
[self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
[self 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 =
[self getCameraControllerMockWithAuthorizationStatus:
AVAuthorizationStatusAuthorized];
[self swizzleCameraController:cameraControllerMock];
// Open the QR scanner.
[self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
// Torch becomes available.
[self callTorchAvailabilityChanged:YES];
[self assertQRScannerUIIsVisibleWithTorch:YES];
// Torch becomes unavailable.
[self 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 {
// TODO(crbug.com/663026): Reenable the test for devices.
#if !TARGET_IPHONE_SIMULATOR
EARL_GREY_TEST_DISABLED(@"Disabled for devices because existing system "
@"alerts would prevent app alerts to present "
@"correctly.");
#endif
UIViewController* bvc = [self currentBVC];
[self assertModalOfClass:[QRScannerViewController class]
isNotPresentedBy:bvc];
[self assertModalOfClass:[UIAlertController class] isNotPresentedBy:bvc];
id cameraControllerMock =
[self getCameraControllerMockWithAuthorizationStatus:
AVAuthorizationStatusDenied];
[self swizzleCameraController:cameraControllerMock];
ShowQRScannerWithCommand();
[self assertModalOfClass:[QRScannerViewController class]
isNotPresentedBy:bvc];
[self waitForModalOfClass:[UIAlertController class] toAppearAbove:bvc];
TapButton(DialogCancelButton());
[self waitForModalOfClass:[UIAlertController class] toDisappearFromAbove:bvc];
}
// Tests that a UIAlertController is presented by the QRScannerViewController if
// the camera state changes after the QRScannerViewController is presented.
- (void)testDialogIsDisplayedIfCameraStateChanges {
// TODO(crbug.com/663026): Reenable the test for devices.
#if !TARGET_IPHONE_SIMULATOR
EARL_GREY_TEST_DISABLED(@"Disabled for devices because existing system "
@"alerts would prevent app alerts to present "
@"correctly.");
#endif
id cameraControllerMock =
[self getCameraControllerMockWithAuthorizationStatus:
AVAuthorizationStatusAuthorized];
[self swizzleCameraController:cameraControllerMock];
std::vector<CameraState> tests{MULTIPLE_FOREGROUND_APPS, CAMERA_UNAVAILABLE,
CAMERA_PERMISSION_DENIED,
CAMERA_IN_USE_BY_ANOTHER_APPLICATION};
for (const CameraState& state : tests) {
[self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
[self callCameraStateChanged:state];
[self assertQRScannerIsPresentingADialogForState:state];
// Close the dialog.
[self addCameraControllerDismissalExpectations:cameraControllerMock];
TapButton(DialogCancelButton());
UIViewController* bvc = [self currentBVC];
[self waitForModalOfClass:[QRScannerViewController class]
toDisappearFromAbove:bvc];
[self assertModalOfClass:[UIAlertController class] isNotPresentedBy:bvc];
}
[cameraControllerMock verify];
}
// Tests that a new dialog replaces an old dialog if the camera state changes.
- (void)testDialogIsReplacedIfCameraStateChanges {
// TODO(crbug.com/663026): Reenable the test for devices.
#if !TARGET_IPHONE_SIMULATOR
EARL_GREY_TEST_DISABLED(@"Disabled for devices because existing system "
@"alerts would prevent app alerts to present "
@"correctly.");
#endif
id cameraControllerMock =
[self getCameraControllerMockWithAuthorizationStatus:
AVAuthorizationStatusAuthorized];
[self swizzleCameraController:cameraControllerMock];
// Change state to CAMERA_UNAVAILABLE.
CameraState currentState = CAMERA_UNAVAILABLE;
[self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
[self callCameraStateChanged:currentState];
[self assertQRScannerIsPresentingADialogForState:currentState];
std::vector<CameraState> tests{
CAMERA_PERMISSION_DENIED, MULTIPLE_FOREGROUND_APPS,
CAMERA_IN_USE_BY_ANOTHER_APPLICATION, CAMERA_UNAVAILABLE};
for (const CameraState& state : tests) {
[self callCameraStateChanged:state];
[self assertQRScannerIsPresentingADialogForState:state];
[self assertQRScannerIsNotPresentingADialogForState:currentState];
currentState = state;
}
// Cancel the dialog.
[self addCameraControllerDismissalExpectations:cameraControllerMock];
TapButton(DialogCancelButton());
[self waitForModalOfClass:[QRScannerViewController class]
toDisappearFromAbove:[self currentBVC]];
[self assertModalOfClass:[UIAlertController class]
isNotPresentedBy:[self currentBVC]];
[cameraControllerMock verify];
}
// Tests that an error dialog is dismissed if the camera becomes available.
- (void)testDialogDismissedIfCameraBecomesAvailable {
id cameraControllerMock =
[self getCameraControllerMockWithAuthorizationStatus:
AVAuthorizationStatusAuthorized];
[self swizzleCameraController:cameraControllerMock];
std::vector<CameraState> tests{CAMERA_IN_USE_BY_ANOTHER_APPLICATION,
CAMERA_UNAVAILABLE, MULTIPLE_FOREGROUND_APPS,
CAMERA_PERMISSION_DENIED};
for (const CameraState& state : tests) {
[self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
[self callCameraStateChanged:state];
[self assertQRScannerIsPresentingADialogForState:state];
// Change state to CAMERA_AVAILABLE.
[self callCameraStateChanged: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
response:(std::string)response
edit:(NSString*)editString {
id cameraControllerMock =
[self getCameraControllerMockWithAuthorizationStatus:
AVAuthorizationStatusAuthorized];
[self swizzleCameraController:cameraControllerMock];
// Open the QR scanner.
[self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
[self callTorchAvailabilityChanged:YES];
[self assertQRScannerUIIsVisibleWithTorch:YES];
// Receive a scanned result from the camera.
[self addCameraControllerDismissalExpectations:cameraControllerMock];
[self callReceiveQRScannerResult:base::SysUTF8ToNSString(result)];
[self waitForModalOfClass:[QRScannerViewController class]
toDisappearFromAbove:[self currentBVC]];
[cameraControllerMock verify];
// Optionally edit the text in the omnibox before pressing return.
[self assertOmniboxIsVisibleWithText:result];
if (editString != nil) {
EditOmniboxTextAndTapKeyboardReturn(result, editString);
} else {
TapKeyboardReturnKeyInOmniboxWithText(result);
}
[self assertTestURLIsLoaded:response];
// Press the back button to get back to the NTP.
TapButton(WebToolbarBackButton());
[self assertModalOfClass:[QRScannerViewController class]
isNotPresentedBy:[self currentBVC]];
}
// 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 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:@"\b\bedited/"];
}
// Test that the correct page is loaded if the scanner result is a search query.
- (void)testReceivingQRScannerSearchQueryResult {
[self swizzleWebToolbarControllerLoadGURLFromLocationBar:_testQuery];
[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 swizzleWebToolbarControllerLoadGURLFromLocationBar:_testQueryEdited];
[self doTestReceivingResult:kTestQuery
response:kTestQueryEditedResponse
edit:@"\bedited"];
}
@end