April 15, 2010

Testing JavaScript & XHR with Python

I am working on a library for Django - which I'll describe later - and for building this project I need to test the behavior of XHR on the HTML page. That is, I have a page, inject some JavaScript (that may do a request on the server), and see how it changes the document. The Django application the library is designed for creates some parts of the original page and processes some parts of the server-side XHR.

Python is great for this. I've mentioned already in this blog how I like Qt4, and it proves to be very useful again. I could not get PySide running on OS X just yet, so I opted for PyQt4.

Four tests; four evenings working with them – each having an intricate problem to solve ;) Two tests work on the local document, the other two connect to the Portlet test bench, running on localhost. These tests trigger onclick submit actions from the XHR test page, simulating a user clicking a button.

This way it is possible to test the effect of JavaScript injection onto any web page in the wild. At least the WebKit response.. Firefox could be tested by using python-gtkmozembed. QWebKit also has methods for binding Python variables to the page (it is possible for JavaScript to trigger a Python callback function) and render an image snapshot of the page.

If you wish to run the tests, download the file, start up the Rails server with the test bench and run

$ python xhr_tests.py

# -*- coding: utf-8 -*-
# (C) Mikael Lammentausta 2010
# Released under WTFPL
import unittest
import signal
from PyQt4.QtCore import SIGNAL, QObject, QUrl, QString, QTimer
from PyQt4.QtGui import QApplication
from PyQt4.QtWebKit import *
from BeautifulSoup import BeautifulSoup
# the application instance - one for all tests
QT_APP = QApplication([])
class XHRTestCase(unittest.TestCase):
def setUp(self):
"""
Requires the Portlet test bench packaged with Caterpillar.
"""
# Ctrl-C halts the test suite
signal.signal( signal.SIGINT, signal.SIG_DFL )
self.xUnit_url = 'http://localhost:3000/caterpillar/test_bench/junit/'
def test_javascript(self):
"""
Simple test for validating QWebFrame's reaction.
"""
page = QWebPage()
page.mainFrame().setHtml("""
<html>
<head>
<script type="text/javascript">
document.write("Hello World!");
</script>
</head>
<body></body>
</html>
""")
html = page.mainFrame().toHtml()
soup = BeautifulSoup(html)
self.assertEquals('Hello World!',soup.body.text)
def test_jquery(self):
"""
Test for running an external JavaScript library.
jQuery is used to do a simple page update.
"""
page = QWebPage()
page.mainFrame().setHtml("""
<html>
<head>
<script type="text/javascript" src="/jquery-1.4.2.min.js"></script>
</head>
<body></body>
<script type="text/javascript">
if(jQuery) {
$("body").html('Hello World!');
}
</script>
</html>
""", QUrl( 'http://code.jquery.com' ))
# connect the signal to quit the application after the page is loaded
page.connect( page, SIGNAL( 'loadFinished(bool)' ), QT_APP.quit )
# start the application to load external JS
QT_APP.exec_()
html = page.mainFrame().toHtml()
soup = BeautifulSoup(html)
self.assertEquals('Hello World!',soup.body.text)
def test_xhr_onclick_post(self):
"""
Launch a click event on an input element which sends
an XHR POST that updates the page.
The XHR test page includes the Prototype JS library.
"""
page = QWebPage()
page.mainFrame().load(QUrl( self.xUnit_url + 'xhr' ))
page.connect( page, SIGNAL( 'loadFinished(bool)' ), QT_APP.quit )
QT_APP.exec_()
# launch onClick
evaluateJavaScript(page.mainFrame(), """
$$('#xhr_onclick input').first().click();
""")
html = page.mainFrame().toHtml()
soup = BeautifulSoup(str(html))
self.assertEquals('Hello World!',soup.find(id='onclick_resp').text)
def test_xhr_form_post(self):
"""
Submit a form which sends an XHR POST that updates the page.
The XHR test page includes the Prototype JS library.
"""
page = QWebPage()
page.mainFrame().load(QUrl( self.xUnit_url + 'xhr' ))
page.connect( page, SIGNAL( 'loadFinished(bool)' ), QT_APP.quit )
QT_APP.exec_()
# submit form
evaluateJavaScript(page.mainFrame(), """
$$('#xhr_form form').first().commit.click();
""")
html = page.mainFrame().toHtml()
soup = BeautifulSoup(str(html))
self.assertEquals('Hello World!',soup.find(id='form_resp').text)
def evaluateJavaScript(frame, script):
"""
Evaluates JavaScript on QWebFrame and exits to QApplication
to get on with the unit test.
"""
# start timer to kill QApplication
timer = QTimer()
QObject.connect(timer, SIGNAL( 'timeout()' ), QT_APP.quit)
timer.start(200) # msec
# inject JavaScript
frame.evaluateJavaScript(QString(script))
# execute app, which the timer will kill
QT_APP.exec_()
if __name__ == '__main__':
unittest.main()
view raw xhr_tests.py hosted with ❤ by GitHub

No comments: