Reputation: 95
Trying out some BDD with AngularJS so I'm having a go at automating scenarios with Protractor and CucumberJS. Strangely, It been the devils job trying to get step defintions to fail intelligently.
Features.feature
Feature: Calculator
As a user
I want to perform arithmetic operations
So that I don't have to think too hard
Scenario: Addition
Given I have opened the calculator application
When I add 2 and 2
Then the result 4 should be displayed
Steps.js
module.exports = function() {
this.Given(/^I have opened the calculator application$/, function (callback) {
//load protractor config baseurl
browser.get('').then(
callback());
});
this.When(/^I add (\d+) and (\d+)$/, function (arg1, arg2, callback) {
//enter numbers to be added
element(by.model('firstNumber')).sendKeys(arg1);
element(by.model('secondNumber')).sendKeys(arg2);
//select mathematical operator from dropdown list
element(by.css('select')).click();
element(by.css('select option[value="0"]')).click();
//hit the calculate button
element(by.buttonText('=')).click();
callback();
});
this.Then(/^the result (\d+) should be displayed$/, function (arg1, callback) {
element(by.binding('result')).getText()
.then(function(result){
result === arg1 ? callback() : callback.fail();
});
});
};
Index.html
<!doctype html>
<html class="no-js">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body ng-app="calculator" ng-controller="MainCtrl">
<input ng-model="firstNumber">
<select ng-model="selectedOperation" ng-options="op as op.value for op in operations"></select>
<input ng-model="secondNumber">
<button ng-click="Calculate()">=</button>
<span ng-bind="result"></span>
<script src="bower_components/angular/angular.js"></script>
<script src="scripts/app.js"></script>
</body>
</html>
App.js
angular
.module('calculator', [])
.controller('MainCtrl', function ($scope) {
$scope.operations = [
{ label: 'Add', value: '+' },
{ label: 'Subtract', value: '-' }
];
$scope.selectedOperation = $scope.operations[0];
$scope.Calculate = function(){
switch($scope.selectedOperation.label) {
case 'Add':
var result = Number($scope.firstNumber) + Number($scope.secondNumber);
break;
case 'Subtract':
var result = Number($scope.firstNumber) - Number($scope.secondNumber);
break;
};
$scope.result = result !== NaN || result === 0 ? result : 'Boo! bad input!';
};
});
Protractor output:
1 scenario (1 passed) 3 steps (3 passed)
The setup above works fine. Protractor gives the correct output and I can fail the scenario by evaluating incorrect outcomes in the Then() step. Seems nice.
First problem I see is when I try to make the When step fail. For example using the same setup above but attempting to locate a nonexistent element.
this.When(/^I add (\d+) and (\d+)$/, function (arg1, arg2, callback) {
//enter numbers to be added. Sabotage edition!
element(by.model('AintNoGood')).sendKeys(arg1);
element(by.model('secondNumber')).sendKeys(arg2);
//select mathematical operator from dropdown list
element(by.css('select')).click();
element(by.css('select option[value="0"]')).click();
//hit the calculate button
element(by.buttonText('=')).click();
callback();
});
Protractor output: NoSuchElementError: No element found using locator: by.model("AintNoGood") ... 1 scenario (1 failed) 3 steps (1 failed, 2 passed)
The 2nd step fails correctly. I'm under the impression that when a step fail all subsequent steps are skipped, but protractor continues on to the 3rd step which passes anyway.
Stranger still...I empty the HTML. BDD test-first and all.
Inmdex.html
<!doctype html>
<html class="no-js">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body ng-app="calculator" ng-controller="MainCtrl">
<!--Ghost town here-->
<script src="bower_components/angular/angular.js"></script>
<script src="scripts/app.js"></script>
</body>
</html>
Assuming I was going through the scenario one step at a time, I write the defintion for the 2nd step assuming it will fail.
module.exports = function() {
this.Given(/^I have opened the calculator application$/, function (callback) {
//load protractor config baseurl
browser.get('').then(
callback());
});
this.When(/^I add (\d+) and (\d+)$/, function (arg1, arg2, callback) {
//enter numbers to be added
element(by.model('firstNumber')).sendKeys(arg1);
element(by.model('secondNumber')).sendKeys(arg2);
//select mathematical operator from dropdown list
element(by.css('select')).click();
element(by.css('select option[value="0"]')).click();
//hit the calculate button
element(by.buttonText('=')).click();
callback();
});
this.Then(/^the result (\d+) should be displayed$/, function (arg1, callback) {
callback.pending();
});
};
Protractor output: 1 scenario (1 pending) 3 steps (1 pending, 2 passed)
So the 2nd step passes. Clearly it should not when there are none of the elements it it supposed to be locating in the html.
Questions:
Any idea what's going on here?
If not, before I spend more time trying to make sense of it, I'm wondering if anyone has had success using Protractor with CucumberJS?
Upvotes: 2
Views: 1407
Reputation: 25177
This:
browser.get('').then(
callback());
});
should be:
browser.get('').then(callback);
As it is, you're invoking the callback
immediately and passing whatever it returns as the argument to the then
.
In your first this.When
you end with:
callback();
But the element()
call chains preceding it do not block. They simply schedule operations to be done on the WebDriver control flow, so I suspect this "callback()" will get invoked almost immediately. You can fix this by doing:
element(by.buttonText('=')).click().then(callback);
to get the callback scheduled in the controlflow.
The WebDriver control flow is counter-intuitive and often obtuse, so you'll want to read https://github.com/angular/protractor/blob/master/docs/control-flow.md and https://github.com/SeleniumHQ/selenium/wiki/WebDriverJs#control-flows.
Upvotes: 2