I'm getting started with TDD and got stuck testing a simple UITableViewController (using storyboards).
The tableView should have one row for every element in my model NSArray:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.sortedStories count];
}
My test for this is:
- (void)testTwoStoriesShouldLeadToTwoRowsInSectionZero
{
_sut.sortedStories = [self arrayWithTwoStories];
[_sut.tableView reloadData];
XCTAssertEqual([_sut tableView:_sut.tableView numberOfRowsInSection:0], 2, #"The number of rows should match the number of stories");
}
And I'm initializing my _sut in my test class from my storyboard like this:
- (void)setUp
{
[super setUp];
UIStoryboard *storyboard =
[UIStoryboard storyboardWithName:#"Main_iPhone"
bundle:nil];
_sut = [storyboard instantiateViewControllerWithIdentifier:
#"MyTableViewController"];
}
This works perfectly fine if there is no setup of the model in my production code. But after I added this default setup with seven stories in my UITableViewController's viewDidLoad method:
- (void)viewDidLoad
{
[super viewDidLoad];
self.sortedStories = [self sevenDefaultStories];
}
the test suddenly fails, stating that 'seven isn't equal to two'. The tableView seems not to reload the data, though I have the [_sut.tableView reloadData] in my test after changing the model.
What am I doing wrong?
I found it:
Though the tableViewController was instantiated via storyboard etc., the view had not been loaded yet!
So viewDidLoad also had not been called yet, when I 'changed' my model. But then, when I called [_sut.tableView reloadData] (which should make sure, the tableView would get synced with my model), this caused exactly the opposite: accessing the tableView made it necessary that the view got loaded, so NOW finally viewDidLoad was called on my tableViewController! And so my model got changed again, overwriting my nice test model with default values.
The solution was, to add [_sut view]; to the top of my test case. This makes sure that the view is already loaded when my test begins.
And, in my case, calling [_sut.tableView reloadData] wasn't necessary at all, since I'm only testing the dataSource method directly, not the actual tableView. N.B.:[_sut view] still needs to be called, because the dataSource method in my XCTAssertEqual() statement also triggers the view to be loaded, if that has not happened yet.
So the working test case looks like this:
- (void)testTwoStoriesShouldLeadToTwoRowsInSectionZero
{
[_sut view];
_sut.sortedStories = [self arrayWithTwoStories];
XCTAssertEqual([_sut tableView:_sut.tableView numberOfRowsInSection:0], 2, #"The number of rows should match the number of stories");
}
During test setup (my preferred solution):
[UIApplication sharedApplication].keyWindow.rootViewController = _sut; // Suck it UIKit
You may alternatively consider:
[[UIApplication sharedApplication].keyWindow addSubview:_sut.view]; // Hax
Note that UITableView may not render any cells unless it has been added to the view hierarchy. Similarly, UIViewController lifecycle methods may not be called unless it is added to the view controller hierarchy. I currently use "my preferred solution" above because it accomplishes both.
Related
I am currently writing unit tests for my React + MaterialUi application.
In my application I have a Dialog. I want to make sure depending on what button pressed on the dialog:
<FlatButton
label="Cancel"
secondary={true}
onTouchTap={this._cancelDialog.bind(this)}
/>
<FlatButton
label="Submit"
primary={true}
onTouchTap={this._confirmDialog.bind(this)}
/>
that the internal state changes accordingly.
Unfortunately i cannot get ahold of the dialog content using
TestUtils.scryRenderedComponentsWithType(FlatButton)
or
scryRenderedComponentsWithTag("button")
and so on.
Any ideas on how that flow can be tested?
Update 1
So I can get the Dialog instance by calling TestUtils.scryRenderedComponentsWithType(Dialog). But I can not get the dialogs content. DOM wise the content does not render inside the view itself. Its rendered in a new created node on document level (div). So i tried this:
let cancelButton = window.document.getElementsByTagName("button")[0];
Simulate.click(cancelButton);
cancelButton in the case above is the correct DOM element. Simulate.click however does not trigger the components click function.
regards
Jonas
just ran into the same problem. I looked into the source code, and the Dialog component's render method actually creates an instance of the component RenderToLayer. this component behaves as a portal and breaks react's DOM tree by returning null in its' render function and instead appending directly to the body.
Luckily, the RenderToLayer component accepts the prop render, which essentially allows the component to pass to the portal a function to be called when it is in a render cycle. This means that we can actually manually trigger this event ourselves. It's not perfect, i admit, but after a few days of poking around trying to find a solution for this hack i am throwing in the towel and writing my tests like this:
var component = TestUtils.renderIntoDocument(<UserInteractions.signupDialog show={true}/>)
var dialog = TestUtils.renderIntoDocument(component.refs.dialog.renderLayer())
var node = React.findDOMNode(dialog)
and here is what my UserInteractions.signupDialog looks like:
exports.signupDialog = React.createClass({
...
render: function() {
var self = this;
return (
<div>
<Dialog
ref='dialog'
title="Signup"
modal={false}
actions={[
<Button
label="Cancel"
secondary={true}
onTouchTap={self.__handleClose}
/>,
<Button
label="Submit"
primary={true}
keyboardFocused={true}
onTouchTap={self.__handleClose}
/>
]}
open={self.props.show}
onRequestClose={self.__handleClose}
>
<div className='tester'>ham</div>
<TextField id='tmp-email-input' hintText='email' type='text'/>
</Dialog>
</div>
)
}
})
Now i can make assertions against the child components rendered in the dialog box, and can even make assertions about events bound to my original component, as their relationship is maintained.
I definitely recommend setting up a debugger in your testing stack if you are going to continue using material ui. Theres not a lot of help for things like this. Heres what my debug script looks like:
// package.json
{
...
"scripts": {
"test": "mocha --compilers .:./test/utils/compiler.js test/**/*.spec.js",
"debug": "mocha debug --compilers .:./test/utils/compiler.js test/**/*.spec.js"
}
}
and now you can use npm test to run mocha tests, and npm run debug to enter debugger. Once in the debugger, it will immediately pause and wait for you to enter breakpoints. At this juncture, enter c to continue. Now you can place debugger; statements anywhere in your code to generate a breakpoint which the debugger will respond to. Once it has located your breakpoint, it will pause and allow you to engage your code using local scope. At this point, enter repl to enter your code's local scope and access your local vars.
Perhaps you didnt need a debugger, but maybe someone else will find this helpful. Good luck, happy coding!
Solved it as follows:
/*
* I want to verify that when i click on cancel button my showModal state is set * to false
*/
//shallow render my component having Dialog
const wrapper= shallow(<MyComponent store={store} />).dive();
//Set showModal state to true
wrapper.setState({showModal:true});
//find out cancel button with id 'cancelBtn' object from actions and call onTouchTap to mimic button click
wrapper.find('Dialog').props().actions.find((elem)=>(elem.props.id=='cancelBtn')).props.onTouchTap();
//verify that the showModal state is set to false
expect(wrapper.state('showModal')).toBe(false);
I ran into the same issue and solve it like that :
const myMock = jest.genMockFunction();
const matcherComponent = TestUtils.renderIntoDocument(
<MatcherComponent onClickCancel={myMock} activAction/>
);
const raisedButton = TestUtils.findRenderedComponentWithType(
matcherComponent, RaisedButton);
TestUtils.Simulate.click(ReactDOM.findDOMNode(raisedButton).firstChild);
expect(myMock).toBeCalled();
It works fine for me. However I'm still struggling with Simulate.change
Solution by avocadojesus is excellent. But I have one addition. If you try to apply this solution and get an error:
ERROR: 'Warning: Failed context type: The context muiTheme is marked
as required in DialogInline, but its value is undefined.
You should modify his the code as follows:
var component = TestUtils.renderIntoDocument(
<MuiThemeProvider muiTheme={getMuiTheme()}>
<UserInteractions.signupDialog show={true}/>
</MuiThemeProvider>
);
var dialogComponent = TestUtils.findRenderedComponentWithType(component, UserInteractions.signupDialog);
var dialog = TestUtils.renderIntoDocument(
<MuiThemeProvider muiTheme={getMuiTheme()}>
{dialogComponent.refs.dialog.renderLayer()}
</MuiThemeProvider>
);
var node = React.findDOMNode(dialog);
Material UI fork the 2 enzyme methods. You need to use the createMount or the createShallow with dive option https://material-ui.com/guides/testing/#createmount-options-mount
On a table row click I reference a cell to present a view controller (to select from a list of images)
def open_selector
view_b = ImagesController.new #using rmq hence .new
##cell.superview.superview.controller.presentViewController view_b, animated:true, completion:nil
end
Inside the images controller - I dismiss when finished selecting - but how do I let cell know it was closed?
def collectionView(view, didSelectItemAtIndexPath: index_path)
self.dismissViewControllerAnimated(true, completion: lambda{})
end
I would suggest providing your UICollectionViewController a delegate so it can call back itself. So:
class MyCollectionViewController < UICollectionViewController
attr_writer :parent_controller
# ...
def collectionView(view, didSelectItemAtIndexPath: index_path)
self.dismissViewControllerAnimated(true,
completion: lambda{
#parent_controller.collection_did_close(self)
})
end
Assuming, you have a method called collection_did_close in the parent controller, it will be called with a reference to the collection view controller. Using that you can grab whatever information you need out of there before it gets garbage collected.
I'm experiencing this weird behaviour once I initiate an application root controller with a UINavigationController
On first launch, there is an empty space between the navigationbar and first viewcontroller controller.
but the full content is displayed after I swtiched to another view and back to the first one.
Is something wrong with this?
tab_bar_controller = RootViewController.alloc.initWithNibName(nil, bundle:nil)
#window.rootViewController = UINavigationController.alloc.initWithRootViewController(tab_bar_controller)
Thanks for your help.
It is not considered "proper" to put a UITabBarController inside a UINavigationController:
UINavigationController#initWithRootViewController ... rootViewController:
The view controller that resides at the bottom of the navigation stack.
This object cannot be an instance of the UITabBarController class.
The opposite - a UINavigationController as one of the UITabBarController child view controllers - is allowed.
nav_controller = RootViewController.alloc.initWithNibName(nil, bundle:nil)
#window.rootViewController = UITabBarController.alloc.init
#window.rootViewController.viewControllers = [nav_controller]
Even if you did get this figured out, your app would ultimately be rejected.
Try placing the content in the viewWillAppear callback.
I am overriding the UIAlertView class with something similar seen in this tutorial
http://mobile.tutsplus.com/tutorials/iphone/ios-sdk-uialertview-custom-graphics/
So I am overriding the drawRect...
My problem is not actually customizing the alert itself, but rather the view background color behind the UIAlertView itself right now it is the standard black gradient that all UIAlertViews have... Any ideas on how to do this?
I took this approach and seems to work... In layoutSubview I am looping over the superviews subviews
for (UIView*superSub in self.superview.subviews) {
if ([superSub isMemberOfClass:[UIImageView class]]) {
superSub.backgroundColor = [UIColor colorWithRed:30/255. green:21/255. blue:4/255. alpha:1];
superSub.alpha = 0.77;
}
}
Xcode - version 4.2
Target Platform - iPhone
I have a TabBarController , containing 3 tabs. All tabs contain TableViewController wired to the TabBarController via NavigationController. All TableViewController shows data from external URL, and as well have their corresponding DetailViews.
The application works all fine.
I'm unable to add an 'UIActivityIndicatorView" while the data on the TableView is being loaded. The 'storyboard' neither allows me to add the 'UIActivityIndicatorView' control on the TableViewController or UINavigationController.
Note :
I understand placing UIActivityIndicatorView directly on a UIViewController.
I dont want the UIActivityIndicatorView 'loading' for the cells in the TableView.
Thanks,
Sriram
I'm trying to do the same with Storyboard but it seems that you should bette do it programmatically :
In your ViewController.h :
#property (strong) UIActivityIndicatorView *activityIndicator;
In viewDidLoad :
// init with desired state
activityIndicator = [[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
// set the color
activityIndicator.color = [UIColor blackColor];
activityIndicator.frame = CGRectMake(0.0, 0.0, 40.0, 40.0);
// set the position
activityIndicator.center = CGPointMake(self.view.bounds.size.width/2,
self.view.bounds.size.height/4);
If you have to handle orientation change, think to update the position in viewWillAppear and willRotateToInterfaceOrientation
I think I'll loose less time to code that than time to find a better way to work with Storyboards