Friday, July 16, 2010

Emacs, mozrepl and javascript autotesting

Emacs, mozrepl and javascript autotesting

This article shows how to use mozrepl in conjunction with emacs to run tests (or some other function) automatically in a web page in your browser as you work on a related javascript file in emacs.

This is not some perfect setup. And given how quickly things change these days it may be hard to replicate. But it shows the power of several different technologies interacting to give us more freedom in what we do and how we do it.

  • To follow this blog post, you need to:
    • install mozrepl
      • mozrepl is a firefox addon; there is an older version called mozlab; you should go here instead though where you should find a link for the .xpi file
      • after restarting firefox, you should be able to telnet to the js repl
          bash> telnet localhost 4242
          ... [header stuff]...
          repl1>
        
        • this is a nice little repl; you can experiment with javascript here and test things out; you might want to check out the tutorial
        • of course, doing it in a telnet program isn't so much fun; you could wrap it in readline library such as:
          bash> rlwrap telnet localhost 4242
          ... [header stuff]...
          repl1>
          
        • this should give you similar functionality that you'd expect on the bash prompt
        • of course, there's arguably a better solution still, which is to run it in emacs since this will allow you to
          • edit javascript files and send parts or all thereof to mozrepl directly, and
          • generate command strings dynamically in emacs and send these directly to mozrepl
      • which leads us to mozrepl.el ...
    • install mozrepl.el in your emacs
      • instructions are here
      • I should note that the javascript mode I'm using in emacs comes from here
      • and my mozrepl.el comes from another emacs mode: nxhtml mode
      • these were some additional setup lines in my .emacs
          (add-to-list 'auto-mode-alist '("\.js$" . js-mode))
          (autoload 'inferior-moz-mode "moz" "MozRepl Inferior Mode" t)
          (autoload 'moz-minor-mode "moz" "MozRepl Minor Mode" t)
          (add-hook 'js-mode-hook 'javascript-moz-setup)
          (defun javascript-moz-setup ()
            (interactive)
            (moz-minor-mode 1))
        
      • at the moment, javascript development in emacs seems to be in a state of flux with several js/javascript mode versions floating around; getting set up can be confusing and is beyond what I am trying to share here
      • you should be able to perform this command in your emacs (or something like it)
        M-x run-mozilla
        

        and have a new buffer open up with a repl prompt similar to the telnet experience above.

Putting it together

My set up has the following features:

  • I use a home-grown test framework called unitjs that allows me to group my tests into sections, and group these sections into modules (functions) that I can run either when the page loads, or on clicking a button (etc)
  • I have an html page for testing that loads my js project files and the testing framework I am using to test the project as well as the tests themselves
  • these tests are not run automatically but sit in functions (which I loosely refer to as modules)
  • for instance, I might have
    var tests = {};
    ...
    tests.module1 = function() {
      ... do some setup ...
      ... define some test functions with assertions ...
      ... call the test runner to run the tests above and collect the results ...
    }
    
  • each module corresponds to a separate js file in the tests folder of my project
    • this is important for the setup I am demonstrating as will be shown shortly
  • to run the tests for module1, for instance, you just need to invoke the function
    tests.module1()
    

    and the results will get displayed on the test page

  • similarly for tests.module2 and so on...

Hooking up emacs, mozrepl and autotesting

What we're going to do now is get emacs to automagically work out which test module file we have just saved and send the command (tests.moduleX()) to mozrepl, which will then execute this command within the context of our test page, thereby running and displaying the tests

  • in your .emacs file we set up a function that does the job of reloading the page and sending the test instructions:

      (defun me/mozrepl/run-after-reload (str)
       "Send string to mozrepl, but only after reloading"
       (interactive)
       (progn
         (comint-send-string (inferior-moz-process) "repl.home();")
         (comint-send-string (inferior-moz-process) "BrowserReload();")
         (sleep-for 0 500)
         (comint-send-string (inferior-moz-process) "repl.enter(content);")
         (comint-send-string
          (inferior-moz-process) str )))
    
  • [18-Jul-2010]: I've added ';' after each js command in the above defun

  • all we're doing here is sending a series of commands via the repl
    • each comint-send-string command is as though we were typing that command into the repl ourselves
  • note that the commands reload the browser page, then get mozrepl to enter into the context of the document (repl.enter(content)); this gives us access to our javascript code that is loaded on this page
    • you can check by typing into the repl the name of one of your global variables in your javascript project; the repl should have access to it and you should be able to use it
    • when I was playing with mozrepl this wasn't at all obvious and I almost gave up trying to access the javascript that had loaded with my page until I discovered this step
  • once we're in the correct context, we can get the repl to evaluate 'str'
  • note the sleep command; I'm not sure how important this is but I thought it prudent (and may have had trouble otherwise) to give the browser some time to reload before trying to send another command to the repl

    • there may be other ways to do this such as not reloading the browser page and simple insert a script tag into the head node of the document using DOM operations
  • now we add in some functionality that will examine the buffer we are writing our tests in and guesses which test function to send to the repl:

        (defun proj1/tests/run (test-module)
          "Pass test invocation string to me/mozrepl/run-after-reload
          ; eg test-module might be 'tests.module1'
          ; command will be: tests.module1(document.getElementById('tests-container'))"
          (me/mozrepl/run-after-reload
           (concat test-module "(document.getElementById('tests-container'))")))
    
        (defun proj1/find-and-run-test-module ()
          "Look for test module signature; if found, send it to proj1/tests/run"
          (interactive)
          (cond
           ((search "tests.module1 = function" (buffer-string))
            (proj1/tests/run "tests.module1"))
           ((search "tests.module2 = function" (buffer-string))
            (proj1/tests/run "tests.module2")) ))
    
  • the first defun takes a test module name generates the particular command I want mozrepl to evaluate within the context of the testing page:

    "tests.module1(document.getElementById('tests-container'))"
    
    • will be evaluated by the repl within the context of my testing page and will run the module1 tests and pass in a reference to a test-container node (on the testing page) that the results will be displayed in
  • the second defun searches the current buffer and looks for a matching piece of text using the elisp (search ...) function ; in this case, the first part of our definition of the test module is used

    • if it is found, then we invoke the first defun with the relevant module
  • finally, we don't want to have to run M-x proj1/find-and-run-test-module when we are in our test file, so we set up a save hook to do it automatically:

        (defun proj1/autotest () (interactive)
          (add-hook 'after-save-hook 'proj1/find-and-run-test-module 'append 'local))  
    
        (defun proj1/autotest-remove () (interactive)
          (remove-hook 'after-save-hook 'proj1/find-and-run-test-module 'local))
    
  • we invoke the first defun [M-x proj1/autotest] in our buffer just once; thereafter whenever we save, our test page will get reloaded and the test module for that page gets executed

  • the second defun allows us to turn this off
  • And that's pretty much it!

Notes

  • Don't forget:
    • you need to keep your browser open on the testing page with the loaded code
    • You will need to start the repl in emacs (M-x run-mozilla)
  • I haven't figured out how to navigate tabs in mozrepl

Concluding Remarks

  • mozrepl allows you to tinker with live webpages via a javascript repl
  • emacs is great at connecting with repls in general
  • this article was just experimenting with combining these two

Related pages

  • The inspiration for this artice was this page on emacswiki
    • this page shows how to program emacs to send commands to mozrepl
    • it highlights comint mode; one of the things that makes emacs an extremely powerful, versatile editor and which we used above

No comments: