I am attempting to create a UI Test in XCode 10 that includes a step where the following window will appear:
(FYI - this is to support the creation of a video script, so we want to provide a full experience for the viewer)
Initially, I just recorded the steps to make sure I was properly capturing this request screen, and XCode dutifully logged out all of the steps.
Below is a lightly modified version from what XCode generates. For the record, using the unmodified version also fails.
func testDeletePhotos() {
let app = XCUIApplication()
continueAfterFailure = false
app.launch()
// Select two images from within app to delete
let collectionViewsQuery = app.collectionViews
collectionViewsQuery.children(matching: .cell)
.element(boundBy: 1)
.children(matching: .other)
.element.children(matching: .other)
.element(boundBy: 1)
.tap()
collectionViewsQuery.children(matching: .cell)
.element(boundBy: 0).children(matching: .other)
.element.children(matching: .other)
.element(boundBy: 1)
.tap()
let navigationBar = app.navigationBars["MY_APP_NAME"]
// Select the "Trash can" in nav bar to start delete process
navigationBar.buttons["Delete"].tap()
// Prompt appears properly and text is a precise match for string below
// Below step fails with error seen below
app.alerts["Allow “MY_APP_NAME” to delete 2 photos?"].buttons["Delete"].tap()
navigationBar.buttons["Back"].tap()
}
However, when I attempt to re-run this script, it fails to locate the Delete button as seen in the above image.
UI Test Activity: Assertion Failure: MyVideoScript.swift:###: No
matches found for Find: Descendants matching type Alert from input {(
Application, pid: 17838, label: 'MY_APP_NAME' )}
Also, since someone may mention this - the quantity of photos is the same, as this will change the text that appears in the alert. I also attempted to use the app.alerts.element(boundBy: 0) in place of the name search and got the same error.
The tap is probably performed before the alert finished appearing. You can tell the test to wait for it:
let deleteButton = app.alerts["Allow “MY_APP_NAME” to delete 2 photos?"].buttons["Delete"]
let exists = NSPredicate(format: "exists == 1")
expectationForPredicate(exists, evaluatedWithObject: deleteButton, handler: nil)
waitForExpectationsWithTimeout(5, handler: { error in
// check for error, if none:
deleteButton.tap()
})
Related
I use XCUITest for testing an app that allows the user to select an avatar by picking a photo from the gallery. When I tap on the button that opens the gallery window, I can see the elements in debugDescription. There is a table that contains the folders with photos. The problem is when I tap for the first time on any cell the test fails with error:
Assertion Failure: UserProfileAndSettingsTests.swift:434: Failed to get matching snapshot: No matches found for Element at index 2 from input {(
Table
)}".
If I put a breakpoint there, the second time I tap on any cell, it works.
The command is the following:
XCUIApplication().tables.element(boundBy: 2).cells.element(boundBy: 1).tap()
If before the command I put the line: XCUIApplication().tables.element(boundBy: 2).cells.element(boundBy: 1), it doesn't fail. It fails when trying to tap().
Looks like a timing issue. Familiarize yourself with the XCUIElement class, specifically with this:
/** Waits the specified amount of time for the element's exist property to be true and returns false if the timeout expires without the element coming into existence. */
open func waitForExistence(timeout: TimeInterval) -> Bool
You should be able to do something like this:
let element = XCUIApplication().tables.element(boundBy: 2).cells.element(boundBy: 1)
if element.waitForExistence(timeout: 2) {
element.tap()
}
I recommend making friends with this method, and also other similar methods and expectations, to be able to do convenient stuff like this (self in this context is a XCTestCase):
func waitUntilTappable(_ element:XCUIElement, timeout: TimeInterval = 2) {
let tappableExpectation = self.expectation(for: NSPredicate(format: "isHittable == true"),
evaluatedWith: element)
self.wait(for: [tappableExpectation], timeout: timeout.rawValue)
}
I’m implementing UI tests. The app makes API calls that could make alerts ( it's a UIView attached to the window ) appear. Of course, these are random/not predictable. If they show up, I have to dismiss them (clicking on the close button). Any idea how to do this? Do I have some event that says that something happened on the UI? I was thinking to have a thread that executes every 0.5 seconds that checks if the dismiss button exists and if so I tap on it.
DispatchQueue.global().async {
while true
{
DispatchQueue.main.async {
if(self.app.buttons["NotificationCloseButton"].exists)
{
self.app.buttons["NotificationCloseButton"].tap()
}
}
sleep(5)
}
}
The problem with this is that it causes random crashes: Neither attributes nor error returned
There is nice example of how to wait for element to appear on screen here. Here is example of code taken from the link:
let nextGame = self.app.staticTexts["Game 4 - Tomorrow"]
let exists = NSPredicate(format: "exists == true")
expectation(for: exists, evaluatedWithObject: nextGame, handler: nil)
app.buttons["Load More Games"].tap()
waitForExpectations(timeout: 5, handler: nil)
XCTAssert(nextGameLabel.exists)
Link also provides how to wait for system alert to appear:
addUIInterruptionMonitor(withDescription: "Location Dialog") { (alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
app.buttons["Find Games Nearby?"].tap()
app.tap() // need to interact with the app for the handler to fire
XCTAssert(app.staticTexts["Authorized"].exists)
I'm writing a framework around XCUITest that adds all kinds of convenient features (such as local HTML reports, Testrail integration, etc.) and I found that for our purpose it makes sense to have one class that extends XCTestCase and run all our tests from that (instead of having every test case class extend from XCTestCase and launch it via schemes which is very awkward). Our actual test classes (called Features) are then started sequentially from the one XCUITest method invocation.
This all works well to the point when I want to delete and re-install the tested app in between test cases.
I'm using the class from Is there a way to reset the app between tests in Swift XCTest UI in Xcode 7? to control the springboard via XCUITest.
The app is re-installed before every test case and it succeeds before the first test but then before the second test case I always get an error and the test runner quits:
t = 47.84s Find the Application "com.apple.springboard" 0x6080000ac2a0 (retry 2)
t = 47.84s Snapshot accessibility hierarchy for com.apple.springboard
t = 47.90s Assertion Failure: Springboard.swift:46: (null)
The error happens when resolve() is called a second time:
// Resolve the query for the springboard rather than launching it
springboard.resolve()
My related framework method which deletes and reinstalls the app:
func reinstallApp()
{
Springboard.deleteApp()
app.launchArguments.append("--uitesting")
app.launch()
}
Does anyone know a workaround that would prevent this error?
Update for Xcode 9:
I was hoping to fix the issue with Xcode 9 and Swift 4 and the new XCUIApplication.activate() method. My Springboard proxy class looks like this:
class Springboard
{
static let springboard:XCUIApplication? = XCUIApplication(bundleIdentifier: "com.apple.springboard")
static let settings:XCUIApplication? = XCUIApplication(bundleIdentifier: "com.apple.Preferences")
class func deleteApp()
{
XCUIApplication().terminate()
if let springboard = springboard
{
springboard.activate()
/* Force delete the app from the springboard. */
let icon = springboard.icons["appname"]
if icon.isHittable
{
let iconFrame = icon.frame
let springboardFrame = springboard.frame
icon.press(forDuration: 1.3)
/* Tap the little "X" button at approximately where it is. The X is not exposed directly. */
springboard.coordinate(withNormalizedOffset: CGVector(dx: (iconFrame.minX + 3) / springboardFrame.maxX, dy: (iconFrame.minY + 3) / springboardFrame.maxY)).tap()
springboard.alerts.buttons["Delete"].tap()
/* Press home once to make the icons stop wiggling. */
XCUIDevice.shared.press(.home)
/* Press home again to go to the first page of the springboard. */
XCUIDevice.shared.press(.home)
/* Wait some time for the animation to end. */
Thread.sleep(forTimeInterval: 0.5)
if let settings = settings
{
let settingsIcon = springboard.icons["Settings"]
if settingsIcon.isHittable
{
settingsIcon.tap()
settings.tables.staticTexts["General"].tap()
settings.tables.staticTexts["Reset"].tap()
settings.tables.staticTexts["Reset Location & Privacy"].tap()
settings.buttons["Reset Warnings"].tap()
settings.terminate()
}
else
{
XCUIDevice.shared.press(.home)
let settingsIcon = springboard.icons["Settings"]
if settingsIcon.isHittable
{
settingsIcon.tap()
settings.tables.staticTexts["General"].tap()
settings.tables.staticTexts["Reset"].tap()
settings.tables.staticTexts["Reset Location & Privacy"].tap()
settings.buttons["Reset Warnings"].tap()
settings.terminate()
}
}
}
else
{
print("iOS Settings app not found!")
}
}
else
{
print("App icon not found!")
}
}
else
{
print("Springboard not found!")
}
}
}
But now I'm getting the following error:
Non-critical error encountered: Automatic screenshot failure.
Linked XCTest.framework from /Users/user/Library/Developer/CoreSimulator/Devices/B9AC848C-37C8-46D6-8322-ED78E7201CFE/data/Containers/Bundle/Application/5AB3F814-9DF8-4F1C-BC46-478C42951E60/UITests-Runner.app/Frameworks/XCTest.framework/XCTest, built with Xcode 0900(9A221a), modified on Thursday, September 28, 2017 at 3:10:06 PM Japan Standard Time
XCTest.framework bundle version: 13201
Executing on OS: Version 11.0 (Build 15A372)
Screenshot request returned nil with no additional error information.
Please file a bug and attach the details above.
Does anyone know how to solve this new issue?
In my iOS i'm implementing UITests using XCUITest. It worked great since I had Swift 2.3, but after updating the app to Swift 3 basic actions like tap() don't work anymore.
Just a simple code that doesn't work anymore:
XCUIApplication().buttons["orgMenu"].tap()
throws
Assertion Failure: <unknown>:0: UI Testing Failure - Failure getting snapshot Error Domain=XCTestManagerErrorDomain Code=9 "Error -25204 getting snapshot for element <AXUIElement 0x7f8297d15a50> {pid=32375}" UserInfo={NSLocalizedDescription=Error -25204 getting snapshot for element <AXUIElement 0x7f8297d15a50> {pid=32375}}
The name of the button is correct: if I record the test and tap the button the line above is exactly what I get.
The button is in the view since I'm waiting for the existence of it (tried both manually, through a breakpoint, and programmatically with this:
let exists = NSPredicate(format: "exists == 1")
expectation(for: exists, evaluatedWith: XCUIApplication().buttons["orgMenu"], handler: nil
waitForExpectations(timeout: time, handler: nil)
)
And anyway, it worked before Swift 3.
Any idea? Thanks in advance!
I'll just answer to my own question, since I found a workaround.
Since Swift3, for some reason, UI tests are not able to manage views with a loadMore pattern. If I have in my view a TableView and somewhere in the code a manage the loadMore pattern manually, and in a test I tap a button, the loadMore is called in a sort of infinite loop, and that overheads the app's resources, making the test fail.
Workaround: just deactivate any loadMore if a UI Test is running.
In the tests' setup:
override func setUp() {
super.setUp()
let app = XCUIApplication()
continueAfterFailure = false
app.launchEnvironment = ["isUITest":"YES"]
app.launch()
}
In the view with the loadData:
-(void)loadMore
{
if ([Utils isRunningUITest]) {
return;
}
// My actual function.
}
and my Utils
+(BOOL)isRunningUITest {
NSDictionary *environment = [[NSProcessInfo processInfo] environment];
return environment[#"isUITest"];
}
Sorry for Swift and Objective-C mix, hope this is helpful to someone.
You can try and do something like:
let app = XCUIApplication()
XCTAssert(app.buttons["orgMenu"].waitForExistence(timeout: 10))
app.buttons["orgMenu"].tap()
My navigation bar as an "Add" button on it, and I need to have Xcode's UI test tap that button to perform tests in the view controller it opens. I add the button programmatically like so:
UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:#selector(showAddVC)];
self.navigationItem.rightBarButtonItem = addButton;
And in my test I have:
XCUIApplication *app = [[XCUIApplication alloc] init];
XCTAssert([app.buttons[#"Add"] exists]); // <-- This passes, so the test runner does see the button.
But when I try to tap it using either:
// Generated using the test recorder
[app.navigationBars[#"App Title"].buttons[#"Add"] tap];
or:
// Same expression used with the XCTAsset earlier
[app.buttons[#"Add"] tap];
Nothing happens. The action that should take place when the button is tapped does not happen. I tried adding some sleep(5)'s between lines to let the app load, but that didn't help much.
This is the test log:
Test Case '-[xx]' started.
t = 0.00s Start Test
t = 0.00s Set Up
2015-12-22 16:25:02.898 XCTRunner[10978:384690] Continuing to run tests in the background with task ID 1
t = 0.94s Launch xx
t = 1.01s Waiting for accessibility to load
t = 3.45s Wait for app to idle
t = 9.02s Tap "Add" Button
t = 9.02s Wait for app to idle
t = 39.07s Assertion Failure: UI Testing Failure - App failed to quiesce within 30s
xx: error: -[xx] : UI Testing Failure - App failed to quiesce within 30s
None of the above answers worked for me. What finally made it work, after hours of struggle, was repeating the tap. Try this:
[app.navigationBars[#"App Title"].buttons[#"Add"] tap];
[app.navigationBars[#"App Title"].buttons[#"Add"] tap];
While the above worked for me initially, I found that sometimes the first tap worked, which would result in two taps. My solution to this was to instead tap an arbitrary UI element that doesn't trigger any actions at the beginning of the UI test, and then proceed as normal. I think that the first tap works on certain devices, or maybe after the first UI test is run.
Testing for exists does not seem to be sufficient in your case. Wait for the button to be hittable before trying to tap it.
expectationForPredicate(predicate, evaluatedWithObject: element, handler: nil)
waitForExpectationsWithTimeout(timeoutSeconds, handler: nil)
Where in your case it would be:
expectationForPredicate(NSPredicate(format: "hittable == YES"), evaluatedWithObject: [app.buttons[#"Add"], handler: nil)
waitForExpectationsWithTimeout(15, handler: nil)
[app.buttons[#"Add"] tap];
This will pause execution of code after the waitForExpectationWithTimeout until that predicate has been satisfied with the given element.
Otherwise, in extreme cases I have found that sometimes errors occur when trying to interact with certain components. How, why and when these occur is a bit of a mystery, but they seem to be somewhat consistent to certain components, and things involving UINavigationBars seem to have them happen more often.
To overcome these, I have found that using this extension will sometimes work.
extension XCUIElement {
/* Sends a tap event to a hittable/unhittable element. Needed to get past bug */
func forceTapElement() {
if hittable {
tap()
}
else {
let coordinate: XCUICoordinate = coordinateWithNormalizedOffset(CGVectorMake(0.0, 0.0))
coordinate.tap()
}
}
}
For those for whom Alex's answer is not working, try this:
extension XCUIElement {
func forceTap() {
coordinate(withNormalizedOffset: CGVector(dx:0.5, dy:0.5)).tap()
}
}
I just had an issue with UIWebView which is hittable but the tap didn't work until done it via coordinate
A possibility is your bar button item pushes a new view controller that has a refresh control - you might especially encounter this on iPads only where your bar button item pushes onto the detail part of a split view controller. After fetching your items, you might stop your refresh control that's not even refreshing in the first place. Xcode's automation does throw hissy fits about this and sleeps/timeouts won't help there.
So to fix this issue, on your main app, always check your refresh control is refreshing before ending its refresh. I also like to check it's not nil in the first place: if you're using Swift, you can check this in one fell swoop:
if let refresh = self.refreshControl, refresh.refreshing {
refresh.endRefreshing()
}