Source: Google
Today I’m trying to run some UITest on my app, which uses Facebook login. And here are some of my notes on it.
Safari controller
, we we deal mostly with web view
for now. Starting from iOS 9+, Facebook decided to use safari
instead of native facebook app
to avoid app switching. You can read the detail here Building the Best Facebook Login Experience for People on iOS 9accessibilityIdentifier
or accessibilityLabel
Luckily, you don’t have to create your own Facebook user to test. Facebook supports test users that you can manage permissions and friends, very handy
When creating the test user, you have the option to select language. That will be the displayed language in Safari web view. I choose Norwegian
🇳🇴 for now
Here we use the default FBSDKLoginButton
var showFacebookLoginFormButton: XCUIElement {return buttons["Continue with Facebook"]}
And then tap it
app.showFacebookLoginFormButton.tap()
When going to safari Facebook form, user may have already logged in or not. So we need to handle these 2 cases. When user has logged in, Facebook will say something like “you have already logged in” or the OK
button.
The advice here is to put breakpoint and po app.staticTexts
, po app.buttons
to see which UI elements are at a certain point.
You can check for the static text, or simply just the OK
button
var isAlreadyLoggedInSafari: Bool {return buttons["OK"].exists || staticTexts["Du har allerede godkjent Blue Sea."].exists}
But Facebook form is a webview, so its content is a bit dynamic. And UITest seems to cache content for fast query, so before checking staticTexts
, we need to wait
and refresh the cache
app.clearCachedStaticTexts()
This is the wait
function
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)
}}
But a more solid approach would be to wait for element to appear. For Facebook login form, they should display a Facebook
label after loading. So we should wait for this element
extension XCTestCase {/// Wait for element to appearfunc 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)
}}
And call this before you do any further inspection on elements in Facebook login form
wait(for: app.staticTexts["Facebook"], timeout: 5)
After login, my app shows the main controller with a map view inside. So a basic test would be to check the existence of that map
if app.isAlreadyLoggedInSafari {app.okButton.tap()
handleLocationPermission()// Check for the mapXCTAssertTrue(app.maps.element(boundBy: 0).exists)}
You know that when showing the map with location, Core Location
will ask for permission. So we need to handle that interruption as well. You need to ensure to call it early before the alert happens
fileprivate func handleLocationPermission() {addUIInterruptionMonitor(withDescription: "Location permission", handler: { alert inalert.buttons.element(boundBy: 1).tap()return true})}
There is another problem, this monitor
won't be called. So the workaround is to call app.tap()
again when the alert will happen. In my case, I call app.tap()
when my map
has been shown for 1,2 seconds, just to make sure app.tap()
is called after alert is shown
For a more detailed guide, please read #48
In this case, we need to fill in email and password. You can take a look at the The full source code
section below. When things don't work or po
does not show you the elements you needed, it's probably because of caching or you need to wait until dynamic content finishes rendering.
You need to wait for element to appear
You may get Neither element nor any descendant has keyboard focus
, here are the workaround
Simulator -> Hardware -> Keyboard -> Connect Hardware Keyboard
is not checkedwait
a bit after tapapp.emailTextField.tap()
The idea is to move the caret to the end of the textField, then apply each delete key
for each character, then type the next text
extension XCUIElement {func deleteAllText() {guard let string = value as? String else {return}
let lowerRightCorner = coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.9))
lowerRightCorner.tap()
let deletes = string.characters.map({ \_ in XCUIKeyboardKeyDelete }).joined(separator: "")
typeText(deletes)
}}
For my case, I want to test in Norwegian, so we need to find the Norwegian
option and tap on that. It is identified as static text
by UI Test
var norwegianText: XCUIElement {return staticTexts["Norsk (bokmål)"]}
wait(for: app.norwegianText, timeout: 1)app.norwegianText.tap()
Luckily, email text field is detected by UI Test
as text field
element, so we can query for that. This uses predicate
var emailTextField: XCUIElement {let predicate = NSPredicate(format: "placeholderValue == %@", "E-post eller mobil")return textFields.element(matching: predicate)}
UI Test
can't seem to identify the password text field, so we need to search for it by coordinate
var passwordCoordinate: XCUICoordinate {let vector = CGVector(dx: 1, dy: 1.5)return emailTextField.coordinate(withNormalizedOffset: vector)}
This is the document for func coordinate(withNormalizedOffset normalizedOffset: CGVector) -> XCUICoordinate
Creates and returns a new coordinate with a normalized offset.The coordinate’s screen point is computed by adding normalizedOffset multiplied by the size of the element’s frame to the origin of the element’s frame.
Then type the password
app.passwordCoordinate.tap()app.typeText("My password")
We should not use app.passwordCoordinate.referencedElement
because it will point to email text field ❗️ 😢
Go to Xcode -> Product -> Perform Actions -> Test Again
to run the previous test again
Here are the full source code
Thanks to the helpful feedback on my article Original story https://github.com/onmyway133/blog/issues/44, here are some more ideas
secureTextFields
instead of coordinatewait
function should be made as an extension to XCUIElement
so other element can use that. Or you can just use the old expectation
style, which does not involve a hardcoded interval value.I found these guides to cover many aspects of UITests, worth taking a look