Search code examples
selectplaywrightautomation-testingselect-options

Playwright page.locator selectOption does not work inside imported function


I have been having trouble with the elements with Playwright. I must click the div below the select before I can use the selectOption method from Playwright. However I created a function and imported it into the test to accomplish the two steps together but only the click step works. The select option step fails to work inside the function but it will work outside the function in the test itself.

The select element has a style of display:none. The result is that playwright refuses to select an option because it says the element is not visible. In fact the element is visible and the workaround seems to be to click on the div element below the select to get the options to be visible.

so I made a function that uses the select element's locator to find the div below it using .eval(el=>{ //click the div below the select}). This seems to work as the options then show up on the screen as a drop down.

This then allowed me to use myLocator.selectOption('thevalue') BUT this only works if the selectOption step occurs inside the test and not inside the imported function. I have no idea why. The page context is preserved because page is passed to the function and successfully used inside the function for clicking on the div. But Playwright refuses to select the option if the selectOption step occurs inside the function.

import { customSelectOption } from '../../../utils/DataEntry/customSelectOption.ts'
// other standard imports for playwright test

test('attempt to use select', async ({ page }) => {

    //......navigate to website.. etc...

    // create page object and set up locator
    const selectLocators = new SelectLocators(page);
    const selectLocatorObj = selectLocators.firstSelectElement;


    // clicks the div that occurs after the select 
    await customSelectOption(page, selectLocatorObj, 'desired Value To Select');

    // the step to use selectOption will work here but not inside the customSelectOption function
    await page.locator(selectLocatorString).selectOption(value);
});


export async function customSelectOption(page: Page, select: Locator, value: string | {}) {

    // convert locator back to simple string css locator - which looks like this 'Locator@#theID'
    // removing the first 8 characters results in the css locator as a string: #theID
    let selectLocatorString = select.toString();
    selectLocatorString = selectLocatorString.slice(8);

    // create a new string that uses the css locator to find the next sibling div
    let selectOptionsDiv = selectLocatorString + '+ div';

    // click on that div (This step always works)
    await page.locator(selectOptionsDiv).click();

    // the result is the the dropdown options on the select are now visible on the screen

    //HOWEVER: this next step does not work when it occurs in this function. 
    // If this next occurs after the function but inside the test then it will work, but why?
    await page.locator(selectLocatorString).selectOption(value);

}

Solution

  • I decided that I would just make my own version of selectOption because Playwright's isn't working the way it should. essentially what I had to do was get the css string from the locator by turning the locator to a string and slicing off the extraneous characters that playwright adds. Then using the selectors I was able to click on the div that follows the select.

    to get the select to work I also had to remove the style from the select itself since it was set to display:none.

    The next portion involves creating a function inside page.locator('#thelocator').evaluate(el =>{},[]). If you look at the evaluate method it allows you to pass in an array after the code block. What I did was just use evaluate the grab the body of the document so I have access to basically everything on the page. Then I created a function inside evaluate that allows you to use the choice select using an event. Because this is using typescript there is also an interface because typescript wants to know why type of element is being returned as a nodelist in the function. In this case the nodelist is simply a list of options that the for loop goes through looking for the value you want to change the select to. I also included a custom wait function I made using promises that allows you to determine the wait time in seconds because Playwright doesn't have one.

    export async function customSelectOption(page: Page, select: Locator, value: string, labelOrValue:string ) {
    
    // you can get the locator string with mylocator.toString()
    // which will look like this:  Locator@select[id="theid"]
    let selectLocatorString = select.toString()
    
    selectLocatorString = selectLocatorString.slice(8)
    let selectOptionsDiv = selectLocatorString + '+ div'
    await page.locator(selectOptionsDiv).click()
    
    await wait(2)
    
    let arr = [selectLocatorString, labelOrValue ,value ]
    
    await page.locator('body').evaluate((dom,arr) => {
    
        dom = dom as HTMLElement
        
        let selectLocatorString = arr[0]
        let attr = arr[1]
        let value = arr[2]
    
        dom.querySelector(selectLocatorString)
        // remove style because element has css style of display:none
        dom?.removeAttribute('style')
    
        interface MyOptionElement extends HTMLOptionElement {
            [key: string]: any;
          }
        function selectOptionByAttribute(css:string, attr:string, value:string, dom:HTMLElement) {
        
            var select:HTMLSelectElement | null = dom.querySelector(css) as HTMLSelectElement
            var opts: NodeListOf<MyOptionElement> | undefined = select?.querySelectorAll(`option`)
        
            if(opts !== undefined){
                for (let i = 0; i < opts.length; i++) {
                    console.log(opts[i][attr])
    
                    let optionValue = opts[i][attr]
    
                    if (optionValue.toUpperCase() == value.toUpperCase()) {
                        select.selectedIndex = i;
                        // trigger onchange for chosen select
                        var event = new Event('change');
                        select?.dispatchEvent(event)
                        return true;
                    }
                }
            }
            
            return false;
        }
    
        return selectOptionByAttribute(selectLocatorString, attr, value, dom)
    
    }, arr)
    

    }