1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import <AVFoundation/AVFoundation.h>
6#import <UIKit/UIKit.h>
7
8#include "base/ios/ios_util.h"
9#include "base/strings/stringprintf.h"
10#include "base/strings/sys_string_conversions.h"
11#import "ios/chrome/browser/ui/qr_scanner/qr_scanner_app_interface.h"
12#include "ios/chrome/browser/ui/scanner/camera_state.h"
13#include "ios/chrome/grit/ios_strings.h"
14#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
15#import "ios/chrome/test/earl_grey/chrome_matchers.h"
16#import "ios/chrome/test/earl_grey/chrome_test_case.h"
17#include "ios/chrome/test/earl_grey/earl_grey_scoped_block_swizzler.h"
18#import "ios/testing/earl_grey/earl_grey_test.h"
19#import "net/base/mac/url_conversions.h"
20#include "net/test/embedded_test_server/embedded_test_server.h"
21#include "net/test/embedded_test_server/http_request.h"
22#include "net/test/embedded_test_server/http_response.h"
23#import "third_party/ocmock/OCMock/OCMock.h"
24#import "ui/base/l10n/l10n_util.h"
25#import "ui/base/l10n/l10n_util_mac.h"
26
27#if !defined(__has_feature) || !__has_feature(objc_arc)
28#error "This file requires ARC support."
29#endif
30
31using scanner::CameraState;
32
33// Override a QRScannerViewController voice over check, simulating voice
34// over being enabled. This doesn't reset the previous value, don't use
35// nested.
36class ScopedQRScannerVoiceSearchOverride {
37 public:
38  ScopedQRScannerVoiceSearchOverride(UIViewController* scanner_view_controller)
39      : scanner_view_controller_(scanner_view_controller) {
40    [QRScannerAppInterface
41        overrideVoiceOverCheckForQRScannerViewController:
42            scanner_view_controller_
43                                                    isOn:YES];
44  }
45
46  ~ScopedQRScannerVoiceSearchOverride() {
47    [QRScannerAppInterface overrideVoiceOverCheckForQRScannerViewController:
48                               scanner_view_controller_
49                                                                       isOn:NO];
50  }
51
52 private:
53  UIViewController* scanner_view_controller_;
54
55  DISALLOW_COPY_AND_ASSIGN(ScopedQRScannerVoiceSearchOverride);
56};
57
58// TODO(crbug.com/1015113) The EG2 macro is breaking indexing for some reason
59// without the trailing semicolon.  For now, disable the extra semi warning
60// so Xcode indexing works for the egtest.
61#pragma clang diagnostic push
62#pragma clang diagnostic ignored "-Wc++98-compat-extra-semi"
63GREY_STUB_CLASS_IN_APP_MAIN_QUEUE(QRScannerAppInterface);
64
65namespace {
66
67char kTestURL[] = "/testurl";
68char kTestURLResponse[] = "Test URL page";
69char kTestQuery[] = "testquery";
70char kTestQueryURL[] = "/searchurl/testquery";
71char kTestQueryResponse[] = "Test query page";
72
73char kTestURLEdited[] = "/testuredited";
74char kTestURLEditedResponse[] = "Test URL edited page";
75char kTestQueryEditedURL[] = "/searchurl/testqueredited";
76char kTestQueryEditedResponse[] = "Test query edited page";
77
78// The GREYCondition timeout used for calls to waitWithTimeout:pollInterval:.
79CFTimeInterval kGREYConditionTimeout = 5;
80// The GREYCondition poll interval used for calls to
81// waitWithTimeout:pollInterval:.
82CFTimeInterval kGREYConditionPollInterval = 0.1;
83
84// Returns the GREYMatcher for an element which is visible, interactable, and
85// enabled.
86id<GREYMatcher> VisibleInteractableEnabled() {
87  return grey_allOf(grey_sufficientlyVisible(), grey_interactable(),
88                    grey_enabled(), nil);
89}
90
91// Returns the GREYMatcher for the button that closes the QR Scanner.
92id<GREYMatcher> QrScannerCloseButton() {
93  return grey_allOf(chrome_test_util::ButtonWithAccessibilityLabel(
94                        QRScannerAppInterface.closeIconAccessibilityLabel),
95                    grey_userInteractionEnabled(), nil);
96}
97
98// Returns the GREYMatcher for the button which indicates that torch is off and
99// which turns on the torch.
100id<GREYMatcher> QrScannerTorchOffButton() {
101  return grey_allOf(grey_accessibilityLabel(l10n_util::GetNSString(
102                        IDS_IOS_SCANNER_TORCH_BUTTON_ACCESSIBILITY_LABEL)),
103                    grey_accessibilityValue(l10n_util::GetNSString(
104                        IDS_IOS_SCANNER_TORCH_OFF_ACCESSIBILITY_VALUE)),
105                    grey_accessibilityTrait(UIAccessibilityTraitButton), nil);
106}
107
108// Returns the GREYMatcher for the button which indicates that torch is on and
109// which turns off the torch.
110id<GREYMatcher> QrScannerTorchOnButton() {
111  return grey_allOf(grey_accessibilityLabel(l10n_util::GetNSString(
112                        IDS_IOS_SCANNER_TORCH_BUTTON_ACCESSIBILITY_LABEL)),
113                    grey_accessibilityValue(l10n_util::GetNSString(
114                        IDS_IOS_SCANNER_TORCH_ON_ACCESSIBILITY_VALUE)),
115                    grey_accessibilityTrait(UIAccessibilityTraitButton), nil);
116}
117
118// Returns the GREYMatcher for the QR Scanner viewport caption.
119id<GREYMatcher> QrScannerViewportCaption() {
120  return chrome_test_util::StaticTextWithAccessibilityLabelId(
121      IDS_IOS_QR_SCANNER_VIEWPORT_CAPTION);
122}
123
124// Returns the GREYMatcher for the Cancel button to dismiss a UIAlertController.
125id<GREYMatcher> DialogCancelButton() {
126  return grey_allOf(
127      grey_text(l10n_util::GetNSString(IDS_IOS_QR_SCANNER_ALERT_CANCEL)),
128      grey_accessibilityTrait(UIAccessibilityTraitStaticText),
129      grey_sufficientlyVisible(), nil);
130}
131
132// Opens the QR Scanner view.
133void ShowQRScanner() {
134  // Tap the omnibox to get the keyboard accessory view to show up.
135  [[EarlGrey selectElementWithMatcher:chrome_test_util::NewTabPageOmnibox()]
136      performAction:grey_tap()];
137  [ChromeEarlGrey
138      waitForSufficientlyVisibleElementWithMatcher:chrome_test_util::Omnibox()];
139
140  // Tap the QR Code scanner button in the keyboard accessory view.
141  [[EarlGrey
142      selectElementWithMatcher:grey_accessibilityLabel(@"QR code Search")]
143      performAction:grey_tap()];
144}
145
146// Taps the |button|.
147void TapButton(id<GREYMatcher> button) {
148  [[EarlGrey selectElementWithMatcher:button] performAction:grey_tap()];
149}
150
151// Appends the given |editText| to the |text| already in the omnibox and presses
152// the keyboard return key.
153void EditOmniboxTextAndTapKeyboardReturn(std::string text, NSString* editText) {
154  [[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(text)]
155      performAction:grey_typeText([editText stringByAppendingString:@"\n"])];
156}
157
158// Presses the keyboard return key.
159void TapKeyboardReturnKeyInOmniboxWithText(std::string text) {
160  [[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(text)]
161      performAction:grey_typeText(@"\n")];
162}
163
164// Provides responses for the test page URLs.
165std::unique_ptr<net::test_server::HttpResponse> StandardResponse(
166    const net::test_server::HttpRequest& request) {
167  std::unique_ptr<net::test_server::BasicHttpResponse> http_response =
168      std::make_unique<net::test_server::BasicHttpResponse>();
169  http_response->set_code(net::HTTP_OK);
170
171  char* body_content = nullptr;
172  if (base::StartsWith(request.relative_url, kTestURL,
173                       base::CompareCase::SENSITIVE)) {
174    body_content = kTestURLResponse;
175  } else if (base::StartsWith(request.relative_url, kTestQueryURL,
176                              base::CompareCase::SENSITIVE)) {
177    body_content = kTestQueryResponse;
178  } else if (base::StartsWith(request.relative_url, kTestURLEdited,
179                              base::CompareCase::SENSITIVE)) {
180    body_content = kTestURLEditedResponse;
181  } else if (base::StartsWith(request.relative_url, kTestQueryEditedURL,
182                              base::CompareCase::SENSITIVE)) {
183    body_content = kTestQueryEditedResponse;
184  } else {
185    return nullptr;
186  }
187
188  if (body_content) {
189    http_response->set_content(
190        base::StringPrintf("<html><body>%s</body></html>", body_content));
191  }
192
193  return std::move(http_response);
194}
195
196}  // namespace
197
198#pragma mark - Test Case
199
200@interface QRScannerViewControllerTestCase : ChromeTestCase {
201  GURL _testURL;
202  GURL _testURLEdited;
203  GURL _testQuery;
204  GURL _testQueryEdited;
205}
206
207@end
208
209@implementation QRScannerViewControllerTestCase {
210  // A swizzler for the CameraController method cameraControllerWithDelegate:.
211  std::unique_ptr<EarlGreyScopedBlockSwizzler> _camera_controller_swizzler;
212  // A swizzler for the LocationBarCoordinator method
213  // loadGURLFromLocationBar:transition:.
214  std::unique_ptr<EarlGreyScopedBlockSwizzler>
215      _load_GURL_from_location_bar_swizzler;
216}
217
218- (void)setUp {
219  [super setUp];
220
221  self.testServer->RegisterRequestHandler(
222      base::BindRepeating(&StandardResponse));
223  GREYAssertTrue(self.testServer->Start(), @"Server did not start.");
224
225  _testURL = self.testServer->GetURL(kTestURL);
226  _testURLEdited = self.testServer->GetURL(kTestURLEdited);
227  _testQuery = self.testServer->GetURL(kTestQueryURL);
228  _testQueryEdited = self.testServer->GetURL(kTestQueryEditedURL);
229}
230
231- (void)tearDown {
232  [super tearDown];
233  _load_GURL_from_location_bar_swizzler.reset();
234  _camera_controller_swizzler.reset();
235}
236
237// Checks that the close button is visible, interactable, and enabled.
238- (void)assertCloseButtonIsVisible {
239  [[EarlGrey selectElementWithMatcher:QrScannerCloseButton()]
240      assertWithMatcher:VisibleInteractableEnabled()];
241}
242
243// Checks that the torch off button is visible, interactable, and enabled, and
244// that the torch on button is not.
245- (void)assertTorchOffButtonIsVisible {
246  [[EarlGrey selectElementWithMatcher:QrScannerTorchOffButton()]
247      assertWithMatcher:VisibleInteractableEnabled()];
248  [[EarlGrey selectElementWithMatcher:QrScannerTorchOnButton()]
249      assertWithMatcher:grey_notVisible()];
250}
251
252// Checks that the torch on button is visible, interactable, and enabled, and
253// that the torch off button is not.
254- (void)assertTorchOnButtonIsVisible {
255  [[EarlGrey selectElementWithMatcher:QrScannerTorchOnButton()]
256      assertWithMatcher:VisibleInteractableEnabled()];
257  [[EarlGrey selectElementWithMatcher:QrScannerTorchOffButton()]
258      assertWithMatcher:grey_notVisible()];
259}
260
261// Checks that the torch off button is visible and disabled.
262- (void)assertTorchButtonIsDisabled {
263  [[EarlGrey selectElementWithMatcher:QrScannerTorchOffButton()]
264      assertWithMatcher:grey_allOf(grey_sufficientlyVisible(),
265                                   grey_not(grey_enabled()), nil)];
266}
267
268// Checks that the camera viewport caption is visible.
269- (void)assertCameraViewportCaptionIsVisible {
270  [[EarlGrey selectElementWithMatcher:QrScannerViewportCaption()]
271      assertWithMatcher:grey_sufficientlyVisible()];
272}
273
274// Checks that the close button, the camera preview, and the camera viewport
275// caption are visible. If |torch| is YES, checks that the torch off button is
276// visible, otherwise checks that the torch button is disabled. If |preview| is
277// YES, checks that the preview is visible and of the same size as the QR
278// Scanner view, otherwise checks that the preview is in the view hierarchy but
279// is hidden.
280- (void)assertQRScannerUIIsVisibleWithTorch:(BOOL)torch {
281  [self assertCloseButtonIsVisible];
282  [self assertCameraViewportCaptionIsVisible];
283  if (torch) {
284    [self assertTorchOffButtonIsVisible];
285  } else {
286    [self assertTorchButtonIsDisabled];
287  }
288}
289
290// Presents the QR Scanner with a command, waits for it to be displayed, and
291// checks if all its views and buttons are visible. Checks that no alerts are
292// presented.
293- (void)showQRScannerAndCheckLayoutWithCameraMock:(id)mock {
294  UIViewController* bvc = QRScannerAppInterface.currentBrowserViewController;
295  NSError* error =
296      [QRScannerAppInterface assertModalOfClass:@"QRScannerViewController"
297                               isNotPresentedBy:bvc];
298  GREYAssertNil(error, error.localizedDescription);
299  error = [QRScannerAppInterface assertModalOfClass:@"UIAlertController"
300                                   isNotPresentedBy:bvc];
301  GREYAssertNil(error, error.localizedDescription);
302
303  [QRScannerAppInterface addCameraControllerInitializationExpectations:mock];
304  ShowQRScanner();
305  [self waitForModalOfClass:@"QRScannerViewController" toAppearAbove:bvc];
306  [self assertQRScannerUIIsVisibleWithTorch:NO];
307  error =
308      [QRScannerAppInterface assertModalOfClass:@"UIAlertController"
309                               isNotPresentedBy:[bvc presentedViewController]];
310  GREYAssertNil(error, error.localizedDescription);
311  error = [QRScannerAppInterface assertModalOfClass:@"UIAlertController"
312                                   isNotPresentedBy:bvc];
313  GREYAssertNil(error, error.localizedDescription);
314}
315
316// Closes the QR scanner by tapping the close button and waits for it to
317// disappear.
318- (void)closeQRScannerWithCameraMock:(id)mock {
319  [QRScannerAppInterface addCameraControllerDismissalExpectations:mock];
320  TapButton(QrScannerCloseButton());
321  [self waitForModalOfClass:@"QRScannerViewController"
322       toDisappearFromAbove:QRScannerAppInterface.currentBrowserViewController];
323}
324
325// Checks that the omnibox is visible and contains |text|.
326- (void)assertOmniboxIsVisibleWithText:(std::string)text {
327  [[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(text)]
328      assertWithMatcher:grey_notNil()];
329}
330
331#pragma mark helpers for dialogs
332
333// Checks that the QRScannerViewController is presenting a UIAlertController and
334// that the title of this alert corresponds to |state|.
335- (void)assertQRScannerIsPresentingADialogForState:(CameraState)state {
336  NSError* error = [QRScannerAppInterface
337      assertModalOfClass:@"UIAlertController"
338           isPresentedBy:[QRScannerAppInterface.currentBrowserViewController
339                                 presentedViewController]];
340  GREYAssertNil(error, error.localizedDescription);
341  [[EarlGrey selectElementWithMatcher:grey_text([QRScannerAppInterface
342                                          dialogTitleForState:state])]
343      assertWithMatcher:grey_notNil()];
344}
345
346// Checks that there is no visible alert with title corresponding to |state|.
347- (void)assertQRScannerIsNotPresentingADialogForState:(CameraState)state {
348  [[EarlGrey selectElementWithMatcher:grey_text([QRScannerAppInterface
349                                          dialogTitleForState:state])]
350      assertWithMatcher:grey_nil()];
351}
352
353#pragma mark -
354#pragma mark Helpers for mocks
355
356// Swizzles the QRScannerViewController property cameraController: to return
357// |cameraControllerMock| instead of a new instance of CameraController.
358- (void)swizzleCameraController:(id)cameraControllerMock {
359  id swizzleCameraControllerBlock = [QRScannerAppInterface
360      cameraControllerSwizzleBlockWithMock:cameraControllerMock];
361
362  _camera_controller_swizzler = std::make_unique<EarlGreyScopedBlockSwizzler>(
363      @"QRScannerViewController", @"cameraController",
364      swizzleCameraControllerBlock);
365}
366
367// Swizzles the LocationBarCoordinator loadGURLFromLocationBarBlock:transition:
368// method to load |searchURL| instead of the generated search URL.
369- (void)swizzleLocationBarCoordinatorLoadGURLFromLocationBar:
370    (const GURL&)replacementURL {
371  NSURL* replacementNSURL = net::NSURLWithGURL(replacementURL);
372
373  id loadGURLFromLocationBarBlock = [QRScannerAppInterface
374      locationBarCoordinatorLoadGURLFromLocationBarSwizzleBlockForSearchURL:
375          replacementNSURL];
376  _load_GURL_from_location_bar_swizzler =
377      std::make_unique<EarlGreyScopedBlockSwizzler>(
378          @"LocationBarCoordinator",
379          @"loadGURLFromLocationBar:postContent:transition:disposition:",
380          loadGURLFromLocationBarBlock);
381}
382
383// Checks that the modal presented by |viewController| is of class |klass| and
384// waits for the modal's view to load.
385- (void)waitForModalOfClass:(NSString*)klassString
386              toAppearAbove:(UIViewController*)viewController {
387  NSError* error = [QRScannerAppInterface assertModalOfClass:klassString
388                                               isPresentedBy:viewController];
389  GREYAssertNil(error, error.localizedDescription);
390  UIViewController* modal = [viewController presentedViewController];
391  GREYCondition* modalViewLoadedCondition =
392      [GREYCondition conditionWithName:@"modalViewLoadedCondition"
393                                 block:^BOOL {
394                                   return [modal isViewLoaded];
395                                 }];
396  BOOL modalViewLoaded =
397      [modalViewLoadedCondition waitWithTimeout:kGREYConditionTimeout
398                                   pollInterval:kGREYConditionPollInterval];
399  NSString* errorString = [NSString
400      stringWithFormat:@"The view of a modal of class %@ should be loaded.",
401                       klassString];
402  GREYAssertTrue(modalViewLoaded, errorString);
403}
404
405// Checks that the |viewController| is not presenting a modal, or that the modal
406// presented by |viewController| is not of class |klass|. If a modal was
407// previously presented, waits until it is dismissed.
408- (void)waitForModalOfClass:(NSString*)klassString
409       toDisappearFromAbove:(UIViewController*)viewController {
410  BOOL (^waitingBlock)() =
411      [QRScannerAppInterface blockForWaitingForModalOfClass:klassString
412                                       toDisappearFromAbove:viewController];
413  Class klass = NSClassFromString(klassString);
414  GREYCondition* modalViewDismissedCondition =
415      [GREYCondition conditionWithName:@"modalViewDismissedCondition"
416                                 block:waitingBlock];
417
418  BOOL modalViewDismissed =
419      [modalViewDismissedCondition waitWithTimeout:kGREYConditionTimeout
420                                      pollInterval:kGREYConditionPollInterval];
421  NSString* errorString = [NSString
422      stringWithFormat:@"The modal of class %@ should be loaded.", klass];
423  GREYAssertTrue(modalViewDismissed, errorString);
424}
425
426#pragma mark -
427#pragma mark Tests
428
429// Tests that the close button, camera preview, viewport caption, and the torch
430// button are visible if the camera is available. The preview is delayed.
431- (void)testQRScannerUIIsShown {
432  id cameraControllerMock =
433      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
434                                 AVAuthorizationStatusAuthorized];
435  [self swizzleCameraController:cameraControllerMock];
436
437  // Open the QR scanner.
438  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
439
440  // Preview is loaded and camera is ready to be displayed.
441  [self assertQRScannerUIIsVisibleWithTorch:NO];
442
443  // Close the QR scanner.
444  [self closeQRScannerWithCameraMock:cameraControllerMock];
445  [cameraControllerMock verify];
446}
447
448// Tests that the torch is switched on and off when pressing the torch button,
449// and that the button icon changes accordingly.
450- (void)testTurningTorchOnAndOff {
451  id cameraControllerMock =
452      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
453                                 AVAuthorizationStatusAuthorized];
454  [self swizzleCameraController:cameraControllerMock];
455
456  // Open the QR scanner.
457  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
458
459  // Torch becomes available.
460  [QRScannerAppInterface callTorchAvailabilityChanged:YES];
461  [self assertQRScannerUIIsVisibleWithTorch:YES];
462
463  // Turn torch on.
464  [QRScannerAppInterface
465      addCameraControllerTorchOnExpectations:cameraControllerMock];
466  [self assertTorchOffButtonIsVisible];
467  TapButton(QrScannerTorchOffButton());
468  [self assertTorchOffButtonIsVisible];
469
470  // Torch becomes active.
471  [QRScannerAppInterface callTorchStateChanged:YES];
472  [self assertTorchOnButtonIsVisible];
473
474  // Turn torch off.
475  [QRScannerAppInterface
476      addCameraControllerTorchOffExpectations:cameraControllerMock];
477  TapButton(QrScannerTorchOnButton());
478  [self assertTorchOnButtonIsVisible];
479
480  // Torch becomes inactive.
481  [QRScannerAppInterface callTorchStateChanged:NO];
482  [self assertTorchOffButtonIsVisible];
483
484  // Close the QR scanner.
485  [self closeQRScannerWithCameraMock:cameraControllerMock];
486  [cameraControllerMock verify];
487}
488
489// Tests that if the QR scanner is closed while the torch is on, the torch is
490// switched off and the correct button indicating that the torch is off is shown
491// when the scanner is opened again.
492- (void)testTorchButtonIsResetWhenQRScannerIsReopened {
493  id cameraControllerMock =
494      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
495                                 AVAuthorizationStatusAuthorized];
496  [self swizzleCameraController:cameraControllerMock];
497
498  // Open the QR scanner.
499  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
500  [self assertQRScannerUIIsVisibleWithTorch:NO];
501  [QRScannerAppInterface callTorchAvailabilityChanged:YES];
502  [self assertQRScannerUIIsVisibleWithTorch:YES];
503
504  // Turn torch on.
505  [QRScannerAppInterface
506      addCameraControllerTorchOnExpectations:cameraControllerMock];
507  TapButton(QrScannerTorchOffButton());
508  [QRScannerAppInterface callTorchStateChanged:YES];
509  [self assertTorchOnButtonIsVisible];
510
511  // Close the QR scanner.
512  [self closeQRScannerWithCameraMock:cameraControllerMock];
513
514  // Reopen the QR scanner.
515  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
516  [QRScannerAppInterface callTorchAvailabilityChanged:YES];
517  [self assertTorchOffButtonIsVisible];
518
519  // Close the QR scanner again.
520  [self closeQRScannerWithCameraMock:cameraControllerMock];
521  [cameraControllerMock verify];
522}
523
524// Tests that the torch button is disabled when the camera reports that torch
525// became unavailable.
526- (void)testTorchButtonIsDisabledWhenTorchBecomesUnavailable {
527  id cameraControllerMock =
528      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
529                                 AVAuthorizationStatusAuthorized];
530  [self swizzleCameraController:cameraControllerMock];
531
532  // Open the QR scanner.
533  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
534
535  // Torch becomes available.
536  [QRScannerAppInterface callTorchAvailabilityChanged:YES];
537  [self assertQRScannerUIIsVisibleWithTorch:YES];
538
539  // Torch becomes unavailable.
540  [QRScannerAppInterface callTorchAvailabilityChanged:NO];
541  [self assertQRScannerUIIsVisibleWithTorch:NO];
542
543  // Close the QR scanner.
544  [self closeQRScannerWithCameraMock:cameraControllerMock];
545  [cameraControllerMock verify];
546}
547
548#pragma mark dialogs
549
550// Tests that a UIAlertController is presented instead of the
551// QRScannerViewController if the camera is unavailable.
552- (void)testCameraUnavailableDialog {
553  UIViewController* bvc = QRScannerAppInterface.currentBrowserViewController;
554  NSError* error =
555      [QRScannerAppInterface assertModalOfClass:@"QRScannerViewController"
556                               isNotPresentedBy:bvc];
557  GREYAssertNil(error, error.localizedDescription);
558  error = [QRScannerAppInterface assertModalOfClass:@"UIAlertController"
559                                   isNotPresentedBy:bvc];
560  GREYAssertNil(error, error.localizedDescription);
561
562  id cameraControllerMock = [QRScannerAppInterface
563      cameraControllerMockWithAuthorizationStatus:AVAuthorizationStatusDenied];
564  [self swizzleCameraController:cameraControllerMock];
565
566  ShowQRScanner();
567  error = [QRScannerAppInterface assertModalOfClass:@"QRScannerViewController"
568                                   isNotPresentedBy:bvc];
569  GREYAssertNil(error, error.localizedDescription);
570
571  [self waitForModalOfClass:@"UIAlertController" toAppearAbove:bvc];
572
573  TapButton(DialogCancelButton());
574  [self waitForModalOfClass:@"UIAlertController" toDisappearFromAbove:bvc];
575}
576
577// Tests that a UIAlertController is presented by the QRScannerViewController if
578// the camera state changes after the QRScannerViewController is presented.
579// TODO(crbug.com/1019211): Re-enable test on iOS12.
580- (void)testDialogIsDisplayedIfCameraStateChanges {
581  id cameraControllerMock =
582      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
583                                 AVAuthorizationStatusAuthorized];
584  [self swizzleCameraController:cameraControllerMock];
585
586  std::vector<CameraState> tests{scanner::MULTIPLE_FOREGROUND_APPS,
587                                 scanner::CAMERA_UNAVAILABLE,
588                                 scanner::CAMERA_PERMISSION_DENIED,
589                                 scanner::CAMERA_IN_USE_BY_ANOTHER_APPLICATION};
590
591  for (const CameraState& state : tests) {
592    [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
593    [QRScannerAppInterface callCameraStateChanged:state];
594    [self assertQRScannerIsPresentingADialogForState:state];
595
596    // Close the dialog.
597    [QRScannerAppInterface
598        addCameraControllerDismissalExpectations:cameraControllerMock];
599    TapButton(DialogCancelButton());
600    UIViewController* bvc = QRScannerAppInterface.currentBrowserViewController;
601    [self waitForModalOfClass:@"QRScannerViewController"
602         toDisappearFromAbove:bvc];
603    NSError* error =
604        [QRScannerAppInterface assertModalOfClass:@"UIAlertController"
605                                 isNotPresentedBy:bvc];
606    GREYAssertNil(error, error.localizedDescription);
607  }
608
609  [cameraControllerMock verify];
610}
611
612// Tests that a new dialog replaces an old dialog if the camera state changes.
613// TODO(crbug.com/1019211): Re-enable test on iOS12.
614- (void)testDialogIsReplacedIfCameraStateChanges {
615  id cameraControllerMock =
616      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
617                                 AVAuthorizationStatusAuthorized];
618  [self swizzleCameraController:cameraControllerMock];
619
620  // Change state to CAMERA_UNAVAILABLE.
621  CameraState currentState = scanner::CAMERA_UNAVAILABLE;
622  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
623  [QRScannerAppInterface callCameraStateChanged:currentState];
624  [self assertQRScannerIsPresentingADialogForState:currentState];
625
626  std::vector<CameraState> tests{scanner::CAMERA_PERMISSION_DENIED,
627                                 scanner::MULTIPLE_FOREGROUND_APPS,
628                                 scanner::CAMERA_IN_USE_BY_ANOTHER_APPLICATION,
629                                 scanner::CAMERA_UNAVAILABLE};
630
631  for (const CameraState& state : tests) {
632    [QRScannerAppInterface callCameraStateChanged:state];
633    [self assertQRScannerIsPresentingADialogForState:state];
634    [self assertQRScannerIsNotPresentingADialogForState:currentState];
635    currentState = state;
636  }
637
638  // Cancel the dialog.
639  [QRScannerAppInterface
640      addCameraControllerDismissalExpectations:cameraControllerMock];
641  TapButton(DialogCancelButton());
642  [self waitForModalOfClass:@"QRScannerViewController"
643       toDisappearFromAbove:QRScannerAppInterface.currentBrowserViewController];
644  NSError* error = [QRScannerAppInterface
645      assertModalOfClass:@"UIAlertController"
646        isNotPresentedBy:QRScannerAppInterface.currentBrowserViewController];
647  GREYAssertNil(error, error.localizedDescription);
648
649  [cameraControllerMock verify];
650}
651
652// Tests that an error dialog is dismissed if the camera becomes available.
653- (void)testDialogDismissedIfCameraBecomesAvailable {
654  id cameraControllerMock =
655      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
656                                 AVAuthorizationStatusAuthorized];
657  [self swizzleCameraController:cameraControllerMock];
658
659  std::vector<CameraState> tests{scanner::CAMERA_IN_USE_BY_ANOTHER_APPLICATION,
660                                 scanner::CAMERA_UNAVAILABLE,
661                                 scanner::MULTIPLE_FOREGROUND_APPS,
662                                 scanner::CAMERA_PERMISSION_DENIED};
663
664  for (const CameraState& state : tests) {
665    [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
666    [QRScannerAppInterface callCameraStateChanged:state];
667    [self assertQRScannerIsPresentingADialogForState:state];
668
669    // Change state to CAMERA_AVAILABLE.
670    [QRScannerAppInterface callCameraStateChanged:scanner::CAMERA_AVAILABLE];
671    [self assertQRScannerIsNotPresentingADialogForState:state];
672    [self closeQRScannerWithCameraMock:cameraControllerMock];
673  }
674
675  [cameraControllerMock verify];
676}
677
678#pragma mark scanned result
679
680// A helper function for testing that the view controller correctly passes the
681// received results to its delegate and that pages can be loaded. The result
682// received from the camera controller is in |result|, |response| is the
683// expected response on the loaded page, and |editString| is a nullable string
684// which can be appended to the response in the omnibox before the page is
685// loaded.
686- (void)doTestReceivingResult:(std::string)result
687                     response:(std::string)response
688                         edit:(NSString*)editString {
689  id cameraControllerMock =
690      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
691                                 AVAuthorizationStatusAuthorized];
692  [self swizzleCameraController:cameraControllerMock];
693
694  // Open the QR scanner.
695  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
696  [QRScannerAppInterface callTorchAvailabilityChanged:YES];
697  [self assertQRScannerUIIsVisibleWithTorch:YES];
698
699  // Receive a scanned result from the camera.
700  [QRScannerAppInterface
701      addCameraControllerDismissalExpectations:cameraControllerMock];
702  [QRScannerAppInterface
703      callReceiveQRScannerResult:base::SysUTF8ToNSString(result)];
704
705  [self waitForModalOfClass:@"QRScannerViewController"
706       toDisappearFromAbove:QRScannerAppInterface.currentBrowserViewController];
707  [cameraControllerMock verify];
708
709  // Optionally edit the text in the omnibox before pressing return.
710  [self assertOmniboxIsVisibleWithText:result];
711  if (editString != nil) {
712    EditOmniboxTextAndTapKeyboardReturn(result, editString);
713  } else {
714    TapKeyboardReturnKeyInOmniboxWithText(result);
715  }
716  [ChromeEarlGrey waitForWebStateContainingText:response];
717
718  // Press the back button to get back to the NTP.
719  [[EarlGrey selectElementWithMatcher:chrome_test_util::BackButton()]
720      performAction:grey_tap()];
721  NSError* error = [QRScannerAppInterface
722      assertModalOfClass:@"QRScannerViewController"
723        isNotPresentedBy:QRScannerAppInterface.currentBrowserViewController];
724  GREYAssertNil(error, error.localizedDescription);
725}
726
727// Test that the correct page is loaded if the scanner result is a URL which is
728// then manually edited when VoiceOver is enabled.
729- (void)testReceivingQRScannerURLResultWithVoiceOver {
730  id cameraControllerMock =
731      [QRScannerAppInterface cameraControllerMockWithAuthorizationStatus:
732                                 AVAuthorizationStatusAuthorized];
733  [self swizzleCameraController:cameraControllerMock];
734
735  // Open the QR scanner.
736  [self showQRScannerAndCheckLayoutWithCameraMock:cameraControllerMock];
737  [QRScannerAppInterface callTorchAvailabilityChanged:YES];
738  [self assertQRScannerUIIsVisibleWithTorch:YES];
739
740  // Add override for the VoiceOver check.
741  ScopedQRScannerVoiceSearchOverride scopedOverride(
742      [QRScannerAppInterface
743              .currentBrowserViewController presentedViewController]);
744
745  // Receive a scanned result from the camera.
746  [QRScannerAppInterface
747      addCameraControllerDismissalExpectations:cameraControllerMock];
748  [QRScannerAppInterface callReceiveQRScannerResult:base::SysUTF8ToNSString(
749                                                        _testURL.GetContent())];
750
751  // Fake the end of the VoiceOver announcement.
752  [QRScannerAppInterface postScanEndVoiceoverAnnouncement];
753
754  [self waitForModalOfClass:@"QRScannerViewController"
755       toDisappearFromAbove:QRScannerAppInterface.currentBrowserViewController];
756  [cameraControllerMock verify];
757
758  // Optionally edit the text in the omnibox before pressing return.
759  [self assertOmniboxIsVisibleWithText:_testURL.GetContent()];
760  TapKeyboardReturnKeyInOmniboxWithText(_testURL.GetContent());
761  [ChromeEarlGrey waitForWebStateContainingText:kTestURLResponse];
762}
763
764// Test that the correct page is loaded if the scanner result is a URL.
765- (void)testReceivingQRScannerURLResult {
766  [self doTestReceivingResult:_testURL.GetContent()
767                     response:kTestURLResponse
768                         edit:nil];
769}
770
771// Test that the correct page is loaded if the scanner result is a URL which is
772// then manually edited.
773- (void)testReceivingQRScannerURLResultAndEditingTheURL {
774  // TODO(crbug.com/753098): Re-enable this test on iPad once grey_typeText
775  // works.
776  if ([ChromeEarlGrey isIPadIdiom]) {
777    EARL_GREY_TEST_DISABLED(@"Test disabled on iPad.");
778  }
779
780  [self doTestReceivingResult:_testURL.GetContent()
781                     response:kTestURLEditedResponse
782                         edit:@"\bedited/"];
783}
784
785// Test that the correct page is loaded if the scanner result is a search query.
786- (void)testReceivingQRScannerSearchQueryResult {
787  [self swizzleLocationBarCoordinatorLoadGURLFromLocationBar:_testQuery];
788  [self doTestReceivingResult:kTestQuery response:kTestQueryResponse edit:nil];
789}
790
791// Test that the correct page is loaded if the scanner result is a search query
792// which is then manually edited.
793- (void)testReceivingQRScannerSearchQueryResultAndEditingTheQuery {
794  // TODO(crbug.com/753098): Re-enable this test on iPad once grey_typeText
795  // works.
796  if ([ChromeEarlGrey isIPadIdiom]) {
797    EARL_GREY_TEST_DISABLED(@"Test disabled on iPad.");
798  }
799
800  [self swizzleLocationBarCoordinatorLoadGURLFromLocationBar:_testQueryEdited];
801  [self doTestReceivingResult:kTestQuery
802                     response:kTestQueryEditedResponse
803                         edit:@"\bedited"];
804}
805
806@end
807