Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing the contents of a temporary element with protractor

I'm trying to test the login page on my site using protractor.

If you log in incorrectly, the site displays a "toast" message that pops up for 5 seconds, then disappears (using $timeout).

I'm using the following test:

  describe('[login]', ()->
    it('should show a toast with an error if the password is wrong', ()->

      username = element(select.model("user.username"))
      password = element(select.model("user.password"))
      loginButton = $('button[type=\'submit\']')
      toast = $('.toaster')

      # Verify that the toast isn't visible yet
      expect(toast.isDisplayed()).toBe(false)

      username.sendKeys("admin")
      password.sendKeys("wrongpassword")
      loginButton.click().then(()->
        # Verify that toast appears and contains an error
        toastMessage = $('.toast-message')
        expect(toast.isDisplayed()).toBe(true)
        expect(toastMessage.getText()).toBe("Invalid password")
      )
    )
  )

The relevant markup (jade) is below:

.toaster(ng-show="messages.length")
  .toast-message(ng-repeat="message in messages") {{message.body}}

The problem is the toastMessage test is failing (it can't find the element). It seems to be waiting for the toast to disappear and then running the test.

I've also tried putting the toastMessage test outside the then() callback (I think this is pretty much redundant anyway), but I get the exact same behaviour.

My best guess is that protractor sees that there's a $timeout running, and waits for it to finish before running the next test (ref protractor control flow). How would I get around this and make sure the test runs during the timeout?

Update:

Following the suggestion below, I used browser.wait() to wait for the toast to be visible, then tried to run the test when the promise resolved. It didn't work.

console.log "clicking button"
loginButton.click()

browser.wait((()-> toast.isDisplayed()),20000, "never visible").then(()->
  console.log "looking for message"
  toastMessage = $('.toaster')
  expect(toastMessage.getText()).toBe("Invalid password")
)

The console.log statements let me see what's going on. This is the series of events, the [] are what I see happening in the browser.

clicking button
[toast appears]
[5 sec pass]
[toast disappears]
looking for message
[test fails]

For added clarity on what is going on with the toaster: I have a service which essentially holds an array of messages. The toast directive is always on the page (template is the jade above), and watches the messages in the toast service. If there is a new message, it runs the following code:

scope.messages.push(newMessage)
# set a timeout to remove it afterwards.
$timeout(
()-> 
  scope.messages.splice(0,1) 
, 
  5000
)

This pushes the message into the messages array on the scope for 5 seconds, which is what makes the toast appear (via ng-show="messages.length").

Why is protractor waiting for the toast's $timeout to expire before moving on to the tests?

like image 568
Ed_ Avatar asked Jul 31 '14 15:07

Ed_


2 Answers

I hacked around this using the below code block. I had a notification bar from a 3rd party node package (ng-notifications-bar) that used $timeout instead of $interval, but needed to expect that the error text was a certain value. I put used a short sleep() to allow the notification bar animation to appear, switched ignoreSynchronization to true so Protractor wouldn't wait for the $timeout to end, set my expect(), and switched the ignoreSynchronization back to false so Protractor can continue the test within regular AngularJS cadence. I know the sleeps aren't ideal, but they are very short.

browser.sleep(500);
browser.ignoreSynchronization = true;
expect(page.notification.getText()).toContain('The card was declined.');
browser.sleep(500);
browser.ignoreSynchronization = false;
like image 122
Chris Traynor Avatar answered Oct 13 '22 07:10

Chris Traynor


In case anyone is still interested, this code works for me with no hacks to $timeout or $interval or Toast. The idea is to use the promises of click() and wait() to turn on and off synchronization. Click whatever to get to the page with the toast message, and immediately turn off sync, wait for the toast message, then dismiss it and then turn back on sync (INSIDE the promise).

element(by.id('createFoo')).click().then(function () {
    browser.wait(EC.stalenessOf(element(by.id('createFoo'))), TIMEOUT);
    browser.ignoreSynchronization = true;
    browser.wait(EC.visibilityOf(element(by.id('toastClose'))), TIMEOUT).then(function () {
      element(by.id('toastClose')).click();
      browser.ignoreSynchronization = false;
   })
});
like image 44
bobsfishmarket Avatar answered Oct 13 '22 06:10

bobsfishmarket