Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(87)

Unified Diff: ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller_egtest.mm

Issue 2589583003: Upstream Chrome on iOS source code [7/11]. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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

Powered by Google App Engine
This is Rietveld 408576698