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

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.

Related

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');

TestCafe: awaiting ClientFunction to resolve in expect method leads to unexpected behavior

I'm currently facing an issue where awaiting a TestCafe ClientFunction within a assertion query chain leads to the behavior demonstrated by the code below:
const getPageUrl = ClientFunction(() => window.location.href.toString());
// This test works fine
test("Test that works as expected", async t => {
await t
.click(pageElements.someNaviButton) // navigates me to the site with url extension /some/url
.expect(getPageUrl()).contains("/some/url")
});
// This test works fine as well
test("Another test that works as expected", async t => {
await t.click(pageElements.someNaviButton) // navigates me to the site with url extension /some/url
const myPageUrl = await getWindowLocation();
await t.expect(myPageUrl).contains("/some/url")
});
// This test fails
test("Test that does not work as expected", async t => {
await t
.click(pageElements.someNaviButton)
.expect(await getPageUrl()).contains("/some/url")
});
According to TestCafe's documentation, one has to await asynchronous ClientFunctions. This leads me to what irritates me: I expect the second test to pass, which it does. Awaiting the method that encapsulates the ClientFunction within the expect method of the third test seems to be problematic, but why is that?
Additional info that could be of interest: I'm using TestCafe version 1.14.0.
Please consider that a test actions chain is a single expression. So, in the third test, getPageUrl is calculated before the chain execution. At that moment, the test is still on the start page.
The first test passes because getPageUrl is calculated lazily via TestCafe Smart Assertion Query Mechanism. This is the most proper way to write this kind of tests.
The second test passes because you broke the test actions chain.

Access Token in React.useEffect generally works, but comes back undefined when page is refreshed / reloaded

I have a simple React page / component in Gatsby that makes an API call. For this API call I need a token. I use gatsby-theme-auth0 to obtain this token via their AuthService object.
I am starting the API call in my useEffect. It looks like this:
useEffect(() => {
//defining the async function
async function fetchFromAPI() {
try {
const data = await fetchData()
setData(data)
}
}
//executing the async function:
fetchFromAPI()
}, [])
The function fetchData(), which is asynchronously called in useEffect currently looks like so:
async function fetchData() {
const client = new GraphQLClient(SERVER_URL_GRAPHQL)
let aToken = await AuthService.getAccessToken()
client.setHeader('authorization', `Bearer ${aToken}`)
const query = ...
const data = await client.request(query)
return data
}
All of this generally works. When I navigate to this page, from a different page within my SPA it works. However, when I reload the page, it doesn't. the access token (aToken) then comes back as undefined.
But: I can make things work, when I wrap a setTimeout around the whole call. Then the access token comes back fine and isn't undefined. So I guess something first needs to initialise before AuthService can be called? I'm just not sure how to ensure this.
But this is not what I want to do in production. Now I am wondering why this is. Maybe I am using useEffect the wrong way? Unfortunately, I have not been able to find anything online or on github so far. I'm sure the problem is rather basic though.
EDIT: The AuthService.getAccessToken() method can be found here It's part of gatsby-theme-auth0
EDIT: To clarify, the server does receive the request and sends back {"error":"jwt malformed"} - which makes sense, since it's undefined.
I don't know if you have the authentication in a hook already or not, but you need to check if the user is authenticated before you make any api call, especially those that on app init. Do you have a hook/context when you handle the authentication ? If you have, you can change your code a bit
const {isAuthenticated} = useContext(userAuthenticatedContext)
useEffect(() => {
//defining the async function
async function fetchFromAPI() {
try {
const data = await fetchData()
setData(data)
}
}
//executing the async function:
if(isAuthenticated) fetchFromAPI()
}, [isAuthenticated])
This way, isAuthenticated is a dependency in your useEffect and it will run again when the value of isAuthenticated is changed and it will not fail as you are doing a check, before making the call.
getAccessToken relies on that modules' this.accessToken value to be set. It looks like you need to call either handleAuthentication or checkSession prior to making your call so that the value gets initialized properly. Consider putting checkSession somewhere that runs when the page loads.

Passing context implicitly across functions and javascript files in nodes.js

I have created a web server i node.js using express and passport. It authenticates using an oauth 2.0 strategy (https://www.npmjs.com/package/passport-canvas). When authenticated, I want to make a call such as:
app.get("/api/courses/:courseId", function(req, res) {
// pass req.user.accessToken implicitly like
// through an IIFE
createExcelToResponseStream(req.params.courseId, res).catch(err => {
console.log(err);
res.status(500).send("Ops!");
});
});
My issue is that i would like, in all subsequent calls from createExcelToResponseStream, to have access to my accessToken. I need to do a ton of api calls later in my business layer. I will call a method that looks like this:
const rq = require("request");
const request = url => {
return new Promise(resolve => {
rq.get(
url,
{
auth: {
bearer: CANVASTOKEN // should be req.user.accessToken
}
},
(error, response) => {
if (error) {
throw new Error(error);
}
resolve(response);
}
);
});
};
If i try to create a global access to the access token, i will risk
race conditions (i think) - i.e. that people get responses in the context of another persons access token.
If i pass the context as a variable i have to refactor a
lof of my code base and a lot of business layer functions have to
know about something they don't need to know about
Is there any way in javascript where i can pass the context, accross functions, modules and files, through the entire callstack (by scope, apply, bind, this...). A bit the same way you could do in a multithreaded environment where you have one user context per thread.
The only thing you could do would be
.bind(req);
But that has has to be chained into every inner function call
somefunc.call(this);
Or you use inline arrow functions only
(function (){
inner=()=>alert(this);
inner();
}).bind("Hi!")();
Alternatively, you could apply all functions onto an Object, and then create a new Instance:
var reqAuthFunctions={
example:()=>alert(this.accessToken),
accessToken:null
};
instance=Object.assign(Object.create(reqAuthFunctions),{accessToken:1234});
instance.example();
You could use a Promise to avoid Race conditions.
Let's have this module:
// ContextStorage.js
let gotContext;
let failedGettingContext;
const getContext = new Promise((resolve,reject)=>{
gotContext = resolve;
failedGettingContext = reject;
}
export {getContext,gotContext, failedGettingContext};
And this inititalization:
// init.js
import {gotContext} from './ContextStorage';
fetch(context).then(contextIGot => gotContext(contextIGot));
And this thing that needs the context:
// contextNeeded.js
import {getContext} from './ContextStorage';
getContext.then(context => {
// Do stuff with context
}
This is obviously not very usable code, since it all executes on load, but I hope it gives you a framework of how to think about this issue with portals... I mean Promises...
The thing that happens when you call the imported 'gotContext', you actually resolve the promise returned by 'getContext'. Hence no matter the order of operations, you either resolve the promise after the context has been requested setting the dependent operation into motion, or your singleton has already a resolved promise, and the dependent operation will continue synchronously.
On another note, you could easily fetch the context in the 'body' of the promise in the 'ContextStorage' singleton. However that's not very modular, now is it. A better approach would be to inject the initializing function into the singleton in order to invert control, but that would obfuscate the code a bit I feel hindering the purpose of the demonstration.

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

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.

Categories