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)
}
Related
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()
})
I am running a XCUI test on a view controller. Every time the view is initialize, an api call is made and an MBProgressHud is shown. My test demands to search for a button and tap on it, however I can't tap since an overlapping MBProgressHud is shown and also the api doesn't receive the response it needs to show the button. This led to failure of test.
My questions is what I am doing wrong.
Can we include this type of scenarios (like api calling) in our ui testing?
How to wait for the api call completion to continue our ui test?
Yes. We can include these scenarios in our ui testing. Here is the function we need to wait for the ui element to appear:
func waitForElementToAppear(_ element: XCUIElement) -> XCUIElement? {
let predicate = NSPredicate(format: "exists == true")
let expectation = XCTNSPredicateExpectation(predicate: predicate,
object: element)
let result = XCTWaiter().wait(for: [expectation], timeout: 10)
if result == .completed {
return element
}
return nil
}
This function will wait for api call to be completed and requires ui element to appear.
You can use and this works fine.
func waitForExistence(timeout: TimeInterval) -> Bool
https://developer.apple.com/documentation/xctest/xcuielement/2879412-waitforexistence
I'm developing a photo editing extension for iOS, bundled in a container app that provides the same photo editing functionality.
In order to reuse code, I have a view controller class that adopts the required PHContentEditingController protocol, and I subclassed it for use both as the main interface of the app extension, and as the "working screen" of the container app.
On the editing extension, the controller's methods are called by the Photos app's editing session as described in Apple's documentation and various tutorials you can find around the web.
On the container app, on the other hand, I first obtain a PHAsset instance by means of the UIImagePickerController class, and directly start the editing session manually on my "work" view controller like this:
// 'work' is my view controller which adopts
// `PHContentEditingController`. 'workNavigation'
// embeds it.
let options = PHContentEditingInputRequestOptions()
options.canHandleAdjustmentData = { (adjustmentData) in
return work.canHandle(adjustmentData)
}
asset.requestContentEditingInput(with: options, completionHandler: { [weak self] (input, options) in
// (Called on the Main thread on iOS 10.0 and above)
guard let this = self else {
return
}
guard let editingInput = input else {
return
}
work.asset = asset
work.startContentEditing(with: editingInput, placeholderImage: editingInput.displaySizeImage!)
this.present(workNavigation, animated: true, completion: nil)
})
When the user finishes editing, the work view controller calls finishContentEditing(completionHandler: on itself to finish the session:
self.finishContentEditing(completionHandler: {(output) in
// nil output results in "Revert" prompt.
// non-nil output results in "Modify" prompt.
let library = PHPhotoLibrary.shared()
library.performChanges({
let request = PHAssetChangeRequest(for: self.asset)
request.contentEditingOutput = output
}, completionHandler: {(didSave, error) in
if let error = error {
// handle error...
} else if didSave {
// proceed after saving...
} else {
// proceed after cancellation...
}
})
})
Within the editing session, the user can 'clear' the previous edits passed as adjustment data, effectively reverting the image to its original state.
I've noticed that, if I finish the editing by calling the completion handler passed to finishContentEditing(completionHandler:) with nil as its argument (instead of a valid PHContentEditingOutput object), the Photos framework will prompt the user to "revert" the image instead of "modifying" it:
func finishContentEditing(completionHandler: #escaping (PHContentEditingOutput?) -> Void) {
guard let editingInput = self.editingInput, let inputURL = editingInput.fullSizeImageURL else {
return completionHandler(nil)
}
if editingInput.adjustmentData != nil && hasUnsavedEdits == false {
// We began with non-nil adjustment data but now have
// no outstanding edits - means REVERT:
return completionHandler(nil)
}
// (...proceed with writing output to url...)
However, this only works when running from the container app. If I try the same trick from the extension (i.e., load an image that contains previous edits, reset them, and tap 'Done') I get the dreaded "Unable to Save Changes" message...
What is the correct way to revert previous edits to an image from within a photo editing extension?
Months later, still no answer, so I begrudgingly adopted this workaround (which is still preferable to an error alert):
When the user taps "Done" from the Photos extension UI and the image has no edits applied to it (either because the user reset previous edits, or didn't apply any changes to a brand new image), perform the following actions from within finishContentEditing(completionHandler:):
Create adjustment data that amounts to no visible changes at all ("null effect") and archive it as Data.
Create a PHAdjustmentData instance with the "null effect" data from above, with the formatVersion and formatIdentifier properly set.
Create a PHContentEditingOutput instance from the editing input passed at the beginning of the session (as usual), and set the adjustment data created above.
Read the unmodified image from the inputURL property of the editing input, and write it unmodified to the url specified by the PHContentEditingOutput instance's renderedContentURL property.
Call the completionHandler block, passing the editing output instance (as normal).
The result: The image is saved in its original state (no effects applied), and no alerts or errors occur.
The drawback: The library asset remains in the 'edited' state (because we passed non-nil editing output and adjustment data, there was no other choice), so the next time the user tries to edit it from Photos.app, the red 'Revert' button will be present:
However, choosing 'revert' results in no visible changes to the image data (duh!), which can be confusing to the user.
—-
Update
I checked what the built-in “Markup” extension does:
...and it is consistent with my workaround above, so I guess this is the best that can be done.
Here is the relevant function in my ViewController:
#IBAction func findPeople(_ sender: Any) {
let center = CLLocation(latitude: myLocation.latitude, longitude: myLocation.longitude)
let radiusQuery = geoFireUserLocations!.query(at: center, withRadius: 0.05)
print("Start Looking for People")
_ = radiusQuery!.observe(.keyEntered, with: { (key: String?, location: CLLocation?) in
print(" ALERT: Found Someone Close : ",key!)
})
_ = radiusQuery?.observeReady({
print(" All initial data has been loaded and events have been fired!")
})
print("Done Looking for People")
//TODO: Based on the GeoFire result do something smart
}
The operation works, but the response comes in an unexpected order. I thought that the .observeReady call would return the initial set of results, but it does not return anything right away, and execution continues on to the line that prints "Done Looking for People".
Is there any way to have my function block until it gets an initial set of results?
Here's the output that I get:
Start Looking for People
Done Looking for People
ALERT: Found Someone Close : oSf00ex6SyMAwpF2NRxymyxxx123
All initial data has been loaded and events have been fired!
I was expecting:
Start Looking for People
ALERT: Found Someone Close : oSf00ex6SyMAwpF2NRxymyxxx123
All initial data has been loaded and events have been fired!
Done Looking for People
How can I create a function that blocks until it gets an initial result from GeoFire?
I'd like to send the request, get a result, then take the appropriate action based on the result. The way the app is currently behaving there does not seem to be a way to have it wait for a result.
Can anyone offer some insight and/or suggestion about how to make this work the way I'm describing? Is there some sort of blocking call that I could call or a synchronization method that I could be using to ensure that I get a result before deciding the next action?
Cheers!
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()
}