13
\$\begingroup\$

What I'm building is a URL builder. A marketing person inputs a URL and can test that URL to see if it's valid, then they select a demand channel and then hit 'Generate Campaign URL'. I've omitted the other form fields, as I'm only interesting in refactoring the code for the onclick event of the 'Test' URL button.

In an effort to become a better developer, I'd like to start writing unit tests and adopting a more TDD approach to my code. I've been working off the example in Writing Testable JavaScript in an effort to refactor my jQuery onclick event into something that can be tested using a framework like Mocha, Jasmine, etc.

    $.validator.setDefaults({
        highlight: function (element) {
            $(element).closest('.form-group').addClass('has-error');
        },
        unhighlight: function (element) {
            $(element).closest('.form-group').removeClass('has-error');
        },
        errorElement: 'span',
        errorClass: 'help-block',
        errorPlacement: function (error, element) {
            if (element.parent('.input-group').length) {
                error.insertAfter(element.parent());
            } else {
                error.insertAfter(element);
            }
        }
    });

    $('#form1').validate({
        rules: {

            baseURL: {
                required: true,
                url: true
            },

            demandChannel: {
                required: true
            },

            userEmail: {
                required: true,
                email: true

            }
        }
    });


    $('#testBaseURL').on('click', function () {
        var $baseURL = $('#baseURL');
        var inputIsValid = $baseURL.valid();
        var url = $baseURL.val();

        if (inputIsValid) {
            window.open(url, '_blank');
        }
    });

    var TestButton = function (el) {
        this.el = $(el);
        this._bindEvents();

    };

    TestButton.prototype._handleClick = function (evt) {
        var baseURL = $(evt.target).closest('.input-group').find('#baseURL');
        console.log('baseURL = ' + baseURL.val());

    };

    TestButton.prototype._bindEvents = function () {
        this.el.on('click', this._handleClick.bind(this));
    };

var foo = new TestButton('#testBaseURL');
<!DOCTYPE html>
<html>
<head>
  <script src="//code.jquery.com/jquery.min.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.13.1/jquery.validate.js"></script>
  
<script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.13.1/additional-methods.js"></script> 
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>

  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
    <div class="container">
        <form class="form-horizontal" id="form1">

            <!-- Email -->
            <div class="form-group">
                <label for="userEmail" class="col-sm-3 control-label">Email</label>
                <div class="col-sm-9">
                    <input type="email" name="userEmail" class="form-control" id="userEmail" placeholder="coolguy@gmail.com">
                </div>
            </div>

            <!-- Campaign URL -->
            <div class="form-group">
                <label for="campUrl" class="col-sm-3 control-label">URL</label>
                <div class="col-sm-9">
                    <div class="input-group">
                        <input type="url" name="baseURL" class="form-control" id="baseURL" placeholder="example.com">
                        <span class="input-group-btn">
                            <button class="btn btn-default" id="testBaseURL" type="button">Test!</button>
                        </span>

                    </div>
                </div>
            </div>

            <!-- Demand Channel -->
            <div class="form-group">
                <label for="selector" class="col-sm-3 control-label">Demand Channel</label>
                <div class="col-sm-9">
                    <select class="form-control" name="demandChannel" id="demandChannel">
                        <option value="" selected disabled>Please select</option>
                        <option value="display">Display</option>
                        <option value="email">Email</option>
                        <option value="ppc">PPC</option>
                        <option value="social">Social</option>
                        <option value="affiliate">Affiliate</option>
                    </select>
                </div>
            </div>

            <div class="form-group">
                <div class="col-sm-offset-3 col-sm-9">
                    <button type="submit" class="btn btn-default" id="generateURL">Generate Campaign URL</button>
                </div>
            </div>
        </form>
  </div>
</body>
</html>

In Rebecca's example, she took this code (do not review it):

var liked = $('#liked');
var resultsList = $('#results');


// ...


resultsList.on('click', '.like', function (e) {
  e.preventDefault();

  var name = $(this).closest('li').find('h2').text();

  liked.find( '.no-results' ).remove();

  $('<li>', { text: name }).appendTo(liked);

});

Doing this:

var Likes = function (el) {
  this.el = $(el);
  return this;
};

Likes.prototype.add = function (name) {
  this.el.find('.no-results').remove();
  $('<li>', { text: name }).appendTo(this.el);
};

var SearchResults = function (el) {
  this.el = $(el);
  this.el.on( 'click', '.btn.like', _.bind(this._handleClick, this) );
};

SearchResults.prototype.setResults = function (results) {
  var templateRequest = $.get('people-detailed.tmpl');
  templateRequest.then( _.bind(this._populate, this, results) );
};

SearchResults.prototype._handleClick = function (evt) {
  var name = $(evt.target).closest('li.result').attr('data-name');
  $(document).trigger('like', [ name ]);
};

SearchResults.prototype._populate = function (results, tmpl) {
  var html = _.template(tmpl, { people: results });
  this.el.html(html);
};

To end up with this:

var liked = new Likes('#liked');
var resultsList = new SearchResults('#results');


// ...


$(document).on('like', function (evt, name) {
  liked.add(name);
});

In the project I'm working on, I have some very similar code, so I thought I would attempt to replicate it.

My before:

$('#testBaseURL').on('click', function () {
    var $baseURL = $('#baseURL');
    var inputIsValid = $baseURL.valid(); // returns true/false
    var url = $baseURL.val();

    if (inputIsValid) {
        window.open(url, '_blank');
    }
});

My after:

var TestButton = function (el) {
    this.el = $(el);
    this._bindEvents();

};

TestButton.prototype._handleClick = function (evt) {
    var baseURL = $(evt.target).closest('.input-group').find('#baseURL');
    console.log('baseURL = ' + baseURL.val());

};

TestButton.prototype._bindEvents = function () {
    this.el.on('click', this._handleClick.bind(this));
};

As you can see, it's a blatant ripoff of her code. And now I'm at a point where I'm not sure how to continue the refactoring of my code, and basically all I'm doing is just console logging the value of the field. I know I need to include some sort of check to see if the input is valid, and if it is, pass the URL into a new window, but I'm not sure how to go about this.

I've created a JSBin with a working example to help assist in what I'm trying to do.

\$\endgroup\$
  • 2
    \$\begingroup\$ Your question doesn't make much sense without the JSBin. Could you post the entire working example here as a Stack Snippet (Ctrl-M)? \$\endgroup\$ – 200_success Mar 14 '15 at 1:20
  • 3
    \$\begingroup\$ @200_success, I've added the Stack Snippet - first time using that! Thanks for letting me know. \$\endgroup\$ – Blexy Mar 14 '15 at 1:27
  • \$\begingroup\$ Perhaps breaking down your post into small focused pieces would make them easier to read and understand? \$\endgroup\$ – Dmitri Zaitsev Apr 11 '15 at 15:59
  • \$\begingroup\$ Are you asking us for advice on how to "check to see if the input is valid, and if it is, pass the URL into a new window" or are you more interested in a review of the code you have so far? \$\endgroup\$ – Thriggle Apr 29 '15 at 0:08
  • \$\begingroup\$ @Thriggle, more interested in the code and how to make it more testable. \$\endgroup\$ – Blexy May 2 '15 at 16:24
2
\$\begingroup\$

Creating layers of indirection just for a click event with two lines of code seems a bit off and I can't see in any way how that would make it easier to test.

What you may want is to use a higher level of abstraction and which wraps the functionality of your form:

var UrlBuilder = function(el, options) {
  this.el = el;
  this.options = _.defaults(options || {}, {
    rules: {
      baseURL: {
          required: true,
          url: true
      },

      demandChannel: {
          required: true
      },

      userEmail: {
          required: true,
          email: true

      }
   }
  });
  this.valid = function(){
    return this.el.valid();
  };
  this.el.validate(options.rules);
  this.el.on('click', '.test-button', this.test.bind(this));
}

Also we can refactor the test method to have usable output:

UrlBuilder.prototype.test = function(){
   if (! this.el.valid()) {
      return false;
   } else {
      this.popup(url); 
      return this.el.val();
   }
};

// The reason is that the browser will not allow you to override anything in the window host object.
UrlBuilder.prototype.popup = function(url){
  window.open(url, '_blank'); 
};

Some key points here:

  • We can can create UrlBuilder from any fragment.
  • We scope from the element when binding handlers.
  • We can test with a valid / invalid object just by stubbing the valid method.
  • We return meaningful values from .test

So to test this in Mocha we could do:

describe('UrlBuilder', function(){

  before(function(){
    // @todo load $fixture which has the HTML content needed.
    this.builder = new UrlBuilder($fixture);
  });

  describe("validation", function{
     // ...
  });

  describe("test", function(){

    context("when invalid", function(){
      before(function(){
        sinon.stub(this.builder, 'valid').returns(false);
      });
      it("returns false", function(){
        expect(this.builder.test()).to.be.falsy;
      });
    });

    context("when invalid", function(){

      var spy;

      before(function(){        
        sinon.stub(this.builder, 'valid').returns(true);
        spy = sinon.stub(this.builder, 'popup');
      });

      it("returns the correct url", function(){
          expect(this.builder.test()).to.eq("http://example.com?foo=baz...");
      });

      it("opens the test url when the test button is clicked", function(){
        this.builder.el.find('.test-button').click();
        expect(spy).calledWith("http://example.com?foo=baz...")
      });
    });
  });
});
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.