Create a test in Playwright with fetch calls - javascript

So i want to write a e2e test using Playwright, and this is what the website does (i'm using React):
When it first renders, it fetches a random fact from an API, and then it uses the first three words as a query to fetch a random picture from another API. Both the fact and the picture are shown after fetching.
There is also a button that gets a new fact with a new fetch request, thus it also gives you a different picture by using the three words from the new fact instead, so it's like reloading the page again.
The test that i want to write has to check that when the user clicks the button, the new fetches are succesful, and the new fact and image are different from the previous ones (i already have a test to check that there is an initial fact and picture).
I tried to save the first paragraph and picture in some variables, then click the button, wait for response, get the new paragraph and pictures and expect them to be different from the previous ones.
Code for the test attempt:
const button = await page.getByRole('button')
const prevText = await page.getByRole('paragraph')
const prevImage = await page.getByRole('img')
await button.click()
page.waitForResponse(res => res.status() === 200)
const newText = await page.getByRole('paragraph')
const newImage = await page.getByRole('img')
await expect(newText).not.toEqual(prevText)
await expect(newImage).not.toEqual(prevImage)
i also tried to have the click and waitForResponse inside of a Promise.all but it didn't work either
website code: https://github.com/krst221/test2
deploy: https://test2-krst221.vercel.app/

Looking at the snippets you provided in your question, I see two main potential culprits that are likely causing you issues.
1 - The response is not actually getting awaited
The waitForResponse method returns a promise that needs to be awaited, otherwise it’s not actually being waited for by the code/test. Also, that wait and promise needs to start before you click, and awaited after in order to actually get to the click while also watching for the response. So you would need to change the code to be something like this:
const responsePromise = page.waitForResponse(res => res.status() === 200)
await button.click()
await responsePromise
The Playwright docs on waitForResponse also give examples of this.
2 - Locators are lazy
In Playwright, Locators are lazy and just store how to locate the element(s) without actually doing it, until an action is taken on them like click. So your new and prev ones are actually equivalent. Plus, to check whether the new differs from the prev, you may be wanting to check their contents anyway. Either way, you’ll have to use the locators to get information to then compare, such as locator.textContent() for the paragraph.
Hope that helps!

Related

Playwright - How to test that a network request have finished after on click event

I am making BDD test with a Cucumber-Playwright suit. A page I am making a test for has buttons that will trigger a PUT API request and update the page (Note: The button will not link to new address, just trigger and API request).
I want to make sure that all network events have finished before moving on to the next step as it may try to act too soon before the API request has returned and cause the test to fail.
I want to avoid hard waits so I read the documentation and found a step structure that uses Promise.all([]) to combine two or more steps. From what I understand they will check that each step in the array to be true at the same time before moving on.
So the steps looks like this:
await Promise.all([inviteUserButton.click(), page.waitForLoadState('networkidle')])
await page.goto('https://example/examplepage')
This stage of the test is flaky however, it will work about 2/3 times. From the trace files I read to debug the test I see that the the network response repsondes with net::ERR_ABORTED POST https://.....
I believe this is due to to the page.goto() step has interrupted the network request/response. Due to this, it will cause the coming assertion to fail as it was not completed.
Is there a way to test that all the pages network events have finished from a onClick event or similar before moving onto the next step?
How about this:
The correct practice is to wait for response before clicking the button to avoid any race conditions so that it will work correctly every time as long as api(s) returning back with ok response.
In this , you may add N number of api calls with commas where response from multiple api is expected.
Note: You may need to change the response status code from 200 to 204 , depending on how the api is implemented.
const [resp]= await Promise.all([
this.page.waitForResponse(resp => resp.url().includes('/api/odata/any unique sub string of API endpoint') && resp.status() === 200),
//API2 ,
//API3,
//APIN,
this.page.click('inviteUSerButtonLocator'),
]);
const body= await resp.json() //Next step of your scenario
If you don't wrap it in a promise is it still flaky?
await inviteUserButton.click();
await page.waitForLoadState('networkidle');
await page.goto('https://example/examplepage');
How long do the network requests last? The default timeout is set to 30 seconds, did you try to increase it?
waitForLoadState(state?: "load"|"domcontentloaded"|"networkidle", options?: {
/**
* Maximum operation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be
* changed by using the
* [browserContext.setDefaultNavigationTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-navigation-timeout),
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout),
* [page.setDefaultNavigationTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout)
* or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
*/
timeout?: number;
}): Promise<void>;
You can try and wait for more milliseconds
await Promise.all([inviteUserButton.click(), page.waitForLoadState('networkidle', { timeout:60_000 })])
await page.goto('https://example/examplepage')

How can I use AbortController in Next js?

My application allows users to do searches and get suggestions as they type in the search box. For each time that the user enters a character, I use 'fetch' to fetch the suggestions from an API. The thing is that if the user does the search fast, he can get the result before the suggestions are fetched. In this case, I want to cancel the fetch request.
I used to have the same application in React and I could easily cancel the request using AbortController, but that isn't working in Next js.
I did some research and I think the problem is happening because Next doesn't have access to AbortController when it tries to generate the pages.
I also had this problem when I tried to use 'window.innerWidth' because it seems Next doesn't have access to 'window' either.
The solution I found was to use 'useEffect'. It worked perfectly when I used it with 'window'.
const [size, setSize] = useState(0)
useEffect(() => {
setSize(window.innerWidth)
}, [])
But it isn't working when I use AbortController. First I did it like this:
let suggestionsController;
useEffect(() => {
suggestionsController = new AbortController();
},[])
But when I tried to use 'suggestionsController', it would always be undefined.
So I tried to do the same thing using 'useRef'.
const suggestionsControllerRef = useRef(null)
useEffect(() => {
suggestionsControllerRef.current = new AbortController();
},[])
This is how I'm fetching the suggestions:
async function fetchSuggestions (input){
try {
const response = await fetch(`url/${input}`, {signal: suggestionsControllerRef.current.signal})
const result = await response.json()
setSuggestionsList(result)
} catch (e) {
console.log(e)
}
}
And this is how I'm aborting the request:
function handleSearch(word) {
suggestionsControllerRef.current.abort()
router.push(`/dictionary/${word}`)
setShowSuggestions(false)
}
Everything works perfectly for the first time. But if the user tries to do another search, 'fetchSuggestions' function stops working and I get this error in the console 'DOMException: Failed to execute 'fetch' on 'Window': The user aborted a request'.
Does anyone know what is the correct way to use AbortController in Next js?
The solution I found to the problem was create a new instance of AbortController each time that the user does the search. While the suggestions were being displayed, 'showSuggestions' was true, but when 'handleSearch' was called, 'showSuggestions' was set to false. So I just added it as a dependency to useEffect.
useEffect(() => {
const obj = new AbortController();
setSuggestionController(obj)
},[showSuggestions])
I also switched from useRef to useState, but I'm not sure if that was necessary because I didn't test this solution with useRef.
I don't know if that is the best way of using AbortController in Next js, but my application is working as expected now.
I suppose you can try an abort controller to cancel your requests if the user stops typing, but this is not the standard way of solving this common problem.
You want to "debounce" the callback that runs when the user types. Debouncing is a strategy that essentially captures the function calls and "waits" a certain amount of time before executing a function. For example, in this case you might want to debounce your search function so that it will only run ONCE, 500 ms after the user has stopped typing, rather than running on every single keypress.
Look into debouncing libraries or write a debounce function yourself, but fair warning it can be pretty tricky at first!

Knex bulk insert not waiting for it to finish before passing to the next async operation

I am having a problem where I am making a bulk insert of multiple elements into a table, then I immediatly get the last X elements from that table that were recently inserted but when I do that it seems that the elements have not yet been inserted fully even thought I am using async await to wait for the async operations.
I am making a bulk insert like
const createElements = elementsArray => {
return knex
.insert(elementsArray)
.into('elements');
};
Then I have a method to immediately access those X elements that were inserted:
const getLastXInsertedElements = (userId, length, columns=['*']) => {
return knex.select(...columns)
.from('elements').where('userId', userId)
.orderBy('createdAt', 'desc')
.limit(length);
}
And finally after getting those elements I get their ids and save them into another table that makes use of element_id of those recently added elements.
so I have something like:
// A simple helper function that handles promises easily
const handleResponse = (promise, message) => {
return promise
.then(data => ([data, undefined]))
.catch(error => {
if (message) {
throw new Error(`${message}: ${error}`);
} else {
return Promise.resolve([undefined, `${message}: ${error}`])
}
}
);
};
async function service() {
await handleResponse(createElements(list), 'error text'); // insert x elements from the list
const [elements] = await handleResponse(getLastXInsertedElements(userId, list.length), 'error text') // get last x elements that were recently added
await handleResponse(useElementsIdAsForeignKey(listMakingUseOfElementsIds), 'error text'); // Here we use the ids of the elements we got from the last query, but we are not getting them properly for some reason
}
So the problem:
Some times when I execute getLastXInsertedElements it seems that the elements are not yet finished inserting, even thought I am waiting with async/await for it, any ideas why this is? maybe something related to bulk inserts that I don't know of? an important note, all the elements always properly inserted into the table at some point, it just seems like this point is not respected by the promise (async operation that returns success for the knex.insert).
Update 1:
I have tried putting the select after the insert inside a setTimeout of 5 seconds for testing purposes, but the problem seems to persist, that is really weird, seems one would think 5 seconds is enough between the insert and the select to get all the data.
I would like to have all X elements that were just inserted accessible in the select query from getLastXInsertedElements consistently.
Which DB are you using, how big list of data are you inserting? You could also test if you are inserting and getLastXInsertedElements in a transaction if that hides your problem.
Doing those operations in transaction also forces knex to use the same connection for both queries so it might lead to a tracks where is this coming from.
Another trick to force queries to use the same connection would be to set pool's min and max configuration to be 1 (just for testing is parallelism is indeed the problem here).
Also since you have not provided complete reproduction code for this, I'm suspecting there is something else here in the mix which causes this odd behavior. Usually (but not always) this kind of weird cases that shouldn't happen are caused by user error in elsewhere using the library.
I'll update the answer if there is more information provided. Complete reproduction code would be the most important piece of information.
I am not 100% sure but I guess the knex functions do not return promise by default (but a builder object for the query). That builder has a function called then that transforms the builder into a promise. So you may try to add a call to that:
...
limit(length)
.then(x => x); // required to transform to promise
Maybe try debugging the actual type of the returned value. It might happen that this still is not a promise. In this case you may not use async await but need to use the then Syntax because it might not be real js promises but their own implementation.
Also see this issue about standard js promise in knex https://github.com/knex/knex/issues/1588
In theory, it should work.
You say "it seems"... a more clear problem explanation could be helpful.
I can argue the problem is that you have elements.length = list.length - n where n > 0; in your code there are no details about userId property in your list; a possible source of the problem could be that some elements in your list has a no properly set userId property.

Problem with updating a variable in Vue.js

I'm creating a web app with Vue.js (this is the first time I use it). The app is basically a multi user real time quiz, in which every user have to choose a role and to answer questions related with his role. I use a collection in cloud firestore database to store questions associated to each role and answers associated to each question. Moreover each answer is characterized by a field "nextQuestion" that contains the id of the next question to visualize, a field "nextUser" that contains the id of the next user at which this new question is related (these fields are used in queries to select the next question and its possible answers) and a boolean field "default" that indicates, if true, the answer that is chosen in the case user don't answer the question within the set time (others to a field indicating the text of the answer). I get questions and answers with a query to visualize them on the webapp.
My problem is related to the situation in which the user doesn't answer a question within the set time (meanwhile if a user selects an answer within the set time, I haven't problems). When the time for an answer expires, I call this function:
CountTerminated: function () {
if(this.optionSelected == false){ //optionSelected is a component variable that informs if a user has selected or not an answer
this.onNotSelectedAnswer(this.getDefaultAnswer())
}
else{
this.onClickButtonConfirm() //function called if the user selects an answer within the set time
}
}
The function getDefaultAnswer() gets the fields (among which "nextUser" and "nextQuestion") of the default answer associated with the current question (through a query) and return them through a variable:
getDefaultAnswer(){
var data
db.collection("Utenti").doc(this.userId).collection("Domande").doc(this.questionId).collection("Risposte").where("default","==",true).get().then(querySnapshot =>{
querySnapshot.forEach(doc=>{
data = doc.data()
})
})
return data
},
the function onNotSelectedAnswer(data) mainly takes in input the value returned by getDefaultAnswer(), it assigns "data" to the component variable answerId and it updates the value of the component variable "userId" (that informs about the role of the user who have to answer the current question),the value of the component variable "questionId" (that contains the id of the current question) and the value of questionValue(that contains the text of the current question) using functions setUserId(), setQuestionId(), setQuestionValue()
onNotSelectedAnswer: function(data){
if(this.userChoice == this.userId){
this.answerId = data
this.setUserId(this.answerId.nextUser)
this.setQuestionId(this.answerId.nextQuestion)
this.setQuestionValue()
this.optionSelected = false
this.answers=[]
this.isActive = ""
this.getAnswers() //function used to query (using updated values of userId and questionId variable) the DB to obtain a question and its related possible answers
var a = this.channel.trigger('client-confirmEvent',{ user: this.userId, question : this.questionId, questionval: this.questionValue})
console.log(a)
}
}
The problem is related to the fact that in onNotSelectedAnswer() function, answerId is "undefined" instead of containing the result of the query and therefore the data that I will use to upload the new question. I don't understand which is the error, I hope that you can help me.
The problem is that the Firestore query is asynchronous but you aren't waiting for the response before continuing. Effectively what you have is this:
getDefaultAnswer () {
var data
// This next bit is asynchronous
db.doLotsOfStuff().then(querySnapshot => {
// This callback won't have been called by the time the outer function returns
querySnapshot.forEach(doc => {
data = doc.data()
})
})
return data
},
The asynchronous call to Firestore will proceed in the background. Meanwhile the rest of the code in getDefaultAnswer will continue to run synchronously.
So at the point the code reaches return data none of the code inside the then callback will have run. You can confirm that by putting in some console logging so you can see what order the code runs in.
The use of then to work with asynchronous code is a feature of Promises. If you aren't already familiar with Promises then you should study them in detail before going any further. Here is one of the many guides available:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
The bottom line is that you cannot force the getDefaultAnswer method to wait for the asynchronous action to complete. What you can do instead is to return a suitable Promise and then wait for that Promise to resolve before you call onNotSelectedAnswer. It might look something like this:
getDefaultAnswer () {
// We return the Promise chain from getDefaultAnswer
return db.doLotsOfStuff().then(querySnapshot => {
var data = null
// I have assumed that forEach is synchronous
querySnapshot.forEach(doc => {
data = doc.data()
})
// This resolves the Promise to the value of data
return data
})
},
It is important to appreciate that the method getDefaultAnswer is not attempting to return the value of the data. It is instead returning a Promise that will resolve to the value of the data.
Within CountTerminated you would then use it like this:
this.getDefaultAnswer().then(defaultAnswer => {
this.onNotSelectedAnswer(defaultAnswer)
})
or if you prefer:
this.getDefaultAnswer().then(this.onNotSelectedAnswer)
The latter is more concise but not necessarily clearer.
You could also write it using async/await but I wouldn't advise trying to use async/await until you have a solid grasp of how Promises work. While async/await can be very useful for tidying up code it is just a thin wrapper around Promises and you need to understand the Promises to debug any problems.
The code I've suggested above should work but there is a delay while it waits for the asynchronous request to complete. In that delay things can happen, such as the user may click on a button. That could get you into further problems.
An alternative would be to load the default answer much sooner. Don't wait until you actually need it. Perhaps load it as soon as the question is shown instead. Save the result somewhere accessible, maybe in a suitable data property, so that it is available as soon as you need it.

Difficulty accessing window object in Cypress

I'm trying to access the window object of my App in Cypress in the following manner.
cy.url().should('include', '/home').then(async () => {
const window = await cy.window();
console.log(window);
});
The above method is not working for me as window is returned as undefined.
However, the answer in this SO post states the following:
Or you can use cy.state('window') which returns the window object
synchronously, but this is undocumented and may change in the future.
This method does return the window value successfully.
cy.url().should('include', '/home').then(async () => {
const window = cy.state('window');
console.log(window);
});
As the answer suggests, cy.state('window') is undocumented so I would still rather use cy.window(). Is there any reason why it's returning undefined? (I started learning cypress today.)
This comes up frequently. Cypress has some documentation stating Commands are not Promises. I did a write up using a custom command to force a Command Chain to behave like a promise, but it is still experimental and nuanced.
First I'll give your example almost verbatim to what you're trying to accomplish:
cy.url().should('include', '/home').then(() => {
cy.window().then(win => {
console.log(win) // The window of your app, not `window` which is the Cypress window object
})
})
Your example could be written a number of ways, but maybe explaining a bit how Cypress works will help more.
Cypress has something called "Commands" that return new "Chainers". It is fluid syntax like JQuery:
// fill and submit form
cy
.get('#firstname')
.type('Nicholas')
.get('#lastname')
.type('Boll')
.get('#submit')
.click()
You can (and should) break up chains to be more like sentences:
// fill and submit form
cy.get('#firstname').type('Nicholas')
cy.get('#lastname').type('Boll')
cy.get('#submit').click()
You might have guessed that all Cypress Chainer commands are asynchronous. They have a .then, but they aren't actually promises. Cypress commands actually enqueue. Cypress hooks into mocha's lifecycle to make sure a before, beforeEach, it, afterEach, after block waits until Cypress commands are no longer enqueued before continuing.
Let's look at this example:
it('should enter the first name', () => {
cy.get('#firstname').type('Nicholas')
})
What actually happens is Cypress sees the cy.get Command and enqueues the get command with the argument '#firstname'. This immediately (synchronously) returns execution to the test. Cypress then sees the cy.type command with the argument 'Nicholas' and returns immediately to the test. The test is technically done at this point since there is no done callback and no Promise was returned. But Cypress hooks into the lifecycle of mocha and doesn't release the test until enqueued commands are completed.
Now that we have 2 enqueued commands and the test is waiting for Cypress to release the test, the get command is popped off the queue. Cypress will try to find an element on the page with an id of firstname until it finds it or times out. Assuming it finds the element, it will set a state variable called subject (cy.state('subject'), but don't rely on that). The next enqueued command type will grab the previous subject and attempt to type each key from the string 'Nicholas' one at a time with a default delay of 50ms until the string is done. Now there are no more enqueued commands and Cypress releases the test and the runner continues to the next test.
That was a bit simplified - Cypress does a lot more to make sure .type only runs on elements that can receive focus and are interactable, etc.
Now, knowing this, you can write your example a bit more simply:
cy.url().should('include', '/home')
// No need for `.then` chaining or async/await. This is an enqueued command
cy.window().then(win => {
console.log(win)
})
For me, the accepted answer is good but not really showing me what is necessary.
For me, it is awesome that everything is synchronous and you can do things like
let bg1 = null;
// simply store the value of a global object in the DOM
cy.window().then((win) => {
bg1 = win.myPreciousGlobalObj.color;
});
// Do something that changes a global object
cy.get('a.dropdown-toggle').contains('File').click();
cy.window().then((win) => {
const bg2 = win.myPreciousGlobalObj.color;
// check if nodes and edges are loaded
expect(bg1 != bg2).to.eq(true);
});
Here the interesting thing is even the things inside the then blocks are synchronous. This is so useful.

Categories