| Index: ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller_egtest.mm
|
| diff --git a/ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller_egtest.mm b/ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller_egtest.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..f6cb15095b0c1dc5605391a5a6e18c7dd06a9a3d
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller_egtest.mm
|
| @@ -0,0 +1,834 @@
|
| +// 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>
|
| +
|
| +#import "base/mac/scoped_nsobject.h"
|
| +#include "base/strings/sys_string_conversions.h"
|
| +#include "base/test/scoped_command_line.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"
|
| +#import "ios/testing/earl_grey/disabled_test_macros.h"
|
| +#include "ios/web/public/test/http_server.h"
|
| +#include "ios/web/public/test/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"
|
| +
|
| +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() {
|
| + base::scoped_nsobject<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 scoped command line to enable the QR Scanner experiment.
|
| + std::unique_ptr<base::test::ScopedCommandLine> scoped_command_line_;
|
| + // 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);
|
| +
|
| + // Enable the QR Scanner experiment.
|
| + scoped_command_line_.reset(new base::test::ScopedCommandLine);
|
| + scoped_command_line_->GetProcessCommandLine()->AppendSwitch(
|
| + switches::kEnableQRScanner);
|
| +}
|
| +
|
| +- (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) {
|
| + // |initWithDelegate:| must return an object with a return count of 1
|
| + // because it is preceded by a call to |alloc|.
|
| + return [cameraControllerMock retain];
|
| + };
|
| +
|
| + camera_controller_swizzler_.reset(new ScopedBlockSwizzler(
|
| + [CameraController class], @selector(initWithDelegate:),
|
| + 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
|
|
|