Check if UIAlertController is Presented in an XCTest Case - ios

I'm writing unit tests for an application and want to check if a UIAlertController is presented in a specific scenario.
-(void)testBadLogin {
// enter username and password in UITextFields
self.viewController.usernameField.text = #"test#test.com";
self.viewController.passwordField.text = #"incorrect_pass";
[loginButton sendActionsForControlEvents: UIControlEventTouchUpInside];
// this isn't right
XCTAssertNotNil([self.viewController alertController], #"alertController should appear");
}
How do I check if a UIAlertController has been presented on top of the current view?

"XCTest is not meant to be used to test UI components." is not truly accurate. I am using XCTest for almost every UI testing and it works just fine. The correct answer shall be "Mocking".
I would use OCMock for mocking the tested view controller and "verify" that the method presentViewController... is called with the alert controller. This is a neat solution and works just fine. (You can even ignore that the alert controller is passed to this method and just test that the view controller has been passed the method presentViewController...)

It can also be done in this way:
Let's say we have a button which when tapped shows view controller:
- (void) didTapButton
{
UIAlertController* c = [UIAlertController alertControllerWithTitle:#"Title" message:#"Message"
preferredStyle:UIAlertControllerStyleAlert];
[self presentViewController:c animated:ANIMATED completion:nil];
}
Notice that ANIMATED parameter is not 'YES' or 'NO'. It is define in PrefixHeader as:
#define ANIMATED (getenv("runningTests") == NULL)
and runningTests is environment variable defined in Test target.
We don't want animation when executing unit/integration tests.
The test method looks like:
- (void) testButtonWillShowAlertView
{
UIApplication.sharedApplication.delegate.window.rootViewController = controller;
[controller.button sendActionsForControlEvents:UIControlEventTouchUpInside];
XCTAssertEqualObjects(controller.presentedViewController.class, UIAlertController.class);
}
Important line is
UIApplication.sharedApplication.delegate.window.rootViewController = controller;
Apparentely, rootViewController on UIWindow has to be set.

I have written a wrapper around UIAlertController for easier unit testing.
You can check if it is visible
XCTAssert(testableAlert.visible)
And you can also execute its actions
testableAlert.simulateAction("OK")
https://github.com/exchangegroup/TestableAlert

you can simply check the existence for UIAlertController By below code(objective c).
XCTAssertFalse(app.alerts.element.staticTexts[#"your alert message"].exists);
it will fail the test if alert is not presented, otherwise you can use
app.alerts.element.staticTexts[#"your alert message"].exists
with if or XCTAssertTrue .

XCTest is not meant to be used to test UI components.
Use Apple's UIAutomation JavaScript Library for this:
https://developer.apple.com/library/ios/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/UsingtheAutomationInstrument/UsingtheAutomationInstrument.html#//apple_ref/doc/uid/TP40004652-CH20
Docs for testing Alerts:
https://developer.apple.com/library/ios/documentation/ToolsLanguages/Reference/UIAAlertClassReference/index.html#//apple_ref/doc/uid/TP40009898

Related

How to know if my keywindow.rootViewController is an Alert?

I'm having a problem to find out if my keywindow.rootViewController is an UIAlertController object. This must be a very simple thing to do, but I don't know what is wrong with my code:
UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
if( [rootViewController isKindOfClass:[UIAlertController class]]){
// Do something
}
Why I never enter in that IF STATEMENT, even when I can see on the debug that the view of this controller is an _UIAlertControllerView*? There is any other way to check if my key window is an Alert?
Thanks.
I assume what you want to do is be able to check if an alert is showing from any view controller in your application, but you're not sure which controller is displaying the alert and don't have access to the alert itself.
I've had to deal with this a few times. In previous versions of iOS you were able to iterate through all the subviews of a window to check if an UIAlertView was on the top, but with the changes to alerts in iOS8 this not longer works because all the delegates are deprecated and apple now recommends you use UIAlertController instead of UIAlertView. In any case, all the techniques were dependent on using certain versions of iOS and I've found them to be extremely unreliable.
What I use now is a singleton that keeps track of how many alerts are displaying. The singleton has a method that returns the current number of alerts displaying, a method for adding one, and a method for subtracting one.
This is implemented by adding one to the singleton when you present the UIAlertController:
[self presentViewController:alert animated:YES completion:^(){
AlertSingleton *muhInstance = [AlertSingleton sharedInstance];
[muhInstance addOne];
//Anything else for completion
}];
And then subtracting one with every possible action choice you add to the alert like this:
UIAlertAction *myAction = [UIAlertAction actionWithTitle:#"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
AlertSingleton *muhInstance = [AlertSingleton sharedInstance];
[muhInstance removeOne];
//Any Other alert actions
}];
Now you can know anywhere in your application if an alert is showing by checking if the count in the singleton is greater than zero like this:
if ([[AlertSingleton sharedInstance] alertCount] > 0) {
//There is an alert showing
//Your code here
}
I've found this technique to be very reliable for tracking alerts.

UIAlertView after MOC background save works iOS 7 not iOS 8

I have a parent detail view controller that provides common custom functions to child detail views.
The parent includes two custom functions.
One function triggers a background save to two NSManagedObjectContexts that saves a main MOC immediately to free up UI, then saves a private MOC. Fairly standard setup pre iOS 8.
The following function presents a UIAlertView to confirm the save was successful. This includes code to automatically dismiss after a set amount of time (about half a second).
This all works fine running iOS 7, both on device and on simulator.
This causes a crash when running iOS 8, both on device and on simulator.
The problem exists for only one of the five child detail views. Following a detailed side by side comparison I confirm that these each have identical code blocks and methods.
I have break points inserted into the two custom functions. The save works fine, but the code crashes after trying to present the UIAlertView, as mentioned only when running iOS 8. The debugger steps into machine code that I do not understand. The attempted save does not persist.
If I comment out the alert view, the save persists, but obviously I no longer have the alert view for the user.
Any suggestions?
UPDATE
Think I may have found a solution... of sorts... not definite yet...
UIAlertController and this article by NSHipster
Augmenting the UIAlertView code in my parent detail view controller resolves the problem.
Within my message method I now check whether iOS responds to the UIAlertController class, and if it does instantiate a UIAlertController, otherwise instantiate a UIAlertView.
- (void)message {
...other code...
if ([UIAlertController class]) { //checking whether iOS responds to the UIAlertController class
UIAlertController *alert = [UIAlertController alertControllerWithTitle:titleComplete
message:messageComplete
preferredStyle:UIAlertControllerStyleAlert];
[self presentViewController:alert animated:YES completion: ^(void){
[self performSelector:(#selector(dismissAlertController:)) withObject:alert afterDelay:durationMessageCompleteSave];
}];
// [self dismissViewControllerAnimated:YES completion:nil];
// display time on screen too short using just the dismissViewController above,
// so add into completion handler in call to presentViewController...
} else {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:titleComplete
message:messageComplete
delegate:self
cancelButtonTitle:nil//self.localisedAlertButtonRemain
otherButtonTitles:nil];//buttonOther, nil];
[alertView setTag:010];
[alertView show];
[self performSelector:(#selector(dismissAlertView:)) withObject:alertView afterDelay:durationMessageCompleteSave];
}
}
...
- (void)dismissAlertView:(UIAlertView *)alert {
[alert dismissWithClickedButtonIndex:0 animated:NO];
}
- (void)dismissAlertController:(UIAlertController *)alert {
[alert dismissViewControllerAnimated:YES completion:nil];
}

Test view title using XCTest

I am using XCtest to test the title of a view. Trying to get into the habit of writing tests first. Setup looks like
- (void)setUp
{
[super setUp];
self.appDelegate = [[UIApplication sharedApplication] delegate];
self.tipViewController = self.appDelegate.tipViewController;
self.tipView = self.tipViewController.view;
self.settingsViewController = self.appDelegate.settingsViewController;
self.settingsView = self.settingsViewController.view;
}
The problem is "settingsViewController". I have two functions for the actual test:
- (void) testTitleOfMainView{
XCTAssertTrue([self.tipViewController.title isEqualToString:#"Tip Calculator"], #"The title should be Tip Calculator");
//why does this not work?
// XCTAssertEqual(self.tipViewController.title, #"Tip Calculator", #"The title should be Tip Calculator");
}
- (void) testTitleOfSettingsView{
//make the setttings view visible
[self.tipViewController onSettingsButton];
//test the title
XCTAssertTrue([self.settingsViewController.title isEqualToString:#"Settings"], #"The title should be Settings");
}
The "testTitleOfMainView" works. But the "testTitleOfSettingsView fails as self.settingsViewController is nil. I can sort of understand why. The view has not been initialized as yet. So I tried sending the message to the main controller that brings the settignscontroller in view
[self.tipViewController onSettingsButton];
The settingsController is still nil. Should I be using mocks? Somebody suggested this for my other question
xctest - how to test if a new view loads on a button press
Should I subclass the settingsview and bring it up manually? Thank you.
Stay away from actually loading views in a real navigation stack. Real UI interactions typically need the run loop to receive events, so they will not work in a fast unit test. So throw away your setUp code.
Instead, instantiate the view controller on its own, and have it load:
- (void)testTitleOfSettingsView
{
SettingsViewController *sut = [[SettingsViewController alloc] init];
[sut view]; // Accessing the view causes it to load
XCTAssertEquals(#"Settings", sut.title);
}
Also, learn the various assertions that are available to you in XCTest, not just XCAssertTrue. Avoid comments in those assertions; a single assertion in a small test should speak for itself.

UIActivity activityViewController not dismissing on iPad

I have a UIActivity subclass that creates its own activityViewController:
- (UIViewController *)activityViewController {
WSLInProgressViewController* progressView = [[[WSLInProgressViewController alloc] init] autorelease];
progressView.message = [NSString stringWithFormat:NSLocalizedString(#"Posting to %#...",#"Posting to..."),
self.activityType];
return progressView;
}
I've add a full repro on GitHub.
According to the documentation, you aren't supposed to dismiss this manually. Instead, the OS does that when you call activityDidFinish:. This works fine when ran on an iPhone.
When I say "works," this is the sequence of events that I'm expecting (and see on the iPhone):
Display the UIActivityViewController
User presses my custom activity
My view controller appears
I call activityDidFinish:
My custom view controller is dismissed
The UIActivityViewController is also dismissed
However, when I run this same code on the iPad Simulator -- the only difference being that I put the UIActivityViewController in a popup, as the documentation says you should -- the activityViewController never dismisses.
As I say, this is code wo/the popUP works on the iPhone and I have stepped through the code so I know that activityDidFinish: is getting called.
I found this Radar talking about the same problem in iOS6 beta 3, but it seems such fundamental functionality that I suspect a bug in my code rather than OS (also note that it works correctly with the Twitter and Facebook functionality!).
Am I missing something? Do I need to do something special in the activityViewController when it's run in a UIPopoverViewController? Is the "flow" supposed to be different on the iPad?
The automatic dismissal only appears to happen when your 'activity' controller is directly presented, not wrapped in anything. So just before showing the popup it's wrapped in, add a completion handler
activity.completionHandler = ^(NSString *activityType, BOOL completed){
[self.popup dismissPopoverAnimated:YES];
};
and you'll be good.
I see the question is quite old, but we've been debugging the same view-controller-not-dismissing issue here and I hope my answer will provide some additional details and a better solution than calling up -dismissPopoverAnimated: manually.
The documentation on the UIActivity is quite sparse and while it hints on the way an implementation should be structured, the question shows it's not so obvious as it could be.
The first thing you should notice is the documentation states you should not be dismissing the view controller manually in anyway. This actually holds true.
What the documentation doesn't say, and what comes as an observable thing when you come across debugging the non-dissmissing-view-controller issue, is iOS will call your -activityViewController method when it needs a reference to the subject view controller. As it turns out, probably only on iPad, iOS doesn't actually store the returned view controller instance anywhere in it's structures and then, when it wants to dismiss the view controller, it merely asks your -activityViewController for the object and then dismisses it. The view controller instantiated in the first call to the method (when it was shown) is thus never dismissed. Ouch. This is the cause of the issue.
How do we properly fix this?
Skimming the UIActivity docs further one may stumble accross the -prepareWithActivityItems: method. The particular hint lies along the following text:
If the implementation of your service requires displaying additional UI to the user, you can use this method to prepare your view controller object and make it available from the activityViewController method.
So, the idea is to instantiate your view controller in the -prepareWithActivityItems: method and tackle it into an instance variable. Then merely return the same instance from your -activityViewController method.
Given this, the view controller will be properly hidden after you call the -activityDidFinish: method w/o any further manual intervention.
Bingo.
NB! Digging this a bit further, the -prepareWithActivityItems: should not instantiate a new view controller each time it's called. If you have previously created one, you should merely re-use it. In our case it happily crashed if we didn't.
I hope this helps someone. :)
I had the same problem. It solved for me saving activityViewController as member and return stored controller. Activity return new object and dismiss invoked on new one.
- (UIViewController *)activityViewController {
if (!self.detaisController) {
// create detailsController
}
return self.detailsController;
}
I pass through the UIActivity to another view then call the following...
[myActivity activityDidFinish:YES];
This works on my device as well as in the simulator. Make sure you're not overriding the activityDidFinish method in your UIActivity .m file as I was doing previously. You can see the code i'm using here.
a workaround is to ask the calling ViewController to perform segue to your destination ViewController via - (void)performActivity although Apple does not recommend to do so.
For example:
- (void)performActivity
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
[self.delegate performSomething]; // (delegate is the calling VC)
[self activityDidFinish: YES];
}
}
- (UIViewController *)activityViewController
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
UIViewController* vc=XXX;
return vc;
}
else
{
return nil;
}
}
Do you use storyboards? Maybe in your iPad storyboard, the UIActivityIndicatorView doesn't have a check on "Hides When Stopped"?
Hope it helps!
So I had the same problem, I had a custom UIActivity with a custom activityViewController and when it was presented modally it would not dismiss not matter what I tried. The work around I choose to go with so that the experience remained the same to the user was to still use a custom UIActivity but give that activity a delegate. So in my UIActiviy subclass I have the following:
- (void)performActivity
{
if ([self.delegate respondsToSelector:#selector(showViewController)]) {
[self.delegate showViewController];
}
[self activityDidFinish:YES];
}
- (UIViewController *)activityViewController
{
return nil;
}
Then I make the view controller that shows the UIActivityViewController the delegate and it shows the view controller that you would otherwise show in activityViewController in the delegate method.
what about releasing at the end? Using non-arc project!
[progressView release];
Many Users have the same problem as u do! Another solution is:
UIActivityIndicatorView *progress= [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(125, 50, 30, 30)];
progress.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;
[alert addSubview:progress];
[progress startAnimating];
If you are using storyboard be sure that when u click on the activityind. "Hides When Stopped" is clicked!
Hope that helped...

Return to root view in IOS

To some this may sound like a daft question. I've searched around, and found little, mostly because I cannot find the appropriate search terms.
Here what I want to do is:
The application begins at view A.
View A launches view B, and view B launches view C.
Is their a way for view C to return directly back to A without dismissing itself and thus exposing B. For example a main menu button.
You can call popToRootViewControllerAnimated: if you have a UINavigationController. If you specify NO to animate it, then it will just jump back to the root without showing B first.
I have discovered a solution to my problem. Its a bit dirty, (and I''ll probably get shot down in flames for it) but works very well under tests and is very quick to implement. Here's how I did it.
In my app I have a Singleton class called GlobalVars (I use this for storing various global settings). This class holds a boolean called home_pressed and associated accessors (via synthesise). You could also store this value in the application delegate if you wish.
In every view controller with a main menu button, I wire the button to the homePressed IBAction method as follows. First setting the global homePressed boolean to YES, then dismissing the view controller in the usual way, but with NO animation.
-(IBAction) homePressed: (id) sender
{
[GlobalVars _instance].homePressed = YES;
[self dismissModalViewControllerAnimated: NO];
}//end homePressed
In every view controller except the main menu I implement the viewDidAppear method (which gets called when a view re-appears) as follows.
-(void) viewDidAppear: (Bool) animated
{
if ([GlobalVars _instance].homePressed == YES)
{
[self dismissModalViewController: NO];
}
else
{
//put normal view did appear code here/
}
}//end viewDidAppead
In the mainMenu view controller which is the root of the app, I set the global homePressed boolean to NO in its view did appear method as follows
-(void) viewDidAppear: (Bool) animated
{
if ([GlobalVars _instance].homePressed == YES)
{
[GlobalVars _instance].homePressed == NO;
}
else
{
//put normal view did appear code here/
}
}//end viewDidAppear
There, this enables me to go back to the root main menu of my app from any view further down the chain.
I was hoping to avoid this method, but its better than re-implementing my app which is what I'd have to do if I wanted use the UINavigationController solution.
Simple, took me 10 minutes to code in my 9 view app. :)
One final question I do have, would my solution be OK with the HIG?

Resources