How to handle elements inside Shadow DOM from Selenium

前端 未结 3 1255
青春惊慌失措
青春惊慌失措 2020-11-28 14:03

I want to automate file download completion checking in chromedriver. HTML of each entry in downloads list looks like



        
相关标签:
3条回答
  • 2020-11-28 14:27

    I would add this as a comment but I don't have enough reputation points--

    The answers by Eduard Florinescu works well with the caveat that once you're inside a shadowRoot, you only have the selenium methods available that correspond to the available JS methods--mainly select by id.

    To get around this I wrote a longer JS function in a python string and used native JS methods and attributes (find by id, children + indexing etc.) to get the element I ultimately needed.

    You can use this method to also access shadowRoots of child elements and so on when the JS string is run using driver.execute_script()

    0 讨论(0)
  • 2020-11-28 14:31

    You can use the driver.executeScript() method to access the HTML elements and JavaScript objects in your web page.

    In the exemple below, executeScript will return in a Promise the Node List of all <a> elements present in the Shadow tree of element which id is host. Then you can perform you assertion test:

    it( 'check shadow root content', function () 
    {
        return driver.executeScript( function ()
        {
            return host.shadowRoot.querySelectorAll( 'a' ).then( function ( n ) 
            {
                return expect( n ).to.have.length( 3 )
            }
        } )
    } )     
    

    Note: I don't know Python so I've used the JavaScript syntax but it should work the same way.

    0 讨论(0)
  • 2020-11-28 14:36

    Sometimes the shadow root elements are nested and the second shadow root is not visible in document root, but is available in its parent accessed shadow root. I think is better to use the selenium selectors and inject the script just to take the shadow root:

    def expand_shadow_element(element):
      shadow_root = driver.execute_script('return arguments[0].shadowRoot', element)
      return shadow_root
    
    outer = expand_shadow_element(driver.find_element_by_css_selector("#test_button"))
    inner = outer.find_element_by_id("inner_button")
    inner.click()
    

    To put this into perspective I just added a testable example with Chrome's download page, clicking the search button needs open 3 nested shadow root elements:

    import selenium
    from selenium import webdriver
    driver = webdriver.Chrome()
    
    
    def expand_shadow_element(element):
      shadow_root = driver.execute_script('return arguments[0].shadowRoot', element)
      return shadow_root
    
    driver.get("chrome://downloads")
    root1 = driver.find_element_by_tag_name('downloads-manager')
    shadow_root1 = expand_shadow_element(root1)
    
    root2 = shadow_root1.find_element_by_css_selector('downloads-toolbar')
    shadow_root2 = expand_shadow_element(root2)
    
    root3 = shadow_root2.find_element_by_css_selector('cr-search-field')
    shadow_root3 = expand_shadow_element(root3)
    
    search_button = shadow_root3.find_element_by_css_selector("#search-button")
    search_button.click()
    

    Doing the same approach suggested in the other answers has the drawback that it hard-codes the queries, is less readable and you cannot use the intermediary selections for other actions:

    search_button = driver.execute_script('return document.querySelector("downloads-manager").shadowRoot.querySelector("downloads-toolbar").shadowRoot.querySelector("cr-search-field").shadowRoot.querySelector("#search-button")')
    search_button.click()
    

    later edit:

    I recently try to access the content settings(see code below) and it has more than one shadow root elements imbricated now you cannot access one without first expanding the other, when you usually have also dynamic content and more than 3 shadow elements one into another it makes impossible automation. The answer above use to work a few time ago but is enough for just one element to change position and you need to always go with inspect element an ho up the tree an see if it is in a shadow root, automation nightmare.

    Not only was hard to find just the content settings due to the shadowroots and dynamic change when you find the button is not clickable at this point.

    driver = webdriver.Chrome()
    
    
    def expand_shadow_element(element):
      shadow_root = driver.execute_script('return arguments[0].shadowRoot', element)
      return shadow_root
    
    driver.get("chrome://settings")
    root1 = driver.find_element_by_tag_name('settings-ui')
    shadow_root1 = expand_shadow_element(root1)
    
    root2 = shadow_root1.find_element_by_css_selector('[page-name="Settings"]')
    shadow_root2 = expand_shadow_element(root2)
    
    root3 = shadow_root2.find_element_by_id('search')
    shadow_root3 = expand_shadow_element(root3)
    
    search_button = shadow_root3.find_element_by_id("searchTerm")
    search_button.click()
    
    text_area = shadow_root3.find_element_by_id('searchInput')
    text_area.send_keys("content settings")
    
    root0 = shadow_root1.find_element_by_id('main')
    shadow_root0_s = expand_shadow_element(root0)
    
    
    root1_p = shadow_root0_s.find_element_by_css_selector('settings-basic-page')
    shadow_root1_p = expand_shadow_element(root1_p)
    
    
    root1_s = shadow_root1_p.find_element_by_css_selector('settings-privacy-page')
    shadow_root1_s = expand_shadow_element(root1_s)
    
    content_settings_div = shadow_root1_s.find_element_by_css_selector('#site-settings-subpage-trigger')
    content_settings = content_settings_div.find_element_by_css_selector("button")
    content_settings.click()
    
    0 讨论(0)
提交回复
热议问题