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
# -*- 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() |