percy / cli

The Percy CLI is used to interact with, and upload snapshots to, percy.io via the command line.
https://docs.percy.io/docs/cli-overview
70 stars 43 forks source link

Unable to capture the dialog web component when it is nested inside a shadow DOM #1670

Closed hoaiduc87 closed 1 month ago

hoaiduc87 commented 2 months ago

The problem

We are using a micro-frontend architecture, where each sub-project is mounted inside a Shadow DOM. Our UI library is built with web components, with each component also attached inside its own Shadow DOM. We are using Cypress for functional testing and integrating with Percy for visual testing.

In this example, I have a button and a dialog, both built as web components and attached inside their respective Shadow DOMs. I want to render this dialog in my sub-project, which is itself attached inside a parent Shadow DOM. When I click the button, the dialog should open.

However, the problem is that the dialog does not appear in the snapshot captured by Percy, although the button remains visible. I have found that the dialog appears correctly when it is wrapped by only one Shadow DOM. I also perform some assertions with Cypress to ensure the elements are visible. It seems like Cypress handles nested Shadow DOMs properly, except for Percy.

Could you help me determine the reason for this issue and suggest how to fix it? Does it require any additional configuration?

Environment

Debug logs

35431593_build_35431593_cli_f357c65217906f7679bd4ebbf77e9a2818531084

Code to reproduce issue

<html>
  <head>
    <script>
      class MyDialog extends HTMLElement {
        constructor() {
          super()
          this.attachShadow({ mode: 'open' })

          const template = document.createElement('template')
          template.innerHTML = `
            <dialog>
              <h2>My dialog</h2>
              <button id="close">Close</button>
            </dialog>
          `
          this.shadowRoot.appendChild(template.content.cloneNode(true))

          this.dialog = this.shadowRoot.querySelector('dialog')

          this.shadowRoot.getElementById('close').addEventListener('click', () => {
            this.close()
          })
        }

        open() {
          this.dialog.showModal()
        }

        close() {
          this.dialog.close()
        }
      }

      customElements.define('my-dialog', MyDialog)
    </script>
    <script>
      class MyButton extends HTMLElement {
        constructor() {
          super()
          this.attachShadow({ mode: 'open' })

          const template = document.createElement('template')
          template.innerHTML = '<button>Open dialog</button>'
          this.shadowRoot.appendChild(template.content.cloneNode(true))

          this.button = this.shadowRoot.querySelector('button')
        }

        connectedCallback() {
          this.button.addEventListener('click', () => {
            this.dispatchEvent(new CustomEvent('my-click-event'))
          })
        }

        disconnectedCallback() {
          this.button.removeEventListener('click')
        }
      }

      customElements.define('my-button', MyButton)
    </script>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        const root = document.querySelector('#root')
        const shadowRoot = root.attachShadow({ mode: 'open' })

        const myDialog = document.createElement('my-dialog')

        const button = document.createElement('my-button')
        button.addEventListener('my-click-event', () => {
          myDialog.open()
        })

        shadowRoot.appendChild(button)
        shadowRoot.appendChild(myDialog)
      })
    </script>
  </head>
  <body>
    <div>
      <h1>Dialog nested in Shadow DOM</h1>
      <div id="root"></div>
    </div>
  </body>
</html>
describe('Simple Cypress Test', () => {
  it.only('Take snapshot', () => {
    cy.visit('http://localhost:8008')
    cy.contains('Dialog nested in Shadow DOM').should('be.visible')
    cy.contains('My dialog').should('not.be.visible')
    cy.wait(2000)
    cy.contains('Open dialog').should('be.visible').click()
    cy.contains('My dialog').should('be.visible')
    cy.wait(2000)
    cy.screenshot() // Can see the dialog in the snapshot
    cy.percySnapshot() // Can't see the dialog in Percy build although the button is visible
  })
})
github-actions[bot] commented 1 month ago

This issue is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

hoaiduc87 commented 1 month ago

It’s waiting for someone to take a look

ninadbstack commented 1 month ago

Hey @hoaiduc87, From the build logs that you have attached I see the snapshot is javascript enabled. When you have javascript enabled snapshots, percy would run javascript in percy's browser - which means java script [ especially what you have added in domContentLoaded event would rerun and reconstuct the DOM - and this reconstruction would put dom in default state where button is not pressed.

You can use with javascript disabled and it would work fine. [ but in my testing backdrop and the location of popup does not seem correct ]

Another note being percy currently does not support CustomElements specifically in js disabled case - so any kind of css that relies on defined? will not work. and the custom elements would be treated as divs [ as browser does when element is not defined ]

hoaiduc87 commented 1 month ago

Thanks for the explaination @ninadbstack I’m not sure why, but the last time I tried with JavaScript disabled, it was unable to capture the dialog element. I tried again today, and it seems to work, except for the backdrop and position. It might be sufficient for verifying the dialog content. Is it impossible to get the screenshot as the browser does when javascript is disabled 🥲

ninadbstack commented 1 month ago

@hoaiduc87 no, currently percy by default works in Snapshot mode where we capture and replicate DOM across multiple browsers. If you want to take screenshot of your test browser - its currently only supported when you are using percy with Browserstack Automate [ it comes with some other restrictions ]