featurist / browser-monkey

Reliable DOM testing
https://browsermonkey.org
53 stars 6 forks source link

Chained API that doesn't change scope #43

Open joshski opened 8 years ago

joshski commented 8 years ago

The current API gently encourages tight coupling to the structure of your HTML. I want to introduce a chained API which goes the other way.

The idea of moving away from the structure is my preference for being more tightly coupled to the user experience (which bits of text to click) rather than the implementation (whether that text appears on buttons or links, which css classes those elements have) and a distaste for being overly DRY in tests.

I'll rework the example from the top of the readme to illustrate what I have in mind (incidentally this example needs work, because it doesn't do any assertions, when browser-monkey is described as an "assertion library"! But anyway, it's good for this discussion):

var adminPanel = browser.component({
  searchUsers: function () {
    return this.find('.search');
  },
  userResult: function (name) {
    // find a user in the results by their name
    return this.find('.results .user', {text: name});
  }
  userEditor: function () {
    // return the user editor, scoped to the .user-editor div.
    return this.find('.user-editor').component({
      name: function () { this.find('.name'); },
      email: function () { this.find('.email'); },
      save: function () { this.find('.save'); }
    });
  }
});

it('can search for, edit and save a user', function () {
  return adminPanel.searchUsers().typeIn('bar').then(function () {
    return adminPanel.userResult('Barry').click();
  }).then(function () {
    var userEditor = adminPanel.userEditor();
    return Promise.all([
      userEditor.name().typeIn('Barry Jones'),
      userEditor.email().typeIn('barryjones@example.com')
    ]).then(function () {
      return userEditor.save().click();
    });
  }).then(function () {
    // verify that the user was saved
    // use mock-xhr-router!
  });
});

Now, if we use more semantic finders and no components, we could theoretically rephrase all of the above as:

it('can search for, edit and save a user', function () {
  return browser
    .typeInto('Search for users', 'bar')
    .click('Barry')
    .thenUnder('Edit User', function(userEditor) {
      return userEditor
        .typeInto('Name', 'Barry Jones')
        .typeInto('Email', 'barryjones@example.com')
        .click('Save');
    })
    .then(function() {
      // verify that the user was saved
      // use mock-xhr-router!
    });
});

The key thing to consider in this example is that thenUnder is an alternative to find. Whereas find changes the scope in the same chain, thenUnder would start a new scope, in an entirely new chain passed to a callback. If we never change the scope in a single chain, then we can lose a lot of promise boilerplate in common cases, i.e:

browser.click('Happy').then(=> browser.click('Days'))

// becomes equivalent to:

browser.click('Happy').click('Days')

To achieve this, we'd have to return promises that were also links in the chained API. Is that too weird?

I'm happy to do this if there's a broad agreement it's a good idea. I don't use components, and this would kind of break the component API. So it could be introduced as an alternative API, or it could replace the existing API. What do you think?

joshski commented 8 years ago

I think this too-many-thens problem can be solved in a general way, so I made this:

https://github.com/featurist/promise-builder

With this, we can transform the browser monkey API to the style above, relatively easily:

http://requirebin.com/?gist=2fa7c16da25a55f5e824959c5ad2fbc5

dereke commented 7 years ago

I am guessing we are going to close this now?