I am performing a test on a dropdown animated menu using Playwright. The test purpose is to know if the last menu item appears in the viewport. The menu is higher than the viewport so the user needs to scroll the page in order to see it.
I have two issues:
- It takes time till the menu's last item is seen because the animation takes time.
- In order to know if the last menu item is in the viewport I use the following function. the issue is that it takes about 1.5 - 2 seconds to receive the item's position.
At the moment I am using waitForTimeout
But I want to know how I can replace them with methods that will recognize that the previous stage is ended.
The functions:
async function openMobileMenu( page ) {
await page.waitForSelector( '[aria-label="Menu Toggle"]' );
await page.locator( '[aria-label="Menu Toggle"]' ).click();
await page.locator( '.nav-menu--dropdown.nav-menu__container .submenu-class-for-playwright-test' ).first().click();
await page.locator( '.nav-menu--dropdown.nav-menu__container .subsubmenu-class-for-playwright-test' ).first().click();
// Estimated timeout needed for the menu to open.
await page.waitForTimeout( 1500 ); // This is the first time out I want to replace.
// Scroll to the bottom of the page.
await page.evaluate( () => window.scrollBy( { top: 2000, behaviour: 'instant' } ) );
// Scroll to the bottom of the sticky menu.
await page.evaluate( () => {
const mobileMenu = document.querySelector( '.nav-menu--dropdown.nav-menu__container' );
mobileMenu.scrollBy( { top: 2000, behaviour: 'instant' } );
} );
// Estimated timeout needed scroll down to the bottom of the screen.
await page.waitForTimeout( 1500 ); // This is the second time out I want to replace.
}
//This is how I calculate if the last menu item is in the viewport.
async function isItemInViewport( page, lastMenuItem ) {
return await page.evaluate( ( lastMenuItem ) => {
let isVisible = false;
const element = document.querySelector( lastMenuItem );
if ( element ) {
const rect = element.getBoundingClientRect();
if ( rect.top >= 0 && rect.left >= 0 ) {
const vw = Math.max( document.documentElement.clientWidth || 0, window.innerWidth || 0 );
const vh = Math.max( document.documentElement.clientHeight || 0, window.innerHeight || 0 );
if ( rect.right <= vw && rect.bottom <= vh ) {
isVisible = true;
}
}
}
return isVisible;
}, lastMenuItem );
}
The test:
await openMobileMenu( page );
const lastMenuItem = '.nav-menu--dropdown.nav-menu__container .lastitem-class-for-playwright-test';
// Assertion
await expect( await isItemInViewport( page, lastMenuItem ) ).toBeTruthy();
I have tried using waitForFunction
and also adding a timeout to the scroll event but it didn't help.
This is what I have tried so far:
async function openMobileMenu( page, lastMenuItem ) {
await page.waitForSelector( '[aria-label="Menu Toggle"]' );
const menuToggle = await page.locator( '[aria-label="Menu Toggle"]' );
await menuToggle.click();
await page.locator( '.-menu--dropdown.nav-menu__container .submenu-class-for-playwright-test' ).first().click();
const subSubMenuClass = '.nav-menu--dropdown.nav-menu__container .subsubmenu-class-for-playwright-test';
const subSubMenu = page.waitForSelector( subSubMenuClass );
await subSubMenu.waitForElementState( 'stable' ); // receive this error: subSubMenu.waitForElementState is not a function
await page.locator( subSubMenuClass ).first().click();
// Estimated timeout needed for the menu to open.
// await page.waitForTimeout( 1500 );
// await subSubMenu.waitFor( { state: 'attached' } );
await page.waitForLoadState( 'networkidle' ); // this wont help because the page is already fully loaded
// Scroll to the bottom of the page.
await page.evaluate( () => window.scrollBy( { top: 2000, behaviour: 'instant' } ) );