• Running UITests with Facebook Login in iOS
  • By Khoa Pham
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: LoneyIsError
  • Proofread by Alan

Image credit: Google

Today I’m trying to run some UITest on my app, which integrates with Facebook login. Here are some of my notes.

challenge

  • For us, the main challenge of using Facebook is that it usesSafari controller, and we mainly deal withweb view. Starting with iOS 9+, Facebook decided to use itsafarireplacenative facebook appTo avoid switching between applications. You can read the details hereBuild the best Facebook login experience for people on iOS 9
  • It doesn’t have what we wantaccessibilityIdentifieroraccessibilityLabel
  • Webview content may change in the future 😸

Create a Facebook test user

Fortunately, you don’t have to create your own Facebook users for testing. Facebook allows you to create test users and manage permissions and friends, which is very convenient

You can also choose different languages when we create test users. This will be the language displayed in Safari Web view. I’m with Norwegian 🇳🇴

Click the Login button and display the Facebook login

Here we use the default FBSDKLoginButton

var showFacebookLoginFormButton: XCUIElement {
  return buttons["Continue with Facebook"]}Copy the code

And then click on it

app.showFacebookLoginFormButton.tap()
Copy the code

Checking login Status

When accessing the Facebook form in Safari, the user may or may not be logged in. So we need to deal with both cases. So we need to deal with these two scenarios. When the user is logged in, Facebook will return you logged in or the OK button.

The suggestion here is to add breakpoints, and then use the LLDB commands Po app.staticTexts and Po app.buttons to view the UI elements under the current breakpoint.

You can examine static text, or just click the OK button

var isAlreadyLoggedInSafari: Bool {
  return buttons["OK"].exists || staticTexts["Du har allerede godkjent Blue Sea."].exists
}
Copy the code

Wait and refresh

Because the Facebook form is a WebView, its content is somewhat dynamic. And UITest seems to cache content for quick queries, so we need to wait and refresh the cache before examining the staticTexts

app.clearCachedStaticTexts()
Copy the code

The WAIT function is implemented here

extension XCTestCase {
  func wait(for duration: TimeInterval) {
    let waitExpectation = expectation(description: "Waiting")

    let when = DispatchTime.now() + duration
    DispatchQueue.main.asyncAfter(deadline: when) {
      waitExpectation.fulfill()
    }

    // We use a buffer here to avoid flakiness with Timer on CI
    waitForExpectations(timeout: duration + 0.5)}}Copy the code

Waiting for elements to appear

But a safer bet is to wait for the element to appear. For Facebook login forms, they display the Facebook TAB when it loads. So we should wait for this element to appear

extension XCTestCase {
  /// Wait for element to appear
  func wait(for element: XCUIElement, timeout duration: TimeInterval) {
    let predicate = NSPredicate(format: "exists == true")
    let _ = expectation(for: predicate, evaluatedWith: element, handler: nil)

    // Here we don't need to call `waitExpectation.fulfill()` // We use a buffer here to avoid flakiness with Timer on CI WaitForExpectations (timeout: duration + 0.5)}}Copy the code

Call this method before performing any further checks on the elements in the Facebook login form

wait(for: app.staticTexts["Facebook"], timeout: 5)
Copy the code

If the user is logged in

Once logged in, my application displays a map page in the home controller. Therefore, we need to do a simple test to check if the map exists

if app.isAlreadyLoggedInSafari {
  app.okButton.tap()

  handleLocationPermission()
  // Check for the map
  XCTAssertTrue(app.maps.element(boundBy: 0).exists)
}
Copy the code

Handling interrupts

We know that when we want to display a Location map, Core Location sends a request for permission. So we need to deal with this interruption as well. You need to make sure to call it early before it pops up

fileprivate func handleLocationPermission() {
  addUIInterruptionMonitor(withDescription: "Location permission", handler: { alert in
    alert.buttons.element(boundBy: 1).tap()
    return true})}Copy the code

Another problem is that the monitor will not be called. So the solution is to call app.tap() again when the box pops up. For me, I call app.tap() 1 or 2 seconds after my ‘map’ is displayed, to make sure app.tap() is called after the popup is displayed.

For a more detailed guide, see #48

If the user is not logged in

In this case, we need to fill in our email account and password. You can view the full source code section below. If the method doesn’t work or the Po command doesn’t print the elements you need, this could be because of caching or you need to wait until the dynamic content rendering is complete.

You need to wait for the element to appear

Click the text entry box

If you have this situation Neither element nor any descendant has keyboard focus, this is the solution

  • If you’re testing on the simulator, make sure it’s not checkedSimulator -> Hardware -> Keyboard -> Connect Hardware Keyboard
  • Slightly after I clickWait a momentthe
app.emailTextField.tap()
Copy the code

Clear all text

The idea is to move the cursor to the end of the text box, then delete each character in turn and type new text

extension XCUIElement {
  func deleteAllText() {
    guard let string = value as? String else {
      return
    }

    letLowerRightCorner = coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.9)) Lowerrightcorner.tap ()let deletes = string.characters.map({ _ in XCUIKeyboardKeyDelete }).joined(separator: "")
    typeText(deletes)
  }
}
Copy the code

Modify the locale

For me, I want to test in Norwegian, so we need to find the Norwegian option and click on it. It is recognized as static text by UI Test

var norwegianText: XCUIElement {
  return staticTexts["Norsk (called bokmal)"]}wait(for: app.norwegianText, timeout: 1)
app.norwegianText.tap()
Copy the code

Email account entry box

Fortunately, the mailbox account input field can be detected by UI Test as a Text field element, so we can query it. We use predicates here

var emailTextField: XCUIElement {
  let predicate = NSPredicate(format: "placeholderValue == %@"."E-post eller mobil")
  return textFields.element(matching: predicate)
}
Copy the code

Password input box

UI Test doesn’t seem to recognize the password input box, so we need to search through coordinate

var passwordCoordinate: XCUICoordinate {
  letVector = CGVector(dx: 1, dy: 1.5)return emailTextField.coordinate(withNormalizedOffset: vector)
}
Copy the code

Here is the documentation description of this method func coordinate(withNormalizedOffset normalizedOffset: CGVector) -> XCUICoordinate

Creates and returns a new coordinate with a normalized offset. Coordinates of screen points are obtained by multiplying the normalizedOffset elementframeSize and elementframeTo calculate by adding the origin of.

And then enter your password

app.passwordCoordinate.tap()
app.typeText("My password")
Copy the code

We should not use app. PasswordCoordinate. ReferencedElement because it can point to email accounts input box ❗ ️ 😢

Run the test again

Here we run the previous Test from Xcode -> Product -> Perform Actions -> Test Again

Below is the complete source code

import XCTest
class LoginTests: XCTestCase {
  var app: XCUIApplication!
  func testLogin() {
    continueAfterFailure = false
    app = XCUIApplication()
    app.launch()
    passLogin()
  }
}
extension LoginTests {
  func passLogin() {
    // Tap login
    app.showFacebookLoginFormButton.tap()
    wait(for: app.staticTexts["Facebook"], timeout: 5) // This requires a high timeout
     
    // There may be location permission popup when showing map
    handleLocationPermission()    
    if app.isAlreadyLoggedInSafari {
      app.okButton.tap()
      // Show map
      let map = app.maps.element(boundBy: 0)
      wait(for: map, timeout: 2)
      XCTAssertTrue(map.exists)
      // Need to interact with the app for interruption monitor to work
      app.tap()
    } else {
      // Choose norsk
     wait(for: app.norwegianText, timeout: 1)
      app.norwegianText.tap()
      app.emailTextField.tap()
      app.emailTextField.deleteAllText()
      app.emailTextField.typeText("[email protected]")
      app.passwordCoordinate.tap()
      app.typeText("Bob Alageaiecghfb Sharpeman")
      // login
      app.facebookLoginButton.tap()
      // press OK
      app.okButton.tap()
      // Show map
      let map = app.maps.element(boundBy: 0)
      wait(for: map, timeout: 2)
      XCTAssertTrue(map.exists)
      // Need to interact with the app for interruption monitor to work
      app.tap()
    }
  }
  fileprivate func handleLocationPermission() {
    addUIInterruptionMonitor(withDescription: "Location permission", handler: { alert in
      alert.buttons.element(boundBy: 1).tap()
      return true
    })
  }
}
fileprivate extension XCUIApplication {
  var showFacebookLoginFormButton: XCUIElement {
    return buttons["Continue with Facebook"]
  }
  var isAlreadyLoggedInSafari: Bool {
    return buttons["OK"].exists || staticTexts["Du har allerede godkjent Blue Sea."].exists
  }
  var okButton: XCUIElement {
    return buttons["OK"]
  }
  var norwegianText: XCUIElement {
    return staticTexts["Norsk (called bokmal)"]
  }
  var emailTextField: XCUIElement {
    let predicate = NSPredicate(format: "placeholderValue == %@"."E-post eller mobil")
    return textFields.element(matching: predicate)
  }
  var passwordCoordinate: XCUICoordinate {
    letVector = CGVector(dx: 1, dy: 1.5)return emailTextField.coordinate(withNormalizedOffset: vector)
  }
  var facebookLoginButton: XCUIElement {
    return buttons["Logg inn"]
  }
}
extension XCTestCase {
  func wait(for duration: TimeInterval) {
    let waitExpectation = expectation(description: "Waiting")
    let when = DispatchTime.now() + duration
    DispatchQueue.main.asyncAfter(deadline: when) {
      waitExpectation.fulfill()
    }
    // We use a buffer here to avoid flakiness with Timer on CI
    waitForExpectations(timeout: duration + 0.5)} /// Waitfor element to appear
  func wait(for element: XCUIElement, timeout duration: TimeInterval) {
    let predicate = NSPredicate(format: "exists == true")
    let _ = expectation(for: predicate, evaluatedWith: element, handler: nil)
    // We use a buffer here to avoid flakiness with Timer on CI
    waitForExpectations(timeout: duration + 0.5)}} Extension XCUIApplication {// Because of"Use cached accessibility hierarchy"
  func clearCachedStaticTexts() {
    let _ = staticTexts.count
  }
  func clearCachedTextFields() {
    let _ = textFields.count
  }
  func clearCachedTextViews() {
    let _ = textViews.count
  }
}
extension XCUIElement {
  func deleteAllText() {
    guard let string = value as? String else {
      return
    }
    letLowerRightCorner = coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.9)) Lowerrightcorner.tap ()let deletes = string.characters.map({ _ in XCUIKeyboardKeyDelete }).joined(separator: "")
    typeText(deletes)
  }
}
Copy the code

Another point

Thanks for the useful feedback onmy original articles github.com/onmyway133/… Here are some more ideas

  • To find the password entry field, we can actually usesecureTextFieldsInstead of usingcoordinate
  • waitThe function should act asXCUIElementExtension so that other elements can use it. Or you can use an old oneexpectationStyle, which does not involve hard-coded interval values.

Further development

These guides cover many aspects of UITests and are worth a look

  • UI-Testing-Cheat-Sheet
  • Everything About Xcode UI Testing

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.