-1

My Html code has Button-tags that have same id "hoge".

If you get the selector from the Chrome Dev Tool, it will be the same for both "#hoge".

<html>
  <body>
    <button id="hoge">Hoge</button>
    <div class="shadow">
      #shadow-root (open)
        <button id="hoge">Hoge</button>
    </div>
  </body>
</html>

I want to get element of button-tag in shadow dom with puppeteer. But, my javascript code gets element of 1st button.

const element = page.waitForSelector("pierce/#hoge");

This is not what I want.

I'm guessing it's because you didn't specify a unique selector, but i don't know what is unique selector for puppeteer.

If you know how to solve this problem, please let me know.

Pirikara
  • 343
  • 3
  • 12
  • 3
    How is the second button with `id="hoge"` created? An `id` is a unique identifier and should only exist once per page. – Silvan Bregy Jan 26 '22 at 10:47
  • Straddling the Shadow Root boundary, the same ID can exist on the same page. https://stackoverflow.com/questions/56023322/are-duplicate-ids-allowed-in-separate-shadow-roots – Pirikara Jan 27 '22 at 02:57

1 Answers1

3

Long story short

I work with puppeteer a lot and wanted this knowlegde to be in my bag. One way to select a shadow Element is by accessing the parent DOM Node's shadowRoot property. The answer is based on this article.

Accessing Shadow Root property

For your html example this does the trick:

const button = document.querySelector('.shadow').shadowRoot.querySelector('#hoge')

waiting

Waiting though is a little more complicated but can be acquired using page.waitForFunction().

Working Sandbox

I wrote this full working sandbox example on how to wait for a certain shadowRoot element.

  • index.html (located in same directory as app.js)
<html>
<head>
    <script>
        // attach shadowRoot after 6 seconds for emulating waiting..
        setTimeout(() => {

            const btn = document.getElementById('hoge')

            const container = document.getElementsByClassName('shadow')[0]

            const shadowRoot = container.attachShadow({
                mode: 'open'
            })

            shadowRoot.innerHTML = `<button id="hoge" onClick="doStuff()">hoge2</button>`

            console.log('attached!.')
        }, 6000)


        function doStuff() {
            alert('shadow button clicked!')
        }
    </script>
</head>
<body>
    <button id="hoge">Hoge</button>
    <div class="shadow">

    </div>
</body>
</html>
  • app.js (located in same directory as index.html)
var express = require('express')
var { join } = require('path')
var puppeteer = require('puppeteer')

//utility..
const wait = (seconds) => {
    console.log('waiting', seconds, 'seconds')
    return new Promise((res, rej) => {
        setTimeout(res, seconds * 1000)
    })
}


const runPuppeteer = async() => {

    const browser = await puppeteer.launch({
        defaultViewport: null,
        headless: false
    })

    const page = await browser.newPage()

    await page.goto('http://127.0.0.1:5000')
    await wait(3)

    console.log('page opened..')

    // only execute this function within a page context!.
    // for example in page.evaluate() OR page.waitForFunction etc.
    // don't forget to pass the selector args to the page context function!
    const selectShadowElement = (containerSelector, elementSelector) => {
        try {

            // get the container
            const container = document.querySelector(containerSelector)

            // Here's the important part, select the shadow by the parentnode of the 
            // actual shadow root and search within the shadowroot which is like another DOM!,
            return container.shadowRoot.querySelector(elementSelector)
        } catch (err) {
            return null
        }
    }

    console.log('waiting for shadow elemetn now.')

    const containerSelector = '.shadow'
    const elementSelector = '#hoge'
    const result = await page.waitForFunction(selectShadowElement, { timeout: 15 * 1000 }, containerSelector, elementSelector)

    if (!result) {
        console.error('Shadow element not found..')
        return
    }

    // since waiting succeeded we can get the elemtn now.
    const element = await page.evaluateHandle(selectShadowElement, containerSelector, elementSelector)

    try {

        // click the element.
        await element.click()
        console.log('clicked')

    } catch (err) {
        console.log('failed to click..')
    }

    await wait(10)

}

var app = express()

app.get('/', (req, res) => {
    res.sendFile(join(__dirname, 'index.html'))
})

app.listen(5000, '127.0.0.1', () => {
    console.log('listening!')

    runPuppeteer()
})

Start example

 $ npm i express puppeteer
 $ node app.js

Make sure to use headless:false option to see what's happening.

The application does this:

  • start a small express server only serving index.html on /
  • open puppeteer after server has started and wait for the shadow root element to appear.
  • Once it appeared, it gets clicked and an alert() is shown. => success!

Browser Support

Tested with chrome.

Cheers ' ^^

Silvan Bregy
  • 2,544
  • 1
  • 8
  • 21