In a recent project I needed a auto complete plugin that worked in a textarea, and only based on certain keywords / context. What I found was the jQuery TextComplete plugin, which is awesome! I recommend others try it out.

What I did not find was automated JavaScript tests for the auto complete behavior. Since my use case was pretty complicated (involving multiple strategies and complex regular expressions), I wanted to be sure I had tests for all the scenarios I wanted to cover.

Using Jasmine + PhantomJS, I got the tests down to the following succinct structure:

var textarea;

beforeEach(function() {
  // render UI
  
  textarea = $('#auto_complete_test textarea');
});

it("should auto complete string operators when after a string node that is after a number node", function() {
  inputTextAndInvokeKeyPressEvent(textarea, 'bike_ride > 5 min and user_name ');

  var dropdown = assertDropdown();
  expect(autoCompleteList(dropdown)).toEqual([ '=', 'contains', 'present' ]);
});

it("should auto complete number operators when after a number node that is after a string node", function() {
  inputTextAndInvokeKeyPressEvent(textarea, 'user_name contains "Bobby" and bike_ride ');

  var dropdown = assertDropdown();
  expect(autoCompleteList(dropdown)).toEqual([ '>', '<', '>=', '<=', '=' ]);
});

Test Helpers

The real power is in the test helper functions I created. The trick to get these tests to work was:

  • Focus must be set on the element
  • Ensure the caret is at the right point in textarea (just setting the value via jQuery is not sufficient)
  • Simulate a true keyup event for the plugin
  • Make it easy and DRY to both set the value and the caret

Set the text value and correct caret position

Accepts the jQuery reference to a textarea, and the existing string input to simulate the user has already typed into the textarea:

  function inputTextAndInvokeKeyPressEvent(textarea, input) {
    textarea.val(input);
    setCaret(textarea, input.length);
    textarea.trigger({ type : 'keyup', keyCode : input.charCodeAt(input.length - 1) });
  }

Utility: set the focus & caret

function setCaret($el, index) {
    if ($el.length === 0) {
      throw("No dom element found for $el in setCaret");
    }

    var el = $el[0];

    if (el.createTextRange) {
      var range = el.createTextRange();
      range.move("character", index);
      range.select();
    } else if (el.selectionStart !== null) {
      el.focus();
      el.setSelectionRange(index, index);
    }
  }

Utility: Assert an auto complete dropdown exists, and return it

  function assertDropdown() {
    var dropdown = $('.textcomplete-dropdown');
    expect(dropdown.length).toEqual(1, "Should be 1 dropdown DOM element");

    return dropdown;
  }

Utility: Return array of items in dropdown

  function autoCompleteList(dropdown) {
    return dropdown.find('li').map(function() {
      return $(this).text();
    }).get();
  }