Commit 87367777 authored by Robbie Gibson's avatar Robbie Gibson Committed by Commit Bot

[iOS] Convert QRScannerViewController EG test to EG2 test

This conversion has a very large app_interface. This is mostly due to
the heavy use of mocking and swizzling these tests use to interface
with the iOS camera status.

This also moves the CameraState enum into a separate file and gn
target so it can be included in test-side processes.

Bug: 987646
Change-Id: I5755ee1861de3ba74b9594e2c528d234b85feca8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1879249
Commit-Queue: Robbie Gibson <rkgibson@google.com>
Reviewed-by: default avatarStepan Khapugin <stkhapugin@chromium.org>
Reviewed-by: default avatarEugene But <eugenebut@chromium.org>
Cr-Commit-Position: refs/heads/master@{#709876}
parent c681ff32
......@@ -50,40 +50,130 @@ source_set("coordinator") {
source_set("eg_tests") {
configs += [ "//build/config/compiler:enable_arc" ]
defines = [ "CHROME_EARL_GREY_1" ]
testonly = true
sources = [
"qr_scanner_view_controller_egtest.mm",
]
deps = [
":test_support",
"//base",
"//ios/chrome/app/strings",
"//ios/chrome/browser/ui/scanner:camera_state",
"//ios/chrome/test/earl_grey:test_support",
"//ios/testing/earl_grey:earl_grey_support",
"//ios/third_party/earl_grey:earl_grey+link",
"//ios/web/public/test/http_server",
"//net",
"//third_party/ocmock",
"//ui/base",
]
libs = [
"AVFoundation.framework",
"UIKit.framework",
]
}
source_set("eg2_tests") {
defines = [ "CHROME_EARL_GREY_2" ]
configs += [
"//build/config/compiler:enable_arc",
"//build/config/ios:xctest_config",
]
testonly = true
sources = [
"qr_scanner_view_controller_egtest.mm",
]
deps = [
":eg_test_support+eg2",
"//base",
"//ios/chrome/app/strings",
"//ios/chrome/browser/ui/scanner:camera_state",
"//ios/chrome/test/earl_grey:eg_test_support+eg2",
"//ios/testing/earl_grey:eg_test_support+eg2",
"//ios/third_party/earl_grey2:test_lib",
"//ios/web/public/test/http_server",
"//net",
"//third_party/ocmock",
"//ui/base",
]
libs = [
"AVFoundation.framework",
"UIKit.framework",
]
}
source_set("test_support") {
defines = [ "CHROME_EARL_GREY_1" ]
configs += [ "//build/config/compiler:enable_arc" ]
testonly = true
sources = [
"qr_scanner_app_interface.h",
"qr_scanner_app_interface.mm",
]
deps = [
":qr_scanner",
"//base",
"//base/test:test_support",
"//components/strings",
"//components/version_info",
"//ios/chrome/app:app_internal",
"//ios/chrome/app/strings",
"//ios/chrome/browser",
"//ios/chrome/browser/main",
"//ios/chrome/browser/ui/commands",
"//ios/chrome/browser/ui/icons",
"//ios/chrome/browser/ui/location_bar",
"//ios/chrome/browser/ui/omnibox",
"//ios/chrome/browser/ui/scanner",
"//ios/chrome/browser/ui/toolbar",
"//ios/chrome/browser/ui/toolbar/public:feature_flags",
"//ios/chrome/browser/ui/util",
"//ios/chrome/browser/ui/scanner:camera_state",
"//ios/chrome/browser/url_loading",
"//ios/chrome/test/app:test_support",
"//ios/chrome/test/base",
"//ios/chrome/test/earl_grey:test_support",
"//ios/testing/earl_grey:earl_grey_support",
"//ios/third_party/earl_grey:earl_grey+link",
"//ios/web/public/test/http_server",
"//ios/testing:nserror_support",
"//net",
"//third_party/ocmock",
"//ui/base",
]
libs = [
"AVFoundation.framework",
"UIKit.framework",
}
source_set("eg_app_support+eg2") {
defines = [ "CHROME_EARL_GREY_2" ]
configs += [
"//build/config/compiler:enable_arc",
"//build/config/ios:xctest_config",
]
testonly = true
sources = [
"qr_scanner_app_interface.h",
"qr_scanner_app_interface.mm",
]
deps = [
":qr_scanner",
"//base",
"//components/version_info",
"//ios/chrome/app:app_internal",
"//ios/chrome/app/strings",
"//ios/chrome/browser/main",
"//ios/chrome/browser/ui/icons",
"//ios/chrome/browser/ui/location_bar",
"//ios/chrome/browser/ui/scanner",
"//ios/chrome/browser/ui/scanner:camera_state",
"//ios/chrome/browser/url_loading",
"//ios/chrome/test/app:test_support",
"//ios/testing:nserror_support",
"//net",
"//third_party/ocmock",
"//ui/base",
]
}
source_set("eg_test_support+eg2") {
defines = [ "CHROME_EARL_GREY_2" ]
configs += [
"//build/config/compiler:enable_arc",
"//build/config/ios:xctest_config",
]
testonly = true
sources = [
"qr_scanner_app_interface.h",
]
deps = [
"//ios/chrome/browser/ui/scanner:camera_state",
]
}
// Copyright 2019 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.
#ifndef IOS_CHROME_BROWSER_UI_QR_SCANNER_QR_SCANNER_APP_INTERFACE_H_
#define IOS_CHROME_BROWSER_UI_QR_SCANNER_QR_SCANNER_APP_INTERFACE_H_
#import <AVFoundation/AVFoundation.h>
#import <UIKit/UIKit.h>
#include "ios/chrome/browser/ui/scanner/camera_state.h"
@interface QRScannerAppInterface : NSObject
// Returns the current BrowserViewController. This is used throughout the
// tests to get the currently-presented modal view controller (usually the
// QRScannerViewController).
@property(nonatomic, class, readonly)
UIViewController* currentBrowserViewController;
// Returns the image used for the close icon.
@property(nonatomic, class, readonly) UIImage* closeIcon;
#pragma mark Swizzling
// Returns the block to use for swizzling the QRScannerViewController property
// cameraController: to return |cameraControllerMock| instead of a new instance
// of CameraController.
// This block is only used for swizzling, which is why its type is opaque. It
// should not be called directly.
+ (id)cameraControllerSwizzleBlockWithMock:(id)cameraControllerMock;
// Returns the block to use for swizzling the LocationBarCoordinator
// loadGURLFromLocationBarBlock:transition: method to load |searchURL| instead
// of the generated search URL.
// This block is only used for swizzling, which is why its type is opaque. It
// should not be called directly.
+ (id)locationBarCoordinatorLoadGURLFromLocationBarSwizzleBlockForSearchURL:
(NSURL*)searchURL;
#pragma mark Mocking and Expectations
// Creates a new CameraController mock with |authorizationStatus| set.
+ (id)cameraControllerMockWithAuthorizationStatus:
(AVAuthorizationStatus)authorizationStatus;
// Adds functions which are expected to be called when the
// QRScannerViewController is presented to |cameraControllerMock|.
+ (void)addCameraControllerInitializationExpectations:(id)cameraControllerMock;
// Adds functions which are expected to be called when the
// QRScannerViewController is dismissed to |cameraControllerMock|.
+ (void)addCameraControllerDismissalExpectations:(id)cameraControllerMock;
// Adds functions which are expected to be called when the torch is switched on
// to |cameraControllerMock|.
+ (void)addCameraControllerTorchOnExpectations:(id)cameraControllerMock;
// Adds functions which are expected to be called when the torch is switched off
// to |cameraControllerMock|.
+ (void)addCameraControllerTorchOffExpectations:(id)cameraControllerMock;
#pragma mark CameraControllerDelegate calls
// Calls |cameraStateChanged:| on the presented QRScannerViewController.
+ (void)callCameraStateChanged:(scanner::CameraState)state;
// Calls |torchStateChanged:| on the presented QRScannerViewController.
+ (void)callTorchStateChanged:(BOOL)torchIsOn;
// Calls |torchAvailabilityChanged:| on the presented QRScannerViewController.
+ (void)callTorchAvailabilityChanged:(BOOL)torchIsAvailable;
// Calls |receiveQRScannerResult:| on the presented QRScannerViewController.
+ (void)callReceiveQRScannerResult:(NSString*)result;
#pragma mark Modal helpers for dialogs
// Checks that the modal presented by |viewController| is of class
// |className|. Returns nil if the check passes and an NSError otherwise.
+ (NSError*)assertModalOfClass:(NSString*)className
isPresentedBy:(UIViewController*)viewController
__attribute__((warn_unused_result));
// Checks that the |viewController| is not presenting a modal, or that the modal
// presented by |viewController| is not of class |className|. Returns nil if
// the check passes and an NSError otherwise.
+ (NSError*)assertModalOfClass:(NSString*)className
isNotPresentedBy:(UIViewController*)viewController
__attribute__((warn_unused_result));
// Returns a block that checks that the |viewController| is not presenting a
// modal, or that the modal presented by |viewController| is not of class
// |className|. This block can be waited on in the test process.
+ (BOOL (^)())blockForWaitingForModalOfClass:(NSString*)className
toDisappearFromAbove:(UIViewController*)viewController;
// Returns the expected title for the dialog which is presented for |state|.
+ (NSString*)dialogTitleForState:(scanner::CameraState)state;
#pragma mark VoiceOver overrides
// Overrides the VoiceOver check for |qrScanner| to |isOn|. |qrScanner| should
// be an instance of QRScannerViewController. Calls to this method should also
// be paired, so the VoiceOver status is left in the same state as it started.
+ (void)overrideVoiceOverCheckForQRScannerViewController:
(UIViewController*)qrScanner
isOn:(BOOL)isOn;
// Posts a fake VoiceOver end announcement.
+ (void)postScanEndVoiceoverAnnouncement;
@end
#endif // IOS_CHROME_BROWSER_UI_QR_SCANNER_QR_SCANNER_APP_INTERFACE_H_
// Copyright 2019 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 "ios/chrome/browser/ui/qr_scanner/qr_scanner_app_interface.h"
#include "base/mac/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "components/version_info/version_info.h"
#import "ios/chrome/app/main_controller.h"
#include "ios/chrome/browser/main/browser.h"
#include "ios/chrome/browser/ui/icons/chrome_icon.h"
#import "ios/chrome/browser/ui/location_bar/location_bar_coordinator.h"
#import "ios/chrome/browser/ui/location_bar/location_bar_url_loader.h"
#include "ios/chrome/browser/ui/qr_scanner/qr_scanner_camera_controller.h"
#include "ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller.h"
#include "ios/chrome/browser/ui/scanner/camera_controller.h"
#import "ios/chrome/browser/url_loading/url_loading_params.h"
#import "ios/chrome/browser/url_loading/url_loading_service.h"
#import "ios/chrome/browser/url_loading/url_loading_service_factory.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/testing/nserror_util.h"
#import "net/base/mac/url_conversions.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using scanner::CameraState;
@implementation QRScannerAppInterface
+ (UIViewController*)currentBrowserViewController {
MainController* mainController = chrome_test_util::GetMainController();
return mainController.interfaceProvider.mainInterface.viewController;
}
+ (UIImage*)closeIcon {
return [ChromeIcon closeIcon];
}
#pragma mark Swizzling
+ (id)cameraControllerSwizzleBlockWithMock:(id)cameraControllerMock {
QRScannerCameraController* (^swizzleCameraControllerBlock)(
id<QRScannerCameraControllerDelegate>) =
^(id<QRScannerCameraControllerDelegate> delegate) {
return cameraControllerMock;
};
return swizzleCameraControllerBlock;
}
+ (id)locationBarCoordinatorLoadGURLFromLocationBarSwizzleBlockForSearchURL:
(NSURL*)searchURL {
GURL searchGURL = net::GURLWithNSURL(searchURL);
void (^loadGURLFromLocationBarBlock)(LocationBarCoordinator*,
TemplateURLRef::PostContent*,
const GURL&, ui::PageTransition) =
^void(LocationBarCoordinator* self,
TemplateURLRef::PostContent* postContent, const GURL& url,
ui::PageTransition transition) {
web::NavigationManager::WebLoadParams params(searchGURL);
params.transition_type = transition;
UrlLoadingServiceFactory::GetForBrowserState(
self.browser->GetBrowserState())
->Load(UrlLoadParams::InCurrentTab(params));
[self cancelOmniboxEdit];
};
return loadGURLFromLocationBarBlock;
}
#pragma mark Mocking and Expectations
+ (id)cameraControllerMockWithAuthorizationStatus:
(AVAuthorizationStatus)authorizationStatus {
id mock = [OCMockObject mockForClass:[QRScannerCameraController class]];
[[[mock stub] andReturnValue:OCMOCK_VALUE(authorizationStatus)]
getAuthorizationStatus];
return mock;
}
+ (void)addCameraControllerInitializationExpectations:(id)cameraControllerMock {
[[cameraControllerMock expect] setTorchMode:AVCaptureTorchModeOff];
[[cameraControllerMock expect] loadCaptureSession:[OCMArg any]];
[[cameraControllerMock expect] startRecording];
}
+ (void)addCameraControllerDismissalExpectations:(id)cameraControllerMock {
[[cameraControllerMock expect] setTorchMode:AVCaptureTorchModeOff];
[[cameraControllerMock expect] stopRecording];
}
+ (void)addCameraControllerTorchOnExpectations:(id)cameraControllerMock {
[[[cameraControllerMock expect] andReturnValue:@NO] isTorchActive];
[[cameraControllerMock expect] setTorchMode:AVCaptureTorchModeOn];
}
+ (void)addCameraControllerTorchOffExpectations:(id)cameraControllerMock {
[[[cameraControllerMock expect] andReturnValue:@YES] isTorchActive];
[[cameraControllerMock expect] setTorchMode:AVCaptureTorchModeOff];
}
#pragma mark CameraControllerDelegate calls
+ (void)callCameraStateChanged:(CameraState)state {
QRScannerViewController* vc = (QRScannerViewController*)
[self.currentBrowserViewController presentedViewController];
[vc cameraStateChanged:state];
}
+ (void)callTorchStateChanged:(BOOL)torchIsOn {
QRScannerViewController* vc = (QRScannerViewController*)
[self.currentBrowserViewController presentedViewController];
[vc torchStateChanged:torchIsOn];
}
+ (void)callTorchAvailabilityChanged:(BOOL)torchIsAvailable {
QRScannerViewController* vc = (QRScannerViewController*)
[self.currentBrowserViewController presentedViewController];
[vc torchAvailabilityChanged:torchIsAvailable];
}
+ (void)callReceiveQRScannerResult:(NSString*)result {
QRScannerViewController* vc = (QRScannerViewController*)
[self.currentBrowserViewController presentedViewController];
[vc receiveQRScannerResult:result loadImmediately:NO];
}
#pragma mark Modal helpers for dialogs
+ (NSError*)assertModalOfClass:(NSString*)className
isPresentedBy:(UIViewController*)viewController {
Class klass = NSClassFromString(className);
UIViewController* modal = [viewController presentedViewController];
NSString* errorString = [NSString
stringWithFormat:@"A modal of class %@ should be presented by %@.", klass,
[viewController class]];
BOOL condition = modal && [modal isKindOfClass:klass];
if (!condition) {
return testing::NSErrorWithLocalizedDescription(errorString);
}
return nil;
}
+ (NSError*)assertModalOfClass:(NSString*)className
isNotPresentedBy:(UIViewController*)viewController {
Class klass = NSClassFromString(className);
UIViewController* modal = [viewController presentedViewController];
NSString* errorString = [NSString
stringWithFormat:@"A modal of class %@ should not be presented by %@.",
klass, [viewController class]];
BOOL condition = !modal || ![modal isKindOfClass:klass];
if (!condition) {
return testing::NSErrorWithLocalizedDescription(errorString);
}
return nil;
}
+ (BOOL (^)())blockForWaitingForModalOfClass:(NSString*)className
toDisappearFromAbove:(UIViewController*)viewController {
Class klass = NSClassFromString(className);
BOOL (^waitingBlock)() = ^BOOL {
UIViewController* modal = [viewController presentedViewController];
return !modal || ![modal isKindOfClass:klass];
};
return waitingBlock;
}
// 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 scanner::CAMERA_AVAILABLE:
case scanner::CAMERA_NOT_LOADED:
return nil;
case scanner::CAMERA_IN_USE_BY_ANOTHER_APPLICATION:
return l10n_util::GetNSString(
IDS_IOS_QR_SCANNER_CAMERA_IN_USE_ALERT_TITLE);
case scanner::CAMERA_PERMISSION_DENIED:
return l10n_util::GetNSString(
IDS_IOS_SCANNER_CAMERA_PERMISSIONS_HELP_TITLE_GO_TO_SETTINGS);
case scanner::CAMERA_UNAVAILABLE_DUE_TO_SYSTEM_PRESSURE:
case scanner::CAMERA_UNAVAILABLE:
return l10n_util::GetNSString(
IDS_IOS_QR_SCANNER_CAMERA_UNAVAILABLE_ALERT_TITLE);
case scanner::MULTIPLE_FOREGROUND_APPS:
return l10n_util::GetNSString(
IDS_IOS_QR_SCANNER_MULTIPLE_FOREGROUND_APPS_ALERT_TITLE);
}
}
#pragma mark VoiceOver Overrides
+ (void)overrideVoiceOverCheckForQRScannerViewController:
(UIViewController*)qrScanner
isOn:(BOOL)isOn {
QRScannerViewController* qrScannerViewController =
base::mac::ObjCCast<QRScannerViewController>(qrScanner);
[qrScannerViewController overrideVoiceOverCheck:isOn];
}
+ (void)postScanEndVoiceoverAnnouncement {
NSString* scannedAnnouncement = l10n_util::GetNSString(
IDS_IOS_SCANNER_SCANNED_ACCESSIBILITY_ANNOUNCEMENT);
[[NSNotificationCenter defaultCenter]
postNotificationName:UIAccessibilityAnnouncementDidFinishNotification
object:nil
userInfo:@{
UIAccessibilityAnnouncementKeyStringValue :
scannedAnnouncement
}];
}
@end
......@@ -22,6 +22,7 @@ source_set("scanner") {
"video_preview_view.mm",
]
deps = [
":camera_state",
"resources:scanner_torch_off",
"resources:scanner_torch_on",
"//base",
......@@ -40,3 +41,9 @@ source_set("scanner") {
"UIKit.framework",
]
}
source_set("camera_state") {
sources = [
"camera_state.h",
]
}
......@@ -9,36 +9,7 @@
#import <UIKit/UIKit.h>
#include "base/ios/block_types.h"
namespace scanner {
// Values to distinguish between different camera states to display the correct
// view controller or system alert.
// Note: no state encodes the state where the usage of the camera is prohibited
// because the app is in the background. The reason is that iOS transparently
// stops/starts the camera when the app enter/leaves the background.
// See AVCaptureSessionInterruptionReasonVideoDeviceNotAvailableInBackground for
// more information.
enum CameraState {
// Camera is loaded and available;
CAMERA_AVAILABLE = 0,
// The application cannot use the camera because it is in use exclusively by
// another application.
CAMERA_IN_USE_BY_ANOTHER_APPLICATION,
// The application cannot use the camera because video input is not supported
// if there are multiple foreground apps running.
MULTIPLE_FOREGROUND_APPS,
// The application does not have the permission to use the camera.
CAMERA_PERMISSION_DENIED,
// Camera unavailable due to "system pressure".
CAMERA_UNAVAILABLE_DUE_TO_SYSTEM_PRESSURE,
// The camera is unavailable for an unspecified reason.
CAMERA_UNAVAILABLE,
// The camera was not yet loaded.
CAMERA_NOT_LOADED,
};
} // namespace scanner
#include "ios/chrome/browser/ui/scanner/camera_state.h"
@protocol CameraControllerDelegate
......
// Copyright 2019 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.
#ifndef IOS_CHROME_BROWSER_UI_SCANNER_CAMERA_STATE_H_
#define IOS_CHROME_BROWSER_UI_SCANNER_CAMERA_STATE_H_
namespace scanner {
// Values to distinguish between different camera states to display the correct
// view controller or system alert.
// Note: no state encodes the state where the usage of the camera is prohibited
// because the app is in the background. The reason is that iOS transparently
// stops/starts the camera when the app enter/leaves the background.
// See AVCaptureSessionInterruptionReasonVideoDeviceNotAvailableInBackground for
// more information.
enum CameraState {
// Camera is loaded and available;
CAMERA_AVAILABLE = 0,
// The application cannot use the camera because it is in use exclusively by
// another application.
CAMERA_IN_USE_BY_ANOTHER_APPLICATION,
// The application cannot use the camera because video input is not supported
// if there are multiple foreground apps running.
MULTIPLE_FOREGROUND_APPS,
// The application does not have the permission to use the camera.
CAMERA_PERMISSION_DENIED,
// Camera unavailable due to "system pressure".
CAMERA_UNAVAILABLE_DUE_TO_SYSTEM_PRESSURE,
// The camera is unavailable for an unspecified reason.
CAMERA_UNAVAILABLE,
// The camera was not yet loaded.
CAMERA_NOT_LOADED,
};
} // namespace scanner
#endif // IOS_CHROME_BROWSER_UI_SCANNER_CAMERA_STATE_H_
......@@ -255,6 +255,7 @@ source_set("test_support") {
"//ios/chrome/browser/ui/omnibox:omnibox_internal",
"//ios/chrome/browser/ui/payments:payments_ui",
"//ios/chrome/browser/ui/popup_menu:constants",
"//ios/chrome/browser/ui/qr_scanner:test_support",
"//ios/chrome/browser/ui/recent_tabs:recent_tabs_ui_constants",
"//ios/chrome/browser/ui/safe_mode",
"//ios/chrome/browser/ui/safe_mode:test_support",
......@@ -381,6 +382,7 @@ source_set("eg_app_support+eg2") {
"//ios/chrome/browser/ui/omnibox:omnibox_internal",
"//ios/chrome/browser/ui/payments:payments_ui",
"//ios/chrome/browser/ui/popup_menu:constants",
"//ios/chrome/browser/ui/qr_scanner:eg_app_support+eg2",
"//ios/chrome/browser/ui/recent_tabs:recent_tabs_ui_constants",
"//ios/chrome/browser/ui/safe_mode",
"//ios/chrome/browser/ui/safe_mode:eg_app_support+eg2",
......
......@@ -74,6 +74,7 @@ chrome_ios_eg2_test("ios_chrome_ui_eg2tests_module") {
"//ios/chrome/browser/ui/open_in:eg2_tests",
"//ios/chrome/browser/ui/page_info:eg2_tests",
"//ios/chrome/browser/ui/popup_menu:eg2_tests",
"//ios/chrome/browser/ui/qr_scanner:eg2_tests",
"//ios/chrome/browser/ui/sad_tab:eg2_tests",
"//ios/chrome/browser/ui/safe_mode:eg2_tests",
"//ios/chrome/browser/ui/side_swipe:eg2_tests",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment