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()
}
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)
}
WKInterfaceMap sometimes renders blank in a very simple app I am developing. The map appears completely empty (no grid of lines or anything similar). It looks as though the thread responsible for drawing the tiles gets blocked.
In order to reproduce the issue just add the code below to the extension delegate.
func applicationDidEnterBackground() {
let watchExtension = WKExtension.shared()
// Schedule the background refresh task.
watchExtension.scheduleBackgroundRefresh(withPreferredDate: Date().addingTimeInterval(15.0*60.0), userInfo: nil) { (error) in
// Check for errors.
if let error = error {
print("ExtensionDelegate: \(error.localizedDescription)")
return
}
print("ExtensionDelegate: Background Task scheduled successfuly")
}
}
Add a WKInterfaceMap to the main view and run once on the simulator. Close the app using the crown and stop it from XCode. Wait for at least 15 minutes and open the app again directly from the simulator.
The map renders then as in the image below.
Basically the problem is the same as this one: XCTestCase: Wait for app to idle
I am using perpetually repeating "background animations" in my views. The UI testing of Xcode/iOS wants to wait for all UIView animations to end before it considers the app idle and goes on with stuff like tapping buttons etc. It just doesn't work with the way we've designed the app(s). (Specifically, we have a button that is animated with UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse options, so it never stops.)
But I'm thinking there might be some way to turn off and/or shorten the state "Wait for app to idle". Is there? How? Is there any other way around this?
You actually can disable wait for app to idle. This is a hack and may not be stable. With animations disabled, and this hack enabled, I am seeing about a 20% performance gain (on top of the performance boost from disabling animations).
All you have to do is swizzle out the method that is called to idle the app and no-op it. That method is XCUIApplicationProcess waitForQuiescenceIncludingAnimationsIdle:
Here is my working solution in swift 3 - there is likely a better way but this works for a proof of concept.
Extend the XCTestCase class. I'll call mine MyTestCase
static var swizzledOutIdle = false
override func setUp() {
if !MyTestCase.swizzledOutIdle { // ensure the swizzle only happens once
let original = class_getInstanceMethod(objc_getClass("XCUIApplicationProcess") as! AnyClass, Selector(("waitForQuiescenceIncludingAnimationsIdle:")))
let replaced = class_getInstanceMethod(type(of: self), #selector(MyTestCase.replace))
method_exchangeImplementations(original, replaced)
MyTestCase.swizzledOutIdle = true
}
super.setUp()
}
#objc func replace() {
return
}
Note wait for app to idle will no longer appear in the logs.
Unfortunately using Apple's UI Testing you can't turn 'wait for app to idle' or poll other network activity, however you can use environment variables to disable animations in your app to make the tests more stable. In your setup method before your test set an environment variable like this.
override func setUp() {
super.setUp()
continueAfterFailure = false
let app = XCUIApplication()
app.launchEnvironment = ["UITEST_DISABLE_ANIMATIONS" : "YES"]
app.launch()
}
Now in your source code:
if (ProcessInfo.processInfo.environment["UITEST_DISABLE_ANIMATIONS"] == "YES") {
UIView.setAnimationsEnabled(false)
}
You can place that check in a specific view if you only want it to disable animations for that specific view or in a delegate file to disable animations throughout the app.
I used gh123man answer in Objective-C in case anyone needs it:
- (void)disableWaitForIdle {
SEL originalSelector = NSSelectorFromString(#"waitForQuiescenceIncludingAnimationsIdle:");
SEL swizzledSelector = #selector(doNothing);
Method originalMethod = class_getInstanceMethod(objc_getClass("XCUIApplicationProcess"), originalSelector);
Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (void)doNothing {
// no-op
}
I was using gh123man's solution in setUp() of a couple of test classes and it worked like a charm until updating to iOS 13.3. Since then app gets stuck in launching state.
Found that it still works if I move it to methods like disableWaitForIdle() and enableWaitForIdle() and call them only in the most granular manner (before and after the tap where I know the app will never become idle), e.g. like this:
#discardableResult func selectOption() -> Self {
disableWaitForIdle()
app.cells["Option"].firstMatch.waitAndForceTap(timeout: 20)
enableWaitForIdle()
return self
}
I translated to Objective-C, and successfully used, h.w.powers' Swift solution, in case anyone needs it.
For setting up:
XCUIApplication *app = [[XCUIApplication alloc] init];
app.launchEnvironment = #{#"UITEST_DISABLE_ANIMATIONS":#"YES"};
[app launch];
and then in your code
if ([[[NSProcessInfo processInfo] environment][#"UITEST_DISABLE_ANIMATIONS"] isEqualToString:#"YES"]) {
// do something like stopping the animation
}
For anyone who intermittently runs into this wait for app to idle problem, I also experienced it a few times while running local XCUITests. Quitting and re-opening the simulator has done the trick for me, not sure exactly why. Maybe some system UIKit stuff getting wacky after the simulator has been running for 2 weeks.
I am attempting to do what is described perfectly here: https://github.com/bryx-inc/BRYXBanner
I want to create a banner that pops down on the screen for a few seconds before being removed (or removed when it is tapped on). The above project is great up until iOS 9. After that and with iOS 10, the banner no longer works as predicted and either shows itself without an animation for a third of a second or it doesn't show.
How can I add a view that animates onto the screen and then back off to provide a user a quick "No Internet" notification in-app. I want to avoid using the notification center.
I had a similar problem and created my own library for it: MDNotificationView
The example app on GitHub implements your idea. Here is a small snippet implementing it:
let view = MDNotificationCompactLayoutView()
view.textLabel.text = "No internet connection."
let notificationView = MDNotificationView(view: view)
notificationView.delegate = self
notificationView.show()
// MARK: - Notification View Delegate
func notificationDidShow(notificationView: MDNotificationView) {
// Hide the notification view automatically after 5 seconds.
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
notificationView.hide()
}
}
Does any body know what I need to check if app freezes after some time? I mean, I can see the app in the iPhone screen but no view responds.
I did some google and i found that, i've blocked the main thread somehow.
But my question is how to identify which method causes blocking of main thread? is there any way to identify?
Launch your app and wait for it to freeze. Then press the "pause" button in Xcode. The left pane should show you what method is currently running.
Generally, it is highly recommended to perform on the main thread all animations method and interface manipulation, and to put in background tasks like download data from your server, etc...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//here everything you want to perform in background
dispatch_async(dispatch_get_main_queue(), ^{
//call back to main queue to update user interface
});
});
Source : http://www.raywenderlich.com/31166/25-ios-app-performance-tips-tricks
Set a break point from where the freeze occurs and find which line cause that.
Chances may be,Loading of large data,disable the controls,overload in main thread,Just find out where that occurs using breakpoints and rectify based on that.
I believe it should be possible to periodically check to see if the main thread is blocked or frozen. You could create an object to do this like so:
final class FreezeObserver {
private let frequencySeconds: Double = 10
private let acceptableFreezeLength: Double = 0.5
func start() {
DispatchQueue.global(qos: .background).async {
let timer = Timer(timeInterval: self.frequencySeconds, repeats: true) { _ in
var isFrozen = true
DispatchQueue.main.async {
isFrozen = false
}
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + self.acceptableFreezeLength) {
guard isFrozen else { return }
print("your app is frozen, so crash or whatever")
}
}
let runLoop = RunLoop.current
runLoop.add(timer, forMode: .default)
runLoop.run()
}
}
}
Update October 2021:
Sentry now offers freeze observation, if you don't wanna roll this yourself.
I reached an error similar to this, but it was for different reasons. I had a button that performed a segue to another ViewController that contained a TableView, but it looked like the application froze whenever the segue was performed.
My issue was that I was infinitely calling reloadData() due to a couple of didSet observers in one of my variables. Once I relocated this call elsewhere, the issue was fixed.
Most Of the Time this happened to me when a design change is being called for INFINITE time. Which function can do that? well it is this one:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
Solution is to add condition where the function inside of viewDidLayoutSubviews get calls only 1 time.
It could be that another view is not properly dismissed and it's blocking user interaction! Check the UI Debugger, and look at the top layer, to see if there is any strange thing there.