Nawin P.
Nawin P.

Reputation: 301

Is it possible to testing uitest (XCUI) when open url external browser?

Currently, I have to create automation ui test with XCUI and I have some action to open external browser by default is Safari.

I need to create some uitest's behavior like this.

  1. Go to external browser when click my button.
  2. Check is open correct url in browser (or just open browser) ?
  3. Go back to our app with resume state.

Is it impossible to do that ?.

Upvotes: 3

Views: 6121

Answers (5)

Bayram Binbir
Bayram Binbir

Reputation: 2217

✅ Following code snippet has worked for me.

    static func validateBrowserURL(url: String){
    let safari = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")
    defer {safari.terminate()}
    let addressBarIpad = safari.buttons["Address"]
    let addressBarIphone = safari.textFields["Address"]
    let addressBarTextField = safari.textFields["Address"]
    
    if app.isIPad {
        addressBarIpad.tapWhenDidAppear(waitSecondsForExistence: 10)
    } else {
        addressBarIphone.tapWhenDidAppear(waitSecondsForExistence: 10)
    }
    
    let urlValue = addressBarTextField.value as! String
    XCTAssertTrue(urlValue.contains(url), "Couldn't locate \(url)")
}

Upvotes: 0

Alchi
Alchi

Reputation: 849

You could achieve this by capturing the Safari app and validating your URL in its address bar in two steps. Assuming the tested URL is https://myapp.com/something, here is how to test if it's opened inside Safari:

        let safari = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")
        guard safari.wait(for: .runningForeground, timeout: 10) else {
            XCTFail("could not open Safari")
            return
        }
        // we use CONTAINS because at first, only the base URL is displayed inside the text field. For example, if the URL is https://myapp.com/something, the value of the address bar would be 'myapp.com'
        let pred = NSPredicate(format: "value CONTAINS[cd] %@", "myapp.com")
        let addressBar = safari.textFields.element(matching: pred)
        // tap is needed to reveal the full URL string inside the address bar
        addressBar.tap()
        
        let textField = safari.textFields.element(matching: pred)
        XCTAssertEqual(textField.value as? String, "https://myapp.com/something")

What happens here is that, as soon as the app opens the link inside Safari, it tries to read the target URL from the address bar, tap on it in order to be able to read its text field content, and finally asserts the expected URL string against the value of the address bar.

This approach is more stable as it directly checks the value of all the text fields inside the browser instead of referring to their identifiers, for example, checking the "Address" identifier won't work today as it no longer exists in the Safari view hierarchy.

Please note that this is a working solution for the actual state of the iOS which might be prone to breaking changes in the future.

Upvotes: 0

Mike Collins
Mike Collins

Reputation: 4549

The previous answers work fine for an external browser. If you app happens to launch inline browsers, they won't work as there is no URL bar. You could detect the inline browser and tap the Safari button to launch it externally before checking the URL, but then you'd have to reactivate your app and close the inline browser. Many more steps.

THERE IS AN EASIER WAY THAT WILL WORK WITH BOTH BROWSER TYPES!

What do these browsers share? A share button! How can we get the URL from there? Copy will place the URL in the pasteboard, which we have access to.

 app.buttons["ShareButton"].tap()
 _ = copyButton.waitForExistence(timeout: Waits.short.rawValue) // a tiny wait is necessary here for the share page to open fully
 app.buttons["Copy"].tap()
 let URL = UIPasteboard.general.string!

There are two catches here:

  1. When inline, app is your app. When external, it's com.apple.mobilesafari. You'll have to have logic to handle that.
  2. You want to wait for the page to finish loading. By not doing so, you will run into flakiness. When the page is loading, there is a ReloadButton. When that goes away, you should expect to see a StopButton. If the reload button doesn't disappear, the page isn't loading completely so you may need to hit the stop button, but do so in a safe way as I have seen maybe 1% of the time the page load completes in the milliseconds between giving up on it and performing that tap - I do a quick isHittable check before tapping.

Upvotes: 0

Providing the exact code, that worked for me

     enum SafariError: Error {
          case appLoadTimeout
     }
    
    let safari = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")
    app.buttons["MyButtom"].tap() // MyButton launches Safari
    
    guard safari.wait(for: .runningForeground, timeout: 5) else {
        throw SafariError.appLoadTimeout
    }
    
    safari.otherElements["Address"].tap()
    XCTAssertEqual(safari.textFields["Address"].value  as! String, "https://check-url.com")

    app.activate() //Back to my app

Upvotes: 4

Mike Collins
Mike Collins

Reputation: 4549

Absolutely. Safari is an XCUIApplication just like any other with a bundle identifier of com.apple.mobilesafari.

To check the URL you'll tap() the URL field (it's a button when a page loads) and read the value of the URL field (at this point it's a textField). Once you're done asserting that, activate() your app and you'll be back in it (note: my tests finish after asserting so I don't have to do this, but it's the published method - you could always enter debug and find how to tap the button to return to your application in the top-left of the screen if this doesn't work).

I'm happy to provide exact code if you show me you've tried this and can't get it working, but it's pretty straightforward XCUI automation.

Upvotes: 2

Related Questions