Playwright: Click button does not work reliably (flaky) - cypress

1. Initial post
I know that Playwright has auto-waiting.
I have a test where sometimes click works, sometimes does not. Below is the code.
import { test, expect } from '#playwright/test';
test('test', async ({ page }) => {
await page.goto('https://website.com');
await page.getByRole('link', { name: 'Log in' }).click();
await expect(page).toHaveURL('https://website.com/login/');
});
Error:
expect(received).toHaveURL(expected)
Expected string: "https://website.com/login/"
Received string: "https://website.com"
Here is a piece of HTML with a login button
<span data-v-154d6ddf="" class="absolute-top-right">
<div class="row justify-end q-gutter-x-sm q-pt-lg q-px-sm">
<div class="col-auto">
<a tabindex="0" type="button" href="/login/potential" role="link" class="q-btn q-btn-item non-selectable no-outline q-btn--flat q-btn--rectangle text-white q-btn--actionable q-focusable q-hoverable q-btn--no-uppercase q-btn--wrap">
<span class="q-focus-helper" tabindex="-1"></span>
<span class="q-btn__wrapper col row q-anchor--skip">
<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
<span>Log in</span>
</span>
</span>
</a>
</div>
<div class="col-auto">
<a tabindex="0" type="button" href="/signup" role="link" class="q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-primary text-white q-btn--actionable q-focusable q-hoverable q-btn--no-uppercase q-btn--wrap">
<span class="q-focus-helper"></span>
<span class="q-btn__wrapper col row q-anchor--skip">
<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
<span>Sign up</span>
</span>
</span>
</a>
</div>
<div class="col-auto">
<label for="f_2334a082-8df9-41a7-9ca0-403e5ce7c237" class="q-field q-validation-component row no-wrap items-start q-select q-field--auto-height q-select--without-input q-select--without-chips q-field--outlined q-field--float q-field--labeled" style="min-width:120px;max-height:20px">
<div class="q-field__inner relative-position col self-stretch column justify-center">
<div tabindex="-1" class="q-field__control relative-position row no-wrap">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<div id="translate" class="q-field__native row items-center">
<span>English</span>
<div id="f_2334a082-8df9-41a7-9ca0-403e5ce7c237" tabindex="0" class="no-outline"></div>
</div>
<div class="q-field__label no-pointer-events absolute ellipsis">Translate</div>
</div>
<div class="q-field__append q-field__marginal row no-wrap items-center q-anchor--skip">
<i aria-hidden="true" role="presentation" class="q-select__dropdown-icon fal fa-caret-down q-icon notranslate"></i>
</div>
</div>
</div>
</label>
</div>
</div>
</span>
2. Update
I tried this variation (see below), and it seems like it fails less frequently, but still does not work 100%.
I also tried .click({force: true}). Fails on 3rd run.
I am new to test automation. Just started learning Playwright.
I will appreciate any comments/suggestions.
thank you.
import { test, expect } from '#playwright/test';
test('test', async ({ page }) => {
await page.goto('https://website.com');
const loginBtn = page.getByRole('link', { name: 'Log in' });
expect(await loginBtn.evaluate(node => node.isConnected)).toBe(true) ;
await page.getByRole('link', { name: 'Log in' }).click();
await expect(page).toHaveURL('https://website.com/login/');
});
3. Update
"You should wait after clicking the button before verifying the url." This ws suggestion from #jaky-ruby
I tried this code
test('test2', async ({ page }) => {
await page.goto('https://website.com/');
// Note that Promise.all prevents a race condition
// between clicking and expecting navigation result.
const [response] = await Promise.all([
page.getByRole('link', { name: 'Log in' }).click(),
page.waitForNavigation({ url: 'https://website.com/login' }),
]);
await expect(page).toHaveURL('https://website/login');
...
});
4. Update
I created similar test in Cypress.
Here is the code
describe("Login", () => {
beforeEach(() => {
cy.visit(baseUrl);
});
it.only("Login page should open", () => {
//does not help: cy.wait(3000);
cy.get('a[href*="login"]').click();
//does not help: cy.get(':nth-child(1) > .q-btn > .q-btn__wrapper > .q-btn__content > span').click();
//does not help: cy.get('#q-app > div > span > div > div:nth-child(1) > a > span.q-btn__wrapper.col.row.q-anchor--skip > span').click();
cy.url().should("eq", authUrl.replace("env", env) + "/login/potential");
});
});
As you can see i tried different variations of locators. The interesting part is that Cypress showed this error. Not sure this is the same problem that occurs in Playwright test.
CypressError
Timed out retrying after 4050ms: cy.click() failed because this element: ...is being covered by another element:
I tried to find that element in the DOM but it is not there.
5. Update
Another thing that I do not understand is that when I select "click" step in Cypress and use picker to select my button it shows me selector cy.get('[data-layer="Content"]'). Again, when I try to search it in the DOM it is not there.
6. Update
I checked each step in Playwright using Trace Viewer.
Interesting. The log shows that Playwright waited for element to be visible and stable. However, on the attached page both Login and Sign up buttons are not visible and still loading (turns out you can inspect it. It's not just a static screenshot). I think, possibly, Playwright's auto-wait is not good enough in this example. Possibly, element is not actually stable and still loading but Playwright performs click which does not work.
Now question, "can I do some additional wait to unsure that element has loaded and stable? "

Okay, looks like I found ~~solution~~ a way to make my test less flaky.
Test still fails sometimes. I think, additional 2 waits which I added just give more time for element to load, so in most cases, probably, that is enough for button to reach stable state. That's why clicking works. Note, on the screenshot below that 'networkidle' is 1.4s
Here is the final code
import { test, expect } from '#playwright/test';
test('test', async ({ page }) => {
await page.goto('https://website.com');
await page.waitForLoadState('networkidle'); // <- until there are no network connections for at least 500 ms.
await page.getByRole('link', { name: 'Log in' }).waitFor(); // <- wait for element
await page.getByRole('link', { name: 'Log in' }).click();
await expect(page).toHaveURL('https://website.com/login/');
});
Sharing here some interesting links I found along the way
Avoiding hard waits in Playwright and Puppeteer - DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป
waitForLoadState waitUntil domcontentloaded doesn't wait ยท Issue #662 ยท microsoft/playwright ยท GitHub

Related

Cypress: How to click on a array-list element if matching text found

I want to add an item in cart with a matching text like 'cashews'. I tried below code but .click() function is giving error as "bind and event handler to the click javascript event"
cy.get('.products').find('.product').each(($e1, index, $list) => {
const textveg = $e1.find('h4.product-name').text() {
if (textveg.includes('Cashews')) {
$e1.find('.button').click();
}
}
})
can someone suggest what can be the possible reason that .click() method is not identified by cypress. I am using cypress version 7
How you do this depends on the structure of the HTML.
It looks like you may have this sort of hierarchy
<div class="products">
<div class="product">
<h4 class="product-name">Almonds</h4>
<button>Add to cart</button>
</div>
<div class="product">
<h4 class="product-name">Cashews</h4>
<button>Add to cart</button>
</div>
</div>
Take the product section containing the text you want, and within that find the products <button>.
Your test might be
cy.contains('.product', 'Cashews') // pick the <div class="product"> with required text
.find('button') // inside the product, find it's button
.click()
You can use .filter() to find your element and click it:
cy.get('h4.product-name').filter(':contains("Cashews")').click()

What is this.loading = false?

I am using Laravel and Vue.
When I was searching on the internet I saw the following code.
<template>
<div>
<h3 class="text-center">Create Movie</h3>
<div class="row">
<div class="col-md-6">
<form #submit.prevent="createMovie">
<div class="form-group">
<label>Name</label>
<input type="text" class="form-control" v-model="movie.name">
</div>
<div class="form-group">
<label>Director</label>
<input type="text" class="form-control" v-model="movie.director">
</div>
<button type="submit" class="btn btn-primary">Create movie</button>
</form>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
movie: {}
}
},
methods: {
createMovie() {
this.axios
.post('http://localhost:8000/api/movie/create', this.movie)
.then(response => (
this.$router.push({name: 'movie'})
))
.catch(error => console.log(error))
.finally(() => this.loading = false)
}
}
}
I am trying to find out what the last line
.finally(() => this.loading = false)
is doing. I am searching on the internet but I can't find what it does. Also, I tried running the code without the last line however, it did not make any change.
Can someone please tell me what this is doing and when it is useful?
Without seeing the associated Vue template we cannot explain what it is doing exactly, however, we can be fairly confident that the value of loading will be used to show/hide some sort of overlay or activity spinner.
The purpose of the overlay/activity spinner is to provide visual feedback to the user that something is happening. This is useful when loading large amounts of data into your page, or when you perform a long running process (such as uploading a large file for example). So rather than the user seeing nothing on first page load, or clicking a button and wondering if it worked, they are provided with something to let them know that something is happening.
A basic example of what this might look like in the Vue template could be:
// if the value of loading is true, show this
<div v-if="this.loading">Loading, please wait ...</div>
// otherwise show this
<div v-else>Other content</div>
Your example is setting the value of loading to false once a response has been received from your axios request. You would probably want to set the value of loading to true prior to making the request to show an overlay/activity spinner.
Uncaught (in promise) ReferenceError: ture is not defined
You have a typo, it should be true not ture.

ElementScrolled Event is not being fired with CdkScrollable and mat-side-nav

I have the following DOM Configuration
<div class="sticky-nav-wrapper">
<app-header (notificationClick)="notificationData=$event; sidenav.toggle()"></app-header>
</div>
<mat-sidenav-container>
<mat-sidenav-content>
<router-outlet></router-outlet>
</mat-sidenav-content>
<mat-sidenav #sidenav mode="over">
<app-notifications [notificationData]="notificationData" [opened]="sidenav.opened" (notificationItemClick)="sidenav.close()"></app-notifications>
</mat-sidenav>
</mat-sidenav-container>
In my app-notification component which is inside mat-sidenav, I want to listen to the scroll. Following is my template for app-notification
<div class="class-notification-container" cdkScrollable>
<div *ngIf="notifications && notifications.length > 0">
<div *ngFor="let item of notifications">
<div class="class-notification" [ngClass]="{'class-notification-new': item.localStatus === 'new','class-notification-old': item.localStatus === 'seen'}" (click)="onNotificationClick(item)">
<app-avatar [imageId]="item?.fromUser?.id"></app-avatar>
</div>
<mat-divider></mat-divider>
</div>
</div>
</div>
In my app-notification.ts:
#ViewChild(CdkScrollable) notificationContainer: CdkScrollable;
and
ngAfterViewInit(): void {
this.notificationContainer.elementScrolled()
.subscribe((scrollPosition) => {
this.ngZone.run(() => {
if (!this.endOfNotifications) {
this.pageNumber = this.pageNumber + 1;
this.getNotifications();
}
});
}, (error) => {});
}
However, this subscription is never invoked, no matter how much I scroll inside the mat-side-nav. The docs says that the CdkScrollable directive emits an observable on host elemnet scroll.
Returns observable that emits when a scroll event is fired on the host
element.
The other alternative is to listen to the scrollDispatcher. The issue however is scrolldispatcher is sending events on window scroll and not specifically on side-nav scroll.
I seem to be doing everything OK as per the doc. Can someone please suggest where the things are going wrong. I specifically want to listen to the scroll of my component within the side-nav i.e. app-notification component. Also I do not want to listen to the window scroll or to say the mat-side-nav-content scroll.
It may be you're listening on too high of a parent.
Have you tried listening on the div with the *ngFor ?
<div >
<div *ngIf="notifications && notifications.length > 0">
<div class="class-notification-container" cdkScrollable *ngFor="let item of notifications">
<div class="class-notification" [ngClass]="{'class-notification-new': item.localStatus === 'new','class-notification-old': item.localStatus === 'seen'}" (click)="onNotificationClick(item)">
<app-avatar [imageId]="item?.fromUser?.id"></app-avatar>
</div>
<mat-divider></mat-divider>
</div>
</div>
</div>

Using $state.go to call ui-router child state

So I am trying to have a sub menu that changes the state of the child view, and I am failing.
So set up the following function to call $state.go
$scope.stateGo = function (state) {
console.log("Loadting state " + state)
$state.go(state);
}
And I can see on the console that the correct (or what I think is the correct state name) is called
Loadting state board.stat
However, nothing at all seems to be happening with the actual router. If I change it so a parent state. It does work. For example, if I set it to board it works.
The files that contains the ui-views looks as follows:
<div ui-view="topmenu">
</div>
</div>
<div id="mainView">
<div ui-view="mainView">
</div>
<div style="height: 100px;"> </div>
</div>
<div class="col-md-offset-3 footer navbar-fixed-bottom" id="adView">
<div ui-view="adView">
</div>
</div>
The state config:
.state('board', {
url: "/view/board",
views: {
topmenu: {templateUrl: "views/partials/menu-board"},
mainView: {templateUrl: "views/partials/welcome"},
adView: {templateUrl: "views/partials/amazon-banner"}
},
//controller: 'testCtrl'
})
.state('board.stat', {
url: "/stat",
views: {
topmenu: {templateUrl: "views/partials/menu-board"},
mainView: {templateUrl: "views/partials/stat"},
adView: {templateUrl: "views/partials/amazon-banner"}
}
})
Am I missing something, should a call to $state.go('board.stat') get ui-router to load stat into mainView? And if so, any idea why it isn't?
======================= EDIT ===================
OK, think I might be doing it wrong, but not certain how...
Changed the buttons to use ui-href
<a ui-sref="board.stat" ui-sref-active="active" class="btn btn-xlarge" ><button class="btn btn-primary btn-circle btn-xl"><i class="fa fa-bar-chart fa-1x"></i><br><h6>Stats</h6></button></a>
<a ui-sref="board.quickbet" ui-sref-active="active" class="btn btn-xlarge" ><button class="btn btn-primary btn-circle btn-xl"><i class="fa fa-plus fa-1x"></i><br><h6>Quick bet</h6></button></a>
So same layout as earlier, but it seems like both child states are loaded ONLY when I enter parent state.
So I added some debugging for the state using the following two functions:
$rootScope.$on('$stateChangeError',
function(event, toState, toParams, fromState, fromParams, error){
console.log("State error: " + error);
})
$rootScope.$on('$viewContentLoading',
function(event, viewConfig){
// Access to all the view config properties.
// and one special property 'targetView'
// viewConfig.targetView
console.log("State event: " + viewConfig)
});
But the only output I get is:
2 State event: mainView#
2 State event: adView#
But when I press the buttons nothing seems to happen
Think I solved it.
Well, I did solve it, but I'm surprised by how it works.
So the problems seems to be that I thought the child state would reload the hole set of ui-views, it does not, and trying to do so seems to do nothing. I have no clue if this is correct interpretation, or expected behaviour, but this is what worked for me.
Load parent state with all three views (named topmenu, mainView and adView) (happens automatically).
Add a new ui-view (in my case in stats.ejs loaded on mainView)
This is your stats and bets
<ui-view></ui-view>
Last thing is to ONLY update the parts of the screen you wan't, so not mainView, but targeting the new ui-view in stats.ejs
.state('board.stat', {
templateUrl: "views/partials/stat"
})
And it works, just not certain if this has to do with the DOM or something, but it worked in my case

Back to Top Link, Dynamically Created, with Scroll

SUMMARY:
I need to insert a "Back to Top" links after every <div class="wrapSection">. I've been successful using the following:
<script>
$('.wrapSection').after('
<div class="backToTop clearfix">
Back To Top
</div>
');
</script>
However, I want to use a smooth scroll when clicking 'Back to Top.' With that in mind, I tried the following:
<script>
$('.wrapSection').after('
<div class="backToTop clearfix">
<a href="javascript:void(0)" onclick="goToByScroll('top')" class="up">
Back To Top
</a>
</div>
');
</script>
That does not work. Being a jQuery rookie, I did what seemed logical, which seems to never be the correct answer.
IN A NUTSHELL
More or less, I need this to appear, dynamically, after every <div class="wrapSection">:
<div class="backToTop">
<a class="top" href="javascript:void(0)" onclick="goToByScroll('top')">
Back to Top
</a>
</div>
This is the solution I came up with:
โ€‹$(document).ready(function() {
// Markup to add each time - just give the element a class to attach an event to
var top_html = '<div class="backToTop">Back To Top</div>';
$(".wrapSection").after(top_html);
// Use event delegation (see http://api.jquery.com/on/)
$("body").on("click", ".top", function(e) {
e.preventDefault();
$("html,body").animate({ scrollTop: 0 }, "slow");
});
});โ€‹
You can try a jsFiddle here: http://jsfiddle.net/F9pDw/

Resources