need protractor to wait for a service to return prior to testing - javascript

I am relatively new to protractor, and I have not been able to make protractor to wait for a page to unload prior to testing. Example below:
//in loginPage object
function login(email, password) {
element(by.id('inputEmail')).sendKeys(email);
element(by.id('inputPassword')).sendKeys(password);
element(by.css('.btn.btn-primary')).click();
browser.driver.sleep(4000);
return !(element(by.binding('userCtrl.labels.signIn()')).isPresent());
}
The sleep statement does not work however, as seen bu the below test always failing even when the login succeeds and the browser navigates away from the login page:
//in separate test page
it('should allow a valid user to login', function() {
expect(loginPage.login('tiratf#gmail.com', '12345678')).toBe(true);
});
Thank you!

Protractor actions (e.g. isPresent()) return a promise, not the underlying value.
i.e. this is a promise: element(by.binding('userCtrl.labels.signIn()')).isPresent()
Please read this https://github.com/angular/protractor/blob/master/docs/control-flow.md.
This should pass:
function login(email, password) {
element(by.id('inputEmail')).sendKeys(email);
element(by.id('inputPassword')).sendKeys(password);
element(by.css('.btn.btn-primary')).click();
browser.driver.sleep(4000);
return element(by.binding('userCtrl.labels.signIn()')).isPresent();
}
--
//in separate test page
it('should allow a valid user to login', function() {
expect(loginPage.login('tiratf#gmail.com', '12345678')).toBe(false);
});
What the expect did was unwrap the promise so that you can assert against its underlying value.

Related

Playwright How To Return page from a function that creates a browser context?

I am trying to make a function that opens the browser and logs into the page using basic auth. I would like the function to return the page so I can pick it up in the test and continue to use it.
When I pass browser to the function I need to create a new context inside the function so I can login with basic auth.
Creating a new browser context in the function works fine to open the page and login.
The problem is that I cannot return the new page from the function. The page that is returned from the function has no intellisense and fails when I attempt to use it in the normal way --- such as doing: page.locator('#id1').click() ---> test fails
// Custom Open Browser Login Function
export async function openBrowserAndLogin(browser, username, password){
const context = await browser.newContext({
httpCredentials:{
username: username,
password: password
},
})
const page = await context.newPage()
await page.goto('websiteurl.com')
return page
}
// Test
import { test } from '#playwright/test';
import {openBrowserAndLogin} from '../customfunctions.openBrowser.mjs'
test('login and do stuff', async ({ browser }) => {
const page = openBrowserAndLogin(browser,'user1', 'secretpassword')
page.locator('#account').click() // no methods exist on this page object???? Test Fail
})
So basically I am importing a function into the test. I pass the browser to the function. The function uses browser to create a new context, logs into application, and returns a new page.
The page returned from the function is dead.
Does anyone know a way to return a page from a context created inside an imported function?
Or if there is some other way to accomplish this that would be helpful too.
openBrowserAndLogin is not a good pattern. This abandons the reference to the browser context object so you can never close it, thereby leaking memory and potentially hanging the process (unless the test suite ungracefully terminates it for you).
Instead, prefer to let Playwright manage the page:
test('login and do stuff', async ({ page }) => {
// ^^^^
Now you can add credentials with Playwright's config or test.use:
import {test} from "#playwright/test"; // ^1.30.0
test.describe("with credentials", () => {
test.use({
httpCredentials: {
username: "user1",
password: "secretpassword"
}
});
test("login and do stuff", async ({page}) => {
await page.goto("https://example.com/");
await page.locator("#account").click();
});
});
Notice that I've awaited page.locator('#account').click().
Another option is to use fixtures.
You’re just missing await.
openBrowserAndLogin is an async function, so it’s returning a promise, which wouldn’t have the page methods itself. You need to unwrap it first, like so:
const page = await openBrowserAndLogin(browser,'user1', 'secretpassword')
That being said, I would definitely recommend doing the auth in global setup with storageState if you need the same login for every test and then just use the page fixture, or you could always override the page fixture or add your own or something similar. There are other potential ways too. But for what you have, just that small piece was missing.
Note that it’s also good practice to close your context if you manually create one.

Unit testing for backend and services methods calls with Jasmine

I started working with tests, more specifically with Jasmine, and I'm having some difficulty to test if the backend and some services methods are being called.
So basically I'm calling the forgotPassword method when the formulary is submitted and I was wondering how can I properly check if the API (apiSendPasswordResetLink) and the services methods (showLoader, showAlert and navigateTo) are being called as expected.
async forgotPassword() {
try {
console.log("1");
this.loadingService.showLoader();
console.log("2");
await this.userService
.apiSendPasswordResetLink(this.form.value['email'])
.toPromise();
console.log("3");
this.utilitiesService.showAlert(`We've emailed you a link to reset your password. Please check your mail box and spam.`);
console.log("4");
delay(1500);
this.navigationService.navigateTo('/login/auth');
console.log('5')
} catch (err) {
this.utilitiesService.showAlert(err);
} finally {
this.loadingService.hideLoader();
}
}
The test:
it('should submit email to reset password after submitting formulary', () => {
component.form.setValue({
email: 'test#test.io',
});
const loadingService = TestBed.inject(LoaderService);
const userService = TestBed.inject(UserService);
const utilitiesService = TestBed.inject(UtilitiesService);
const navigationService = TestBed.inject(NavigationService);
fixture.detectChanges();
const button = fixture.debugElement.nativeElement.querySelector('#button');
spyOn(component, 'forgotPassword').and.callThrough();
spyOn(loadingService, 'showLoader');
spyOn(userService, 'apiUserSendPasswordResetLink');
spyOn(utilitiesService, 'showAlert');
spyOn(navigationService, 'navigateTo');
// Submitting form
fixture.debugElement
.query(By.css('form'))
.triggerEventHandler('ngSubmit', null);
expect(component.form.valid).toEqual(true);
expect(button.disabled).toBeFalsy();
expect(component.forgotPassword).toHaveBeenCalled();
expect(loadingService.showLoader).toHaveBeenCalled();
expect(userService.apiUserSendPasswordResetLinkGet).toHaveBeenCalled();
expect(utilitiesService.showAlert).toHaveBeenCalled();
expect(navigationService.navigateTo).toHaveBeenCalled();
});
Every time I run and / or debug the test, I have the error
Expected spy navigateTo to have been called.
But the console never prints "3", which means showAlert is also not being called and I should also have the same error regarding showAlert spy to be called, but I don't.
I don't know if this problem has to do if the await call to the API or something else. I would like to know how can I fix it, so all the test can pass as expected.
Thank you!
When adding a spy on UserService#apiUserSendPasswordResetLink, without any spy strategy, it defaults to doing nothing. However, in your forgotPassword method, you are chaining the response of the call to a Promise wrapper and awaiting the resolution. Since apiUserSendPasswordResetLink is not invoked, I'm guessing that the promise is never resolved and the test gets stuck.
One simple way to resolve the issue is to add a strategy to the spy so that it returns a value:
spyOn(userService, 'apiUserSendPasswordResetLink').and.returnValue('whatever');

"Failed: Error while waiting for Protractor to sync with the page" while executing Protractor tests

I try to execute some Protractor tests on a web application that consists of both Angular and non-angular components.
My code looks like so:
describe("Test Name", function() {
it("Test case", function() {
// first execute steps on a non-Angular component
browser.waitForAngularEnabled(false);
// some test steps
// then execute tests on an Angular component, which opens in a new browser tab
browser.waitForAngularEnabled(true);
// some more test steps
});
});
The problem is that after the above test is run, the browser launches and immedietaly closes with the following error:
Failed: Error while waiting for Protractor to sync with the page: "both angularJS testability and angular testability are undefined. This could be either because this is a non-angular page or because your test
involves client-side navigation, which can interfere with Protractor's bootstrapping. See https://github.com/angular/protractor/issues/2643 for details"
When I remove browser.waitForAngularEnabled(true); from the code, the steps for the non-Angular component are executed successfully, then the Angular component of the application is opened in the new browser tab, nothing happens for 10 seconds (no steps are executed) and the browser closes with the following error:
Failed: Wait timed out after 10007ms
You should probably account for asynchronous code and wait for promises to resolve. Also, add Jasmine's done parameter to the test function to let selenium know when the test is finished.
Another thing that might cause this is activating waitForAngularEnabled before you're actually in an angular page. I suggest you prefix that call with a call to check that something on the page already got loaded so you know angular is ready to be hooked by protractor before waiting for angular activities.
It's important to note that protractor waits for the next action after waitForAngularEnabled(true) to trigger the check, relying on that might make the problem unclear if sometime later someone changes the code.
describe("Test Name", function() {
it("Test case", function(done) {
// first execute steps on a non-Angular component
browser.waitForAngularEnabled(false)
.then(() => /* step1 */)
.then(() => /* step2 */)
// ...
// 👇 this one is very important to make sure
// we're in an angular page 👇
.then(() => ensurePageTitleIsVisible())
.then(() => browser.waitForAngularEnabled(true))
.then(() => done())
.catch((err) => done.fail(err));
});
});
function ensurePageTitleIsVisible() {
return browser.wait(ExpectedConditions.visibilityOf(PAGE_TITLE_SELECTOR), jasmine.DEFAULT_TIMEOUT_INTERVAL, 'Title does not exist after timeout');
}
This might give you a better error message as well.
and of course, you can do the same with async \ await syntax.
describe("Test Name", function() {
it("Test case", function(done) {
try {
// first execute steps on a non-Angular component
await browser.waitForAngularEnabled(false)
await step1();
await step2();
// ...
await browser.waitForAngularEnabled(true);
done();
} catch(err) {
done.fail(err);
}
});
});
Basically, your problem happens because you continue with the test steps before the browser.waitForAngularEnabled function actually finishes.
use browser.ignoreSynchronization = true; when interacting with non angular ui since it makes protractor not to wait for Angular promises
and browser.ignoreSynchronization = false; followed by browser.waitForAngular(); when interacting with angular ui

Prevent Angular controller from loading using UI Router resolve

Im trying to use a promise to prevent a state from loading in UI Router $resolve.
$stateProvider.state('base.restricted', {
url:'/restricted',
templateUrl: 'views/restricted.html',
controller: 'restrictedCtrl',
resolve: { // user must be logged-in to proceed
// authentication service checks session on back-end
authenticate: function ($q, $state, $timeout, AuthService){
AuthService.validate(token).then(function(res){
if( res.data.response.status===1 ) { // authenticated!
return $q.resolve(); // load state
} else { // authentication failed :(
$timeout(function() { $state.go('base.notfound') }); // load not found page
return $q.reject(); // do not load state
}
});
}
}
})
This scheme works in terms of redirecting the user based on authentication results, but it looks like, upon failed authentication, the controller is still being loaded for a brief instant before the user is re-directed to the "not found" state.
I believe the reason is because Im running AuthService within resolve, but in an asynchronous manner. So the controller is loaded while AuthService does its thing but is then redirected once the callback function executes.
But isnt resolve inherently supposed to be blocking? That is, the controller doesnt load until resolve is "resolved"?
AuthService is basically this, fyi:
.service('AuthService', function($http){
this.validate = function(token){
return $http.get('http://my.api/validate/'+token);
}
});
How can I modify this code to prevent loading until AuthService's callback function has completed.
You should return a promise immediately, instead of doing so after the response from the service comes back:
return AuthService.validate(token).then(function(res){ ... }
Otherwise the router sees no return value instead of a promise, so it thinks it's OK to load the state. Then later when your AJAX request returns you end up redirecting, which is why you see a flicker.

Javascript client side auth customizing Backbone.Sync, does this make sense?

I'm new to Backbone / Marionette and I am having some trouble implementing authorization on my client app. Basically I have a app that have some public and private routes and I need to automate the login flow when the user tries to do some private action, like the original private user flow halts, start the login flow and then resumes the original user flow. Pretty standard stuff...
I am trying to find a way to automate or intercept and implement this behavior from my client app. I am not using any server redirect since that strategy reloads my app all over again and destroys my state, so I am trying to add some custom code to Backbone.sync to accomplish this.
In essence what I am doing is extending Backbone.Model to use my custom sync, that custom sync should return a custom promise (not the $.ajax) like the original Backbone.sync, I always try to resolve the promise using the original sync but if I catch an 401 from server I add Sync context (method, model, options) and also my custom promise object to my globally accessible App object, then i navigate my app to login, "halting" the user flow. Once user submit login info I check if there is a Deferred object on App, if so I call Backbone.sync with the original context and resolve the initial custom promise with that result ("resuming user flow"), then I just finish navigating to the original fragment to sync URLs.
I find this a simple solution in idea but does it make sense in Backbone / Marionette context app?
Here's my customSync
function (method, model, options) {
var deferred = $.Deferred();
if (options)
deferred.then(options.success, options.error);
var sync = Backbone.sync(method, model, _.omit(options, 'success', 'error'));
sync.done(deferred.resolve);
sync.fail(function() {
if (sync.status === 401) {
// Add logic to send the user to the login page,
// or general authentication page.
App.Defer = deferred;
App.Callback = Backbone.sync;
App.Arguments = [method, model, _.omit(options, 'success', 'error')];
App.Fragment = Backbone.history.fragment;
// In this example, we'll send the user to the "login" page:
App.navigate("login", { trigger: true, replace: true });
} else {
deferred.reject.apply(sync, arguments);
}
});
return deferred;
}
And my login submit event of LoginView
loginView.on('auth:login', function (data) {
var sessionModel = new App.Models.Session();
sessionModel.login(data, function (err, user) {
if (err) return loginView.setError(err);
App.trigger('set:user', user);
var defer = App.Defer;
var callback = App.Callback;
var arguments = App.Arguments;
var fragment = App.Fragment;
delete App.Defer;
delete App.Callback;
delete App.Arguments;
delete App.Fragment;
if (defer) {
callback.apply(this, arguments).then(defer.resolve, defer.reject);
App.navigate(fragment, { trigger: false, replace: true });
} else {
App.navigate('', { trigger: true, replace: true });
}
});
});
It's an interesting idea but I'm not sure it does you much good in practice. After login you are going to redirect the user back to the route they were on when the 401 occurred. That route processing is going to end up re-making the service request anyways (unless you do a lot of work to avoid that) so resolving the previously rejected promise won't do any good. I also wouldn't expect promise libraries to all behave the way you want when a rejected promise is later resolved. From a coding perspective, if you do this try to avoid global state - you can pass the data back in the .reject arguments and keep global state clean (which will be particularly important when you have views calling multiple services asynchronously). One more thing... from a security perspective, it would be crucial to ensure that you are dealing with the same user account before and after the 401.
I have done something very similar to this when working with OAuth services where a custom sync method looks for 401s and then attempts to use the refresh_token to get a new access_token then retry the initial request - but retrying a request after kicking the user to a login screen seems a step too far.
The Backbone.sync function seems like a strange place to be handling navigation logic. Sync is for managing your model states, and not the UI. It also seems doubtful that you want to universally retry every sync that fails. What seems more likely to me is that you may want to preserve data that might otherwise be lost by redirects (e.g form data). This is easy with Backbone because you can continue to pass references to models around.
I'd recommend that you get rid of all the global variables and routing logic in your Backbone.sync override. Instead just trigger an event and pass any information you may want to have to a separate handler. Something like this:
sync.fail(function() {
if (sync.status === 401) {
App.vent.trigger("sync:failed", method, model, options);
}
});
This gives you a lot more flexibility with how to handle failures, keeps your sync function clear of navigation code, and makes it much easier to handle the nasty cases you are going to run into with globals (especially since you are likely to get a set of sync calls failing together, not just one).
Now add some handlers to listen for failures.
var redirectTo = "";
App.vent.on("sync:failed", function () {
// Handle routing
redirectTo = Backbone.history.fragment; // Use this url when navigating after auth
Backbone.history.navigate("login", true);
});
App.vent.on("sync:failed", function (method, model, options) {
// Handle model data you care about
// e.g. manage a queue of unsaved changes; clear changes that didn't save; preserve UI specific models, etc.
});
If you are dead set on retrying your promises, you should do it this way as well, especially because you are going to want to only retry selectively (not to mention the issues brought up by Robert around this).
Below is how I would setup a handler to resubmit a message after logging in:
App.vent.on("sync:failed", function (method, model, options) {
if (model.retrySaveOnLogin) { // Property I would include on models that can be safely retried
model.listenOnce(App.vent, "login:success", function () { // Some event you trigger when login is successful
this.save({}, options);
});
}
});

Categories