I'm running E2E tests against an AngularJS site, using Karma and angular-scenario.
I'm executing some login code in a beforeEach function before every it block.
My login function has a timeout delay in it to ensure that the login completes correctly. This is time-consuming and inefficient (not to mention inelegant). In addition, the user would only login once during a session, so this would more accurately model my scenario.
What I'm looking for is a before function that executes the login only once for a collection of it blocks contained within a describe block, but this facility doesn't seem to exist (I've checked the docs and the source code).
Seems like an obvious requirement for a testing library! Has anybody solved this problem?
could you use a variable flag? for example:
var bdone = false;
describe('Search POC', function() {
beforeEach(function() {
if (!bdone) {
browser().navigateTo('login');
console.log('navigated once');
bdone = true;
}
});
it ('should have an img link on the login results', function() {
expect(element('a:last').html()).toMatch(/jpg/);
});
it ('should redirect to user details when clicked', function() {
element('#UserThumbImage:first').click();
expect(browser().window().href()).toMatch(/user/);
});
});
Related
I am struggling with finding a way to test my angularjs application that has a non-angular login page.
All of the app's pages are protected by a login, so whatever page I test, I will need to login first.
I have looked through all kinds of ideas on the internet and nothing seems to work - it just doesn't redirect me to the app after logging in with protractor, while all works great when I do it manually.
This is what I have at the moment:
onPrepare: function() {
browser.driver.get('http://localhost:9000/app);
browser.driver.findElement(by.id('userName')).sendKeys('admin');
browser.driver.findElement(by.id('password')).sendKeys('pass123');
browser.driver.findElement(by.id('loginBtn')).click();
return browser.driver.wait(function() {
return browser.driver.getCurrentUrl().then(function(url) {
return /home/.test(url);
});
}, 10000);
I have also tried with setting browser.ignoreSynchronization = true; but still got nowhere.
Does anybody know what else I can try?
Set ignoreSynchronization=true before browser.get(), and reset to false after click login button.
onPrepare: function() {
browser.ignoreSynchronization = true;
browser.driver.get('http://localhost:9000/app);
browser.driver.findElement(by.id('userName')).sendKeys('admin');
browser.driver.findElement(by.id('password')).sendKeys('pass123');
browser.driver.findElement(by.id('loginBtn')).click();
browser.ignoreSynchronization = false;
return browser.driver.wait(function() {
return browser.driver.getCurrentUrl().then(function(url) {
return /home/.test(url);
});
}, 10000);
if above code not fix your issue, try to move above code for login into a function and don't call the function in onPrepare, call the function like in beforeAll. I had some fail experience start to interact with browser inside onPrepare
I am attempting to write simple styling tests on an angular page using Protractor, and the page I am testing can only be properly viewed after completed a Facebook login.
Problem appears to be that the tests continue to run while the attempt to log in is still actively happening, and so tests fail because the log in has not actually happened yet and the styles have not properly loaded.
Cannot (under current understanding) use an onPrepare(), because this functionality does not occur in the majority of test pages.
Tried using beforeAll(), and even putting the functionality into it's own 'it', same issue occurs.
Problem is not with switching windows.
Current state-
it('...', function() {
facebookbutton.click();
browser.getAllWindowHandles().then(function(handles){
browser.ignoreSynchronization = true;
browser.switchTo().window(handles[1]);
facebookemail.sendKeys('user');
facebookpass.sendKeys('pass');
facebooklogin.click();
browser.ignoreSynchronization = false;
browser.switchTo().window(handles[0]);
});
});
describe('Styling Tests --', function() { ...
Write subsequent test logic in a callback function after clicking facebook login button. it will work.
Code Snippet:
it('...', function() {
facebookbutton.click();
browser.getAllWindowHandles().then(function(handles){
browser.ignoreSynchronization = true;
browser.switchTo().window(handles[1]);
facebookemail.sendKeys('user');
facebookpass.sendKeys('pass');
/*use callback function after clicking login button*/
facebooklogin.click().then(function(){
browser.ignoreSynchronization = false;
browser.switchTo().window(handles[0]);
});
});
});
I am using jasmine data provider to do data driven testing..I have the following code. The code executes but however, it enters undefined infront of the user names.. the same code works when I don't use any data provider and sendKeys("username"). Please advise. I have the need to use data driven. For my rest of my tests I have implemented parallel test Method.
describe('data driven tests', function() {
var EC = protractor.ExpectedConditions;
var using = require('jasmine-data-provider');
using([{val1: 'testUser1#1234.com'}, {val2: 'testUser2#1234.com'}], function (value)//changed here {
it('Should click on ', function() {
browser.get('/#/');
element(by.name('username')).sendKeys(value.val1, value.val2);
element(by.name('password')).clear().sendKeys('password');
element(by.id('login')).click();;
});
});
});
You can use the code below:
browser.executeScript("var editor = $('.CodeMirror')[0].CodeMirror;editor.setValue('{');");
I'm trying to test the login page on my site using protractor.
If you log in incorrectly, the site displays a "toast" message that pops up for 5 seconds, then disappears (using $timeout).
I'm using the following test:
describe('[login]', ()->
it('should show a toast with an error if the password is wrong', ()->
username = element(select.model("user.username"))
password = element(select.model("user.password"))
loginButton = $('button[type=\'submit\']')
toast = $('.toaster')
# Verify that the toast isn't visible yet
expect(toast.isDisplayed()).toBe(false)
username.sendKeys("admin")
password.sendKeys("wrongpassword")
loginButton.click().then(()->
# Verify that toast appears and contains an error
toastMessage = $('.toast-message')
expect(toast.isDisplayed()).toBe(true)
expect(toastMessage.getText()).toBe("Invalid password")
)
)
)
The relevant markup (jade) is below:
.toaster(ng-show="messages.length")
.toast-message(ng-repeat="message in messages") {{message.body}}
The problem is the toastMessage test is failing (it can't find the element). It seems to be waiting for the toast to disappear and then running the test.
I've also tried putting the toastMessage test outside the then() callback (I think this is pretty much redundant anyway), but I get the exact same behaviour.
My best guess is that protractor sees that there's a $timeout running, and waits for it to finish before running the next test (ref protractor control flow). How would I get around this and make sure the test runs during the timeout?
Update:
Following the suggestion below, I used browser.wait() to wait for the toast to be visible, then tried to run the test when the promise resolved. It didn't work.
console.log "clicking button"
loginButton.click()
browser.wait((()-> toast.isDisplayed()),20000, "never visible").then(()->
console.log "looking for message"
toastMessage = $('.toaster')
expect(toastMessage.getText()).toBe("Invalid password")
)
The console.log statements let me see what's going on. This is the series of events, the [] are what I see happening in the browser.
clicking button
[toast appears]
[5 sec pass]
[toast disappears]
looking for message
[test fails]
For added clarity on what is going on with the toaster: I have a service which essentially holds an array of messages. The toast directive is always on the page (template is the jade above), and watches the messages in the toast service. If there is a new message, it runs the following code:
scope.messages.push(newMessage)
# set a timeout to remove it afterwards.
$timeout(
()->
scope.messages.splice(0,1)
,
5000
)
This pushes the message into the messages array on the scope for 5 seconds, which is what makes the toast appear (via ng-show="messages.length").
Why is protractor waiting for the toast's $timeout to expire before moving on to the tests?
I hacked around this using the below code block. I had a notification bar from a 3rd party node package (ng-notifications-bar) that used $timeout instead of $interval, but needed to expect that the error text was a certain value. I put used a short sleep() to allow the notification bar animation to appear, switched ignoreSynchronization to true so Protractor wouldn't wait for the $timeout to end, set my expect(), and switched the ignoreSynchronization back to false so Protractor can continue the test within regular AngularJS cadence. I know the sleeps aren't ideal, but they are very short.
browser.sleep(500);
browser.ignoreSynchronization = true;
expect(page.notification.getText()).toContain('The card was declined.');
browser.sleep(500);
browser.ignoreSynchronization = false;
It turns out that this is known behaviour for protractor. I think it should be a bug, but at the moment the issue is closed.
The workaround is to use $interval instead of $timeout, setting the third argument to 1 so it only gets called once.
you should wait for your toast displayed then do other steps
browser.wait(function() {
return $('.toaster').isDisplayed();
}, 20000);
In case anyone is still interested, this code works for me with no hacks to $timeout or $interval or Toast. The idea is to use the promises of click() and wait() to turn on and off synchronization. Click whatever to get to the page with the toast message, and immediately turn off sync, wait for the toast message, then dismiss it and then turn back on sync (INSIDE the promise).
element(by.id('createFoo')).click().then(function () {
browser.wait(EC.stalenessOf(element(by.id('createFoo'))), TIMEOUT);
browser.ignoreSynchronization = true;
browser.wait(EC.visibilityOf(element(by.id('toastClose'))), TIMEOUT).then(function () {
element(by.id('toastClose')).click();
browser.ignoreSynchronization = false;
})
});
I hope this can help who has some trouble with protractor, jasmine, angular and ngToast.
I create a CommonPage to handle Toast in every pages without duplicate code.
For example:
var CommonPage = require('./pages/common-page');
var commonPage = new CommonPage();
decribe('Test toast', function(){
it('should add new product', function () {
browser.setLocation("/products/new").then(function () {
element(by.model("product.name")).sendKeys("Some name");
var btnSave = element(by.css("div.head a.btn-save"));
browser.wait(EC.elementToBeClickable(btnSave, 5000));
btnSave.click().then(function () {
// this function use a callback to notify
// me when Toast appears
commonPage.successAlert(function (toast) {
expect(toast.isDisplayed()).toBe(true);
});
});
});
})
});
And this is my CommonPage:
var _toastAlert = function (type, cb) {
var toast = null;
switch (type) {
case "success":
toast = $('ul.ng-toast__list div.alert-success');
break;
case "danger":
toast = $('ul.ng-toast__list div.alert-danger');
break;
}
if (!toast) {
throw new Error("Unable to determine the correct toast's type");
}
browser.ignoreSynchronization = true;
browser.sleep(500);
browser.wait(EC.presenceOf(toast), 10000).then(function () {
cb(toast);
toast.click();
browser.ignoreSynchronization = false;
})
}
var CommonPage = function () {
this.successAlert = function (cb) {
_toastAlert("success", cb);
};
this.dangerAlert = function(cb) {
_toastAlert("danger", cb);
}
}
module.exports = CommonPage;
Chris-Traynor's answer worked for me but i've got an update.
ignoreSynchronization is now deprecated.
For those using angular and protractor to test this, the below works nicely for me.
$(locators.button).click();
await browser.waitForAngularEnabled(false);
const isDisplayed = await $(locators.notification).isPresent();
await browser.waitForAngularEnabled(true);
expect(isDisplayed).toEqual(true);
I've simplified this to make it easier to see, I would normally place this inside a method to make the locators dynamic.
I've found the only way to navigate to different URLs to do view and router behavior tests is to use Backbone.history.loadUrl(). Backbone.history.navigate('#something', true) and router.navigate('#something, {trigger: true, replace: true} and any combination thereof do not work within the test. My application does NOT use pushstate.
This works correctly within the context of a single test.
describe('that can navigate to something as expected', function(){
beforeEach(function() {
this.server = sinon.fakeServer.create();
//helper method does my responds to fetches, etc. My router in my app is what starts Backbone.history
this.router = initializeBackboneRouter(this.server, this.fixtures, false);
});
afterEach(function(){
this.server.restore();
Backbone.history.stop();
Backbone.history.loadUrl('#');
});
it('should create the view object', function(){
Backbone.history.loadUrl('#something');
expect(this.router.myView).toBeDefined();
});
});
During testing you can see that backbone is appending hashes as expected to the URL: localhost:8888/#something Depending on the test.
Unfortunately, loadUrl seems to be introducing a lot of inconsistencies in the way the tests behave. During one of my tests that involves some asynchronous JS where I need to wait for an AJAX call to complete, the fails about 50% of the time with a timeout or sometimes Expected undefined to be defined. If I console out the data I expect to be there it is, so I know it's not a BS test.
it('should add the rendered html to the body', function(){
runs(function(){
Backbone.history.loadUrl('#something');
});
waitsFor(function(){
var testEl = $('#title');
if(testEl.length > 0){ return true; }
}, 1000, 'UI to be set up');
runs(function(){
var testEl = $('#title');
expect(testEl.text()).toEqual(this.router.model.get(0).title);
});
});
The important note here is that it only fails when all tests are run; run by itself it passes 100% of the time.
My question then is: is Backbone.history.loadUrl a bad way to do programatic navigation around a backbone app in jasmine? I feel like I've tried everything to get this to simulate a user going to a specific URL. Is my teardown incorrect? I've tried it without the Backbone.history.loadUrl('#'); and got different behavior but not passing tests.
The core problem seems to be that in the context of several, hundreds, or even a few jasmine tests, Backbone.history is not clearing itself out and is sticking around as one instance of itself instead of being completely re-initialized at each test.
This sucked.
The solution was to edit my code a bit to add a loading complete flag that was set to true when i was sure that the DOM was 100% finished loading.
Then I wrote a helper function that waited for that flag to be true in my beforeEach function in the root of each test.
var waitForLoadingComplete = function(view){
waitsFor(function(){
if(view.loadingComplete == true){return true;}
}, 100, 'UI Setup Finished');
}
After that I refactored my setup into a helper function:
var setupViewTestEnvironment = function(options) {
var temp = {};
temp.server = sinon.fakeServer.create();
temp.router = initializeBackboneRouter(temp.server, options.fixture);
waitForLoadingComplete(temp.router.initialview);
runs(function(){
Backbone.history.loadUrl(options.url);
temp.view = temp.router[options.view];
temp.model = temp.router[options.model];
waitForLoadingComplete(temp.view);
});
return temp;
}
Example use:
beforeEach(function() {
this.testEnv = setupViewTestEnvironment({
url: '#profile',
view: 'profileIndex',
model: 'myModel',
fixture: this.fixtures
});
});
After which I had a view that i had loaded which I could be assured was finished loading so I could test stuff on the DOM or anything else I wanted to do.