chromium/ios/testing/earl_grey/system_alert_handler.mm

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/testing/earl_grey/system_alert_handler.h"

#import "base/logging.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/testing/system_alert_handler.h"

@implementation SystemAlertHandlerImpl

- (void)handleSystemAlertIfVisible {
  NSError* systemAlertFoundError = nil;
  [[EarlGrey selectElementWithMatcher:grey_systemAlertViewShown()]
      assertWithMatcher:grey_nil()
                  error:&systemAlertFoundError];

  if (systemAlertFoundError) {
    NSError* alertGetTextError = nil;
    NSString* alertText =
        [EarlGrey SystemAlertTextWithError:&alertGetTextError];
    GREYAssertNil(alertGetTextError, @"Error getting alert text.\n%@",
                  alertGetTextError);

    @try {
      // Calling this method has for side-effect to throw an
      // NSInternalInconsistencyException if the system alert is unknown to the
      // EG2 framework.
      // If it does throw, handle the system alert in @catch.
      // If it doesn’t throw, accept it here.
      // TODO(crbug.com/40127610): Style guide does not allow throwing
      // exceptions.
      [EarlGrey SystemAlertType];

      DLOG(WARNING) << "Accepting iOS system alert: "
                    << base::SysNSStringToUTF8(alertText);

      NSError* acceptAlertError = nil;
      [EarlGrey AcceptSystemDialogWithError:&acceptAlertError];
      GREYAssertNil(acceptAlertError, @"Error accepting system alert.\n%@",
                    acceptAlertError);

    } @catch (NSException* exception) {
      GREYAssert((exception.name == NSInternalInconsistencyException &&
                  [exception.reason containsString:@"Invalid System Alert"]),
                 @"Unknown error caught when handling unknown system alert: %@",
                 exception.reason);

      // Manually handle EG2-unsupported alerts for some known cases, otherwise
      // fail the test.
      [self handleSystemAlertUnsupportedByEG2:alertText];
    }
  }
  // Ensures no visible alert after handling.
  [EarlGrey WaitForAlertVisibility:NO
                       withTimeout:kSystemAlertVisibilityTimeout];
}

#pragma mark - Private

// Tries to dismiss a system alert if it matches a list of known system alerts.
// alertText (NSString*) is the text of the system alert currently being shown.
// If the button cannot be tapped (system alert cannot be dismissed), or the
// text does not match a known text/button pair, the test will fail.
- (void)handleSystemAlertUnsupportedByEG2:(NSString*)alertText {
  NSDictionary<NSString*, NSArray<NSString*>*>* textToButtons =
      TextToButtonsOfKnownSystemAlerts();
  for (NSString* text in textToButtons) {
    if ([alertText containsString:text]) {
      DLOG(WARNING) << "Dismissing iOS system alert with label: " << text;
      NSError* error;

      // Try every wanted button label possibility for that text before checking
      // for an error. Some button labels share an alert text.
      for (NSString* button in textToButtons[text]) {
        error = nil;
        [self tapAlertButtonWithText:button error:&error];

        // Break out of button labels loop if the current button worked.
        if (error == nil) {
          break;
        }
      }

      GREYAssertNil(error, @"Error dismissing iOS alert with label: %@\n%@",
                    text, error);

      // For the case where a second alert will appear in succession, wait for
      // it, and then handle it.
      if ([alertText isEqualToString:@"Software Update"]) {
        [EarlGrey WaitForAlertVisibility:YES
                             withTimeout:kSystemAlertVisibilityTimeout];
        [self handleSystemAlertUnsupportedByEG2:alertText];
      }

      return;
    }
  }

  XCTFail("An unsupported system alert is present on device. Failing the test. "
          "Alert label: %@",
          alertText);
}

// Taps button with `text` in the system alert on screen. If an alert or the
// button doesn't exist, note it in `error` accordingly.
- (void)tapAlertButtonWithText:(NSString*)text error:(NSError**)error {
  XCUIApplication* springboardApp = [[XCUIApplication alloc]
      initWithBundleIdentifier:@"com.apple.springboard"];
  XCUIElement* alert = springboardApp.alerts.firstMatch;
  if (![alert waitForExistenceWithTimeout:kSystemAlertVisibilityTimeout]) {
    *error = [NSError errorWithDomain:kGREYSystemAlertDismissalErrorDomain
                                 code:GREYSystemAlertNotPresent
                             userInfo:nil];
    return;
  }
  XCUIElement* button = alert.buttons[text];
  if (![alert.buttons[text] exists]) {
    *error = [NSError errorWithDomain:kGREYSystemAlertDismissalErrorDomain
                                 code:GREYSystemAlertCustomButtonNotFound
                             userInfo:nil];
    return;
  }

  [button tap];
}

@end