zombie icon indicating copy to clipboard operation
zombie copied to clipboard

How to assert after DOM changes?

Open mkondel opened this issue 7 years ago • 7 comments

What is the proper way to write tests when the DOM changes? No matter how long I wait in my tests, Zombie.js only sees the 1st "static" HTML page.

In my application, there is a script that starts when page 1st loads. The script calls a webservice, reads the response, and then populates a div in the HTML. When using a manual browser, the page loads, script runs and images are displayed, all in under 2 seconds.

const browser = new Browser({waitDuration: 30*1000})

browser.visit('/', function(){
    browser.wait(done, function() {
        return browser.assert.elements('img', 3)
    })
})

The "static" HTML has 2 images, header and footer. There should be a 3rd image that was added after 2s.

mkondel avatar Apr 19 '17 20:04 mkondel

Using setTimeout doesnt help me. The following still doesnt work:

browser.visit('/items')
.then(function() {
    setTimeout(function () {
        browser.assert.elements(................)
.....
        done()
    }, 10000)
})

The assert fails after waiting for 10 seconds.

mkondel avatar Apr 24 '17 23:04 mkondel

Each zombie browser window has it's own event loop that captures timeouts/intervals, and the then upon visit resolve only happens after the entire page is done anyways, so your timeout is just adding unnecessary wait time because nothing else is going to happen after .then.

You probably want to do something like:

https://github.com/assaf/zombie/blob/master/test/google_map_test.js#L33

If your third image isn't there in the resolved .then you might have some code in your script that jsdom doesn't support (what zombie uses under the hood).

leoj3n avatar Apr 28 '17 04:04 leoj3n

Thank you @leoj3n, it must be my code. I have not been able to get zombie to work for any test cases that have async content. This is what I tried based on your reply:

it('should find uploaded items on the /items page', function (done) {
    return browser.visit('/items').then(function() {
        return browser.wait(5000).then(function() {
            console.log(browser.html('.items'))
            return browser.assert.elements(`.item-space .items-icon`, number_of_items);
        })
    }).then(done,done)
})

The new content should have been in the '.items' element, but it's empty after any amount of wait time. I did also try with code like this, but it didntt help:

browser.wait({ element: '.items' });

My page script polls for changes in a text input box. If user types/deletes something, then a POST request is made, some items are returned, they should get appended to the '.items' element. I thought all I would need here is to wait longer.

mkondel avatar Apr 30 '17 19:04 mkondel

Zombie captures setTimeout and setInterval. Calling wait with a number tells the zombie event loop how long to wait before a hard cutoff, but it sounds like the event loop might be empty before that cutoff, and so it returns before then anyways.

To keep the event loop open longer, you might add a setTimeout in the before:

before(function(){
browser.visit('/path');
browser.window.setTimeout(function(){
  console.log('just keeping EL open');
}, 4000);
// Wait extra long for EL to wrap up.
return browser.wait({ duration: 4500 });

Then do your assertions in it.

Also, maybe try instantiating zombie like:

browser = new Browser({ waitDuration: 5000 });

What does your full describe look like, what code does the typing?

leoj3n avatar Apr 30 '17 20:04 leoj3n

I don't have a test for typing yet. Before, the route was giving the list of items to the view (the route did a search). My tests worked with that. I added search, the page now calls a new api end-point, that returns a list of items based on something.

When the page loads, and user does nothing, the page will do a search for "*". This returns all items, and populates HTML elements that display them.

@leoj3n Using your idea and before(), here is my full describe. It fails at the assert for browser.assert.elements() in the it(), never gets to done() at all. Log from this is like so:

      Item operations
Mon May 01 2017 09:59:46 GMT-0400 (EDT) visiting /items
Mon May 01 2017 10:00:01 GMT-0400 (EDT) just keeping EL open
Mon May 01 2017 10:00:01 GMT-0400 (EDT)<div class="items"></div>
        1) should find uploaded items on the /items page


  4 passing (1m)
  1 failing

  1) Items view tests Item management Item operations should find uploaded items on the /items page:

      AssertionError: Expected 3 elements matching ".item-space .items-icon", found 0
      + expected - actual

      -0
      +3
describe('Item operations', function () {
    //this is the same as the total number of items uploaded
    var number_of_items = test_image_local_paths.length;
    const browser = new Browser({ waitDuration: 30000 })

    before(function() {
        console.log((new Date())+' visiting /items');
        browser.visit('/items');
        browser.window.setTimeout(function(){
            console.log((new Date())+' just keeping EL open');
        }, 8000);
        // Wait extra long for EL to wrap up.
        return browser.wait({ duration: 8500 });
    })

    //each item should have icon, status, price, edit, title
    it('should find uploaded items on the /items page', function (done) {
        browser.assert.success();
        browser.assert.url('/items');
        //.items div should have content now, in the form of items
        console.log((new Date())+browser.html('.items'));
        //check that the page has the right number of item elements showing
        browser.assert.elements(`.item-space .items-icon`, number_of_items);
//.....
//never gets to any of the code after this
//.....
    })
})

mkondel avatar May 01 '17 14:05 mkondel

[Edited - found a workaround] We have a similar problem but with a synchronous DOM change, i.e. not going through a webservice call. So this does not seem to be related to a timeout issue, and explains why @mkondel cannot make this work with any amount of timeout or method to implement such a timeout.

An important information I can add is that this was working for us until zombie version 4.3.0, and fails since version 5.0.5, which has prevented us to upgrade so far.

@mkondel you could try version 4.3.0 and see if you can pass your tests with it, In which this would confirm that there is a problem with version 5.0.5.

Here is my test case, which passes in version 4.3.0 but fails in version 5.0.5:

<div id="container"></div>

<script type="text/javascript">
  var node = document.querySelector( "#container" );
  node.innerHTML = '<input id="charts" type="checkbox" />';
  node.onclick = click;
  
  function click() {
    // some stuff done here
  }
<script/>
describe( 'Controls Test Suite', function() {
  before( function( done ) {
    browser.visit( 'test/automated/zombie_js.html', done );
  } );
  
  it( 'should have charts input set to true after checking it', function() {
    browser.check( '#container input[type="checkbox"]' );
    
    expect( browser.query( '#container input[type="checkbox"]' ).checked ).to.be( true );
  } );
} );

To fix the issue with version 5.0.5, we had to add a return true to the click handler to prevent cancellation of the default action, which checks the box:

  function click() {
    // some stuff done here
    // prevent cancellation of default action
    return true;
  }

So the behavior of Zombie is different in version 4.3.0 and 5.0.5. But the question that remains is: Should Zombie interpret the undefined returned value of a click handler as false, cancelling the default action?

We also found another issue with version 5.0.5 which seems to be related to a behavior only found on IE. Does this mean that zombie 5.x now tries to behave like IE?

If so, is there a way to specify zombie's behavior to allow testing under different browser flavors?

These questions are out of the scope of this issue, should we open separate issues for this?

uiteoi avatar May 10 '17 16:05 uiteoi

According to this article: https://javascript.info/default-browser-action

"Not necessary to return true The value returned by an event handler is usually ignored. The only exception – is return false from a handler assigned using on. In all other cases, the return is not needed and it’s not processed anyhow."

Should I open a separate issue for this?

uiteoi avatar May 10 '17 19:05 uiteoi