I am writing UI tests for my app. I have two alerts, location and notifications. I am struggling to find a solution in order to dismiss these alerts.
Currently I am using
systemAlertMonitorToken = addUIInterruptionMonitorWithDescription(systemAlertHandlerDescription) { (alert) -> Bool in
if alert.buttons.matchingIdentifier("OK").count > 0 {
alert.buttons["OK"].tap()
return true
} else {
return false
}
}
and
let notifications = self.app.alerts.element.collectionViews.buttons["OK"]
if notifications.exists {
notifications.tap()
}
however both functions are not allowing me to dismiss the alerts.
EDIT
Now I have added
app.buttons["OK"].tap()
app.tap()
to my code, but it means my tests are failing due to the XCT looking for the button "OK" straight away when it isnt a notification what pops up straight away.
I only want the alert OK to be dismissed when it pops up not on the first thing for launch.
The interruption monitor will only trigger the next time you try to interact with the app, so you need to have code after the interruption monitor is registered to do whatever you want to do after dismissing the alert, e.g tapping another button in your UI.
When the code gets to the part where you next interact with the UI, the handler for the completion handler will be executed and the system alert will be dealt with.
Related
Background
Out app has a feature to launch background using Significant-Change Location Service(https://developer.apple.com/documentation/corelocation/getting_the_user_s_location/using_the_significant-change_location_service).
This an iOS feature that when the app detect significant change in location, the app launched on background. It launches directly to background state so the user won't notice that app has launched(unless writing code to notify to users. ie. sending a local push or etc.) however app is fully launched so didFinishLaunchingWithOptions method on App Delegate is called upon launch.
Situation
When our app is launched by Significant-Change Location, we send local push notification to the user notifying that how much distance the user has walked recently(we have implemented pedometer to our app). At this point, our app is launched on background.
A user may tap the local notification just received and then our app's lifecycle state will be changed to foreground and app's home screen will be displayed to the user(from user point of view app is LAUNCHED at this point).
On home view controller, we observe UIApplication.didBecomeActiveNotification and when detecting this notification, we make an API request. API response may have an award field(nullable) and when the award is not null, we show the award received modal.
// Observe app state changes to active
let center = NotificationCenter.default
center.addObserver(
self,
selector: #selector(callRewardAPI), // calls api when detecting notification
name: UIApplication.didBecomeActiveNotification, // <- didBecomeActiveNotification
object: nil
)
func callRewardAPI {
usecase.callRewardAPI)()
}
// Show modal when received API response
extension: HomeViewController, HomeUseCaseOutput {
func didReceiveReward() {
let vc = RewardReceivedViewController()
vc.modalTransitionStyle = .crossDissolve
vc.modalPresentationStyle = .overFullScreen
self.present(vc, animated: true, completion: nil)
}
}
Problem
Expected
We expect to see the modal animation of RewardReceivedViewController.
Actual
When the app has finished launching(from user point of view, from application point of view it has been launched on background and moving to foreground), the modal had already been displayed.
We are quite sure that modal had finished displayed upon launch since view displayed on the zoom animation of our app launch(the iOS animation by apple that app's view zooms in to fill the device screen on app launch) shows the modal being displayed.
Something we are confused is that this does not happen all the time but rathe happens several times and then stopped happening(behaving as is should be) for several times. We checked this on same git commit.
Something we tried
We tried to simulate the problem by triggering modal display right after receiving UIApplication.didBecomeActiveNotification.
func callRewardAPI {
// Call didReceiveReward right away to show modal.
self.ouptut.didReceiveReward()
/*
self.rewardAPI.request { [weak self] result in
switch result {
case .success(let reward):
self.ouptut.didReceiveReward()
case .failure:
// error case
}
}
*/
}
Since it seems we cannot use Xcode debug breakpoint when simulating app launch from not running to background status, we set local push notification to each point of code to observer what is happening.
What we found out is that code to show modal is called after viewDidAppear. viewDidAppear is the last lifecycle method of the view controller when launching a view controller so we're stuck finding out the solution.
I have a VoIP app. It used to work without CallKit, so it has its own in-app call UI. I'm now integrating CallKit UI to handle calls which happen when the app is not running, and have a question:
What is the recommended way to detect a moment when the CallKit UI is dismissed and is going to switch to the in-app UI?
Currently I see that the CallKit UI disappears in 2 cases:
when the app process is not running, and the device is unlocked (e.g. on the home screen) and the call arrives, the CallKit UI appears at first, but if you accept the call (answer), then the CallKit UI disappears immediately, and the app appears.
when the app process is not running, and the device is locked, the call arrives, the CallKit UI appears, you accept the call (answer), and then press the app icon in the CallKit UI, then again the CallKit UI disappears, and the app appears.
What is the recommended way to detect these conditions in order to spawn the in-app UI?
The reason why I don't want to have the in-app UI started all the time is performance and logic. I think it would be wasteful to show and update the controls and video views (the app has video support) while the CallKit UI is on top of the screen. It would be more logical to me if I could detect that the UI switch is needed, and create a UI at that point.
I have looked over the CallKit API, but found nothing for that purpose.
For the second use case, I have logic in AppDelegate like this:
func applicationWillEnterForeground(_ application: UIApplication) {
maybeShowCallUi()
}
fileprivate func maybeShowCallUi() {
if CallManager.shared.calls.count > 0 {
print("One or more calls active.")
DispatchQueue.main.async() { [weak self] in
print("Trying to show in call UI.")
self?.showInCallViewController()
}
} else {
print("No calls active.")
}
}
My CallManager object is similar to the SpeakerBox sample code that Apple provided. It keeps track of all calls.
After the user Answers the call, app call screen should be visible. For that, inside the following method write your code to make app call screen visible.
- (void)provider:(CXProvider *)provider performAnswerCallAction:(nonnull CXAnswerCallAction *)action {
//show call screen here.
}
I am trying to record a UI test case and run it back, but I am running into this error. Basically, in my application when I land on the home screen, the user is asked to allow use of notification and location. After I record these events and try to segue to another VC, it records normally like so.
[app.alerts[#"\u201cSampleApp\u201d Would Like to Send You Notifications"].collectionViews.buttons[#"Don\u2019t Allow"] tap];
[app.alerts[#"Allow \u201cSampleApp\u201d to access your location while you use the app?"].collectionViews.buttons[#"Allow"] tap];
//segue to VC2
But when I play it back, it fails with the error in the title.
Did not receive view did disappear notification within 2.0s
I suspect that by the time the alerts are cleared, the segue button is already tapped and while it expects the home VC to disappear, it does not. Is this understanding right?. If yes, how can I delay the expectation, if no, please help.
System level alerts should be handled by addUIInterruptionMonitorWithDescription API here is the documentation from apple Link and sample code in swift below:
addUIInterruptionMonitorWithDescription("First Dialog") { (alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
XCUIApplication().tap()
I have a UI Test that dismisses an alert, checks to see if the alert had been dismissed (if not it attempts to dismiss it again) and then continues on with the rest of the test.
Most of the time the test passes just fine, but sometimes seemingly randomly (though more often in the simulator on a machine with older hardware), the test will do the following:
check for the alert
tap on the button that dismisses the alert
the alert is dismissed
check for the alert (and still find it - I have logged the query at this point and it is indeed finding the same alert that was just dismissed)
attempt to tap the button that dismisses the alert
complain that it can't find the alert and fail
A couple of notes:
I have not, as of yet, observed this behaviour when stepping through the test with the debugger.
Forcing the test to wait a bit after dismissing the alert (using things like usleep()) can still cause this behaviour.
Why is the query still finding the alert after it has been dismissed? Why is this behaviour seemingly random?
Have you tried interacting with the alert via a UI Interruption handler?
addUIInterruptionMonitorWithDescription("Alert") { (alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
Upon initial launch of my app, I get a permissions alert asking if I will allow the app to use my current location. My onAlert method successfully dismisses the alert on my device. When I run it on the simulator, it never gets called. Other internal alerts are handled by the onAlert method on the simulator. The permission alert coming from SpringBoard is not handled on the simulator. Any ideas?
UIATarget.onAlert = function onAlert(alert)
{
var title = alert.name();
UIALogger.logMessage(title);
return false;
}
This problem happens because the alert you're seeing comes from the system itself -- before the app actually launches and your automation environment is initialized.
To see this happen, add a debug line before the function definition for UIATarget.onAlert:
UIALogger.logDebug("Now setting up the alert function");
UIATarget.onAlert = function onAlert(alert) {}
Next, Reset Content and Settings... on your simulator and re-run your automation. You should notice that the debug line will not appear until after you manually dismiss the alert about using the current location.
I do not see how this would be fixable from javascript code. You have to delay the alert until the app has properly launched, or follow the example shown in this answer.
If default handler is not working for you, then you can simply use 'return true' instead of the 'return false' so that you can manually dismiss the popover.
Before 'return true' statement you can write some statement for tapping the button (dismiss button) you wish to.
I had the same problem with an app that presents an alert immediately after launching. When I logged the element tree, I could see the additional alert window, and I could let UIAutomation tap the OK button in the alert. But the alert handler was never called.
The reason was that the alert appeared before UIAutomation was set up properly to handle it. If I delayed the presentation of the alert, UIAutomation did catch it.