0

I want to use MutationObserver with Cypress because it feels like an appropriate mechanism to use for detecting specific changes occurred to the DOM and then interact with element(s) that were added.

Assume that I have a UI that has 3 buttons and a div.new-elements-wrapper. Clicking each button results in a new div element into div.new-elements-wrapper with a data-type attribute of value button-a, button-b, and button-c for buttons A, B, and C, respectively. It's possible that these elements can be randomly inserted, not just appended to the end of the div.new-elements-wrapper container.

I want to write a test that effectively is:

  1. Click button A.
  2. Wait for new element to be added to div.new-elements-wrapper region.
  3. When new element appears in region, validate that it has a data-type attribute and that it's value is button-a.

Repeat for buttons B, and C.

For step 2, I don't want to continue on until a MutationObserver, configured to look specifically at div.new-elements-wrapper, has a single node in a mutation's nodesAddedList. Then, in step 3, validate the data-type attribute is button-a.

For the Cypress wizards out there, how would you do this?

EDIT: Note that there is no uniquely identifying information available on these components that are added. If there are two elements, both button-a, then the only way to know which was added would be to informed by a MutationObserver.

Lester Peabody
  • 1,868
  • 3
  • 20
  • 42
  • The premise of your question feels contrived - there are numerous ways that Cypress can handle the scenario you describe. – Cat.Crimes Jun 19 '23 at 21:22
  • It is admittedly slightly contrived, but it does map into an actual use case I have. If you feel like you have an answer I would love to test it. – Lester Peabody Jun 20 '23 at 00:52

2 Answers2

6

The MutationObserver API would need a spy or a promise to work reliably in a Cypress test, since it invokes it's handler asynchronously to the test.

This is how I would implement a mutation observer in Cypress, using * to observe all descendants of a DOM element and jQuery .not() to find the differences.

Cypress.Commands.add('watchForMutation', (selector, action, options = {}) => {
  const domTree = cy.$$(`${selector} *`)
  let diff;
  return cy.wrap(action(), options)
    .should(() => {
      const newDomTree = cy.$$(`${selector} *`)
      diff = newDomTree.not(domTree)
      expect(diff.length).to.be.gt(0)             // retry until true or timeout
    })
    .then(() => diff)
})

Use like this

cy.watchForMutation('#container', () => {
  cy.get('button').click()
}, {timeout:6000})
.then(newElement => {
  ...
})

Notes

  • the selector * works for all descendants (like subTree:true option)
  • the should() clause waits for the new element within options.timeout time frame
  • options.timeout defaults to standard command timeout of 4000 ms if not passed
  • the .then(() => diff) passes back the new element(s)
3

Cypress already does this with normal commands.

Here is a sample page that does what you describe

  • connects buttons to a function that adds a <div> to <body>
  • does the add asynchronously
  • gives the new <div> a (predictable) attribute for selecting
<body>
  <button onclick="add()">a</button>
  <button onclick="add()">b</button>
  <button onclick="add()">c</button>
  <script>
    let id = 0
    function add() {
      setTimeout(() => {
        const div = document.createElement('div')
        div.innerText = `div ${id}`
        div.setAttribute('data-test-id', id++)
        const body = document.querySelector('body')
        body.appendChild(div)      
      }, 1000)
    }
  </script>
</body>

Here's the way Cypress observes the mutations to the DOM

cy.contains('button', 'a').click()
cy.contains('[data-test-id="0"]', 'div 0')   // retries up to 4 seconds

cy.contains('button', 'b').click()
cy.contains('[data-test-id="1"]', 'div 1')   // retries up to 4 seconds

cy.contains('button', 'c').click()
cy.contains('[data-test-id="2"]', 'div 2')   // retries up to 4 seconds    

enter image description here

Woden
  • 168
  • 6
  • Ah. Completely new to Cypress so appreciate the insight. Will give this a whirl when I get home. – Lester Peabody Jun 18 '23 at 21:48
  • Actually. How does this account for testing the element that was just added if that element is otherwise indistinguishable from sibling elements (e.g. if the same widget was added multiple times and no unique id is added)? – Lester Peabody Jun 18 '23 at 21:50
  • Yes I was pondering the same thing - so if the `data-test-id` is left off the simulation and there are a bunch of other anonymous `
    ` kicking around.
    – Woden Jun 18 '23 at 21:51
  • Maybe you would count the `
    ` at the top of the test and use an assertion on `count +1` after clicking - `cy.get('div').should('have.length', previousCount + 1)`.
    – Woden Jun 18 '23 at 21:54
  • If you update this to utilize `MutationObserver` I will mark this as accepted. – Lester Peabody Jun 19 '23 at 21:12