← Back to team overview

divmod-dev team mailing list archive

[Merge] lp:~divmod-dev/divmod.org/athena-events-806545 into lp:divmod.org

 

Jonathan Jacobs has proposed merging lp:~divmod-dev/divmod.org/athena-events-806545 into lp:divmod.org.

Requested reviews:
  Divmod-dev (divmod-dev)
Related bugs:
  Bug #806545 in nevow: "Expose browser-generated events to Athena event handlers"
  https://bugs.launchpad.net/nevow/+bug/806545

For more details, see:
https://code.launchpad.net/~divmod-dev/divmod.org/athena-events-806545/+merge/67455

Wrap DOM events in a universal way and pass these to Athena event handlers.
-- 
https://code.launchpad.net/~divmod-dev/divmod.org/athena-events-806545/+merge/67455
Your team Divmod-dev is requested to review the proposed merge of lp:~divmod-dev/divmod.org/athena-events-806545 into lp:divmod.org.
=== modified file 'Nevow/nevow/athena.py'
--- Nevow/nevow/athena.py	2010-07-12 19:00:11 +0000
+++ Nevow/nevow/athena.py	2011-07-10 13:41:23 +0000
@@ -1511,7 +1511,7 @@
 
 
 handler = stan.Proto('athena:handler')
-_handlerFormat = "return Nevow.Athena.Widget.handleEvent(this, %(event)s, %(handler)s);"
+_handlerFormat = "return Nevow.Athena.Widget.handleEvent(this, %(event)s, %(handler)s, event);"
 
 def _rewriteEventHandlerToAttribute(tag):
     """

=== modified file 'Nevow/nevow/js/Divmod/Runtime/__init__.js'
--- Nevow/nevow/js/Divmod/Runtime/__init__.js	2008-12-31 18:44:01 +0000
+++ Nevow/nevow/js/Divmod/Runtime/__init__.js	2011-07-10 13:41:23 +0000
@@ -709,6 +709,25 @@
      */
     function addBeforeUnloadHandler(self, aWindow, handler) {
         Divmod.Base.addToCallStack(aWindow, 'onbeforeunload', handler);
+    },
+
+
+    /**
+     * Determine which mouse buttons were pressed from a browser event object.
+     *
+     * The default implementation matches the W3C DOM Level 2 Events
+     * specifications.
+     *
+     * @return: A mapping of C{'left'}, C{'middle'}, C{'right'} to C{Boolean}
+     *     values indicating the state of the named mouse buttons.
+     *
+     * @see: <http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-MouseEvent>
+     */
+    function getMouseButtonsFromEvent(self, event) {
+        return {
+            'left': event.button == 0,
+            'middle': event.button == 1,
+            'right': event.button == 2};
     });
 
 
@@ -1205,6 +1224,20 @@
      */
     function addBeforeUnloadHandler(self, aWindow, handler) {
         Divmod.Base.addToCallStack(aWindow.document.body, 'onbeforeunload', handler);
+    },
+
+
+    /**
+     * Internet Explorer specific handling of the C{event.button} property.
+     *
+     * @see: L{Divmod.Runtime.Platform.getMouseButtonsFromEvent}
+     * @see: <http://msdn.microsoft.com/en-us/library/ms533544%28v=vs.85%29.aspx>
+     */
+    function getMouseButtonsFromEvent(self, event) {
+        return {
+            'left': (event.button & 1) != 0,
+            'middle': (event.button & 4) != 0,
+            'right': (event.button & 2) != 0};
     });
 
 

=== modified file 'Nevow/nevow/js/Divmod/Test/TestRuntime.js'
--- Nevow/nevow/js/Divmod/Test/TestRuntime.js	2008-12-31 18:44:01 +0000
+++ Nevow/nevow/js/Divmod/Test/TestRuntime.js	2011-07-10 13:41:23 +0000
@@ -713,6 +713,100 @@
     });
 
 
+
+/**
+ * Tests for L{Divmod.Runtime.Platform}.
+ */
+Divmod.UnitTest.TestCase.subclass(Divmod.Test.TestRuntime,
+                                  'PlatformTests').methods(
+    function setUp(self) {
+        self.runtime = Divmod.Runtime.Platform('name');
+    },
+
+
+    /**
+     * Assert that C{evt.button} when processed with
+     * C{getMouseButtonsFromEvent} yields the same values for its C{'left'},
+     * C{'middle'} and C{'right'} attributes as the parameters with these same
+     * names.
+     */
+    function assertMouseButtons(self, evt, left, middle, right) {
+        var buttons = self.runtime.getMouseButtonsFromEvent(evt);
+        self.assertIdentical(buttons.left, left)
+        self.assertIdentical(buttons.middle, middle)
+        self.assertIdentical(buttons.right, right)
+    },
+
+
+    /**
+     * Most platforms follow the W3C definitions for C{event.button} values.
+     *
+     * These values cannot be combined to indicate multiple buttons being
+     * pushed.
+     */
+    function test_getMouseButtonsFromEvent(self) {
+        var evt = {};
+        // Unknown value.
+        evt.button = 99;
+        self.assertMouseButtons(evt, false, false, false);
+        // Left mouse button.
+        evt.button = 0;
+        self.assertMouseButtons(evt, true, false, false);
+        // Middle mouse button.
+        evt.button = 1;
+        self.assertMouseButtons(evt, false, true, false);
+        // Right mouse button.
+        evt.button = 2;
+        self.assertMouseButtons(evt, false, false, true);
+    });
+
+
+
+/**
+ * Tests for L{Divmod.Runtime.InternetExplorer}.
+ */
+Divmod.Test.TestRuntime.PlatformTests.subclass(
+        Divmod.Test.TestRuntime,
+        'InternetExplorerPlatformTests').methods(
+    function setUp(self) {
+        self.runtime = Divmod.Runtime.InternetExplorer();
+    },
+
+
+    /**
+     * Internet Explorer uses different values for L{event.button} and can
+     * combine these values to indicate multiple buttons being pressed.
+     */
+    function test_getMouseButtonsFromEvent(self) {
+        var evt = {};
+        // No buttons.
+        evt.button = 0;
+        self.assertMouseButtons(evt, false, false, false);
+        // Left mouse button.
+        evt.button = 1;
+        self.assertMouseButtons(evt, true, false, false);
+        // Right mouse button.
+        evt.button = 2;
+        self.assertMouseButtons(evt, false, false, true);
+        // Middle mouse button.
+        evt.button = 4;
+        self.assertMouseButtons(evt, false, true, false);
+        // Left and right mouse buttons.
+        evt.button = 3;
+        self.assertMouseButtons(evt, true, false, true);
+        // Left and middle mouse buttons.
+        evt.button = 5;
+        self.assertMouseButtons(evt, true, true, false);
+        // Right and middle mouse buttons.
+        evt.button = 6;
+        self.assertMouseButtons(evt, false, true, true);
+        // Left, middle and right mouse buttons.
+        evt.button = 7;
+        self.assertMouseButtons(evt, true, true, true);
+    });
+
+
+
 Divmod.Test.TestRuntime.SpidermonkeyRuntimeTests = Divmod.UnitTest.TestCase.subclass(
     'Divmod.Test.TestRuntime.SpidermonkeyRuntimeTests');
 /**

=== modified file 'Nevow/nevow/js/Nevow/Athena/__init__.js'
--- Nevow/nevow/js/Nevow/Athena/__init__.js	2009-01-21 22:58:03 +0000
+++ Nevow/nevow/js/Nevow/Athena/__init__.js	2011-07-10 13:41:23 +0000
@@ -1176,7 +1176,8 @@
  */
 Nevow.Athena.Widget._makeEventHandler = function (domEventName, methodName) {
     return function () {
-        return Nevow.Athena.Widget.handleEvent(this, domEventName, methodName);
+        return Nevow.Athena.Widget.handleEvent(
+            this, domEventName, methodName, event);
     };
 };
 
@@ -1205,15 +1206,233 @@
     return Nevow.Athena.page.dispatchEvent(widget, eventName, handlerName, callable);
 };
 
-/**
- * Given a node and a method name in an event handling context, dispatch the
- * event to the named method on the widget which owns the given node.  This
- * also sets up error handling and does return value translation as
- * appropriate for an event handler.  It also pauses the outgoing message
- * queue to allow multiple messages from the event handler to be batched up
- * into a single request.
- */
-Nevow.Athena.Widget.handleEvent = function handleEvent(node, eventName, handlerName) {
+
+
+/**
+ * Lowest-common denominator DOM event wrapper.
+ *
+ * DOM event objects should wrapped by calling
+ * L{Nevow.Athena.Event.fromDOMEvent}.
+ *
+ * @type knownEventTypes: C{Array} of C{String}
+ * @cvar knownEventTypes: Array of event types that this event wrapper can wrap.
+ *
+ * @ivar event: Original DOM event object.
+ *
+ * @ivar type: C{String}
+ * @ivar type: Event type.
+ *
+ * @ivar target: Element to which the DOM event was originally dispatched.
+ *
+ * @see: <http://www.w3.org/TR/DOM-Level-2-Events/events.html>
+ * @see: <http://www.quirksmode.org/js/introevents.html>
+ */
+Divmod.Class.subclass(Nevow.Athena, 'Event').methods(
+    function __init__(self, event) {
+        self.event = event;
+        self.type = self.event.type;
+        self.target = self.event.target;
+        if (self.target === undefined) {
+            self.target = self.event.srcElement;
+        }
+    },
+
+
+    /**
+     * Cancels the event if it is cancelable, without stopping further
+     * propagation of the event.
+     */
+    function preventDefault(self) {
+        if (self.event.preventDefault) {
+            self.event.preventDefault();
+        } else {
+            self.event.returnValue = false;
+        }
+    },
+
+
+    /**
+     * Stops the propagation of events further along in the DOM.
+     */
+    function stopPropagation(self) {
+        if (self.event.stopPropagation) {
+            self.event.stopPropagation();
+        } else {
+            self.event.cancelBubble = true;
+        }
+    });
+
+
+
+/**
+ * Specific subclass of L{Nevow.Athena.Event} relating to key events.
+ *
+ * @ivar altKey: Was the I{alt} key pressed when the event fired?
+ *
+ * @ivar ctrlKey: Was the I{ctrl} key pressed when the event fired?
+ *
+ * @ivar shiftKey: Was the I{shift} key pressed when the event fired?
+ *
+ * @ivar metaKey: Was the I{meta} key pressed when the event fire?
+ */
+Nevow.Athena.Event.subclass(Nevow.Athena, 'KeyEvent').methods(
+    function __init__(self, event) {
+        Nevow.Athena.KeyEvent.upcall(self, '__init__', event);
+        self.altKey = !!self.event.altKey;
+        self.ctrlKey = !!self.event.ctrlKey;
+        self.shiftKey = !!self.event.shiftKey;
+        self.metaKey = !!self.event.metaKey; // Not in IE < 9.
+    },
+
+
+    /**
+     * Get the Unicode value of key press.
+     *
+     * For the I{keydown} or I{keyup} events this is the virtual key code of the
+     * physical button pushed. For I{keypress} event this is the character code
+     * for an alphanumeric key.
+     *
+     * @see: <https://developer.mozilla.org/en/DOM/event.keyCode>
+     * @see: <http://msdn.microsoft.com/en-us/library/ms533927%28v=VS.85%29.aspx>
+     */
+    function getKeyCode(self) {
+        return self.event.keyCode || self.event.which;
+    },
+
+
+    /**
+     * Set the Unicode value of key press.
+     */
+    function setKeyCode(self, value) {
+        self.event.keyCode = value;
+    });
+
+Nevow.Athena.KeyEvent.knownEventTypes = ['keydown', 'keypress', 'keyup'];
+
+
+
+/**
+ * Specific subclass of L{Nevow.Athena.Event} relating to mouse events.
+ *
+ * @ivar altKey: Was the I{alt} key pressed when the event fired?
+ *
+ * @ivar ctrlKey: Was the I{ctrl} key pressed when the event fired?
+ *
+ * @ivar shiftKey: Was the I{shift} key pressed when the event fired?
+ *
+ * @ivar metaKey: Was the I{meta} key pressed when the event fire?
+ */
+Nevow.Athena.Event.subclass(Nevow.Athena, 'MouseEvent').methods(
+    function __init__(self, event) {
+        Nevow.Athena.MouseEvent.upcall(self, '__init__', event);
+        self.altKey = self.event.altKey;
+        self.ctrlKey = self.event.ctrlKey;
+        self.shiftKey = self.event.shiftKey;
+        self.metaKey = self.event.metaKey; // Not in IE < 9.
+    },
+
+
+    /**
+     * Determine which mouse buttons were pressed in this event.
+     *
+     * @see: L{Divmod.Runtime.Platform.getMouseButtonsFromEvent}
+     */
+    function getMouseButtons(self) {
+        return Divmod.Runtime.theRuntime.getMouseButtonsFromEvent(self.event);
+    },
+
+
+    /**
+     * Get the coordinates of the event relative to the whole document.
+     *
+     * @return: Mapping of C{'x'} and C{'y'} to the horizontal and vertical
+     *     coordinates respectively.
+     */
+    function getPagePosition(self) {
+        return Divmod.Runtime.theRuntime.getEventCoords(self.event);
+    },
+
+
+    /**
+     * Get the coordinates within the browser's client area at which the event
+     * occurred (as opposed to the coordinates within the page).
+     *
+     * For example, clicking in the top-left corner of the client area will
+     * always result in a mouse event with a clientX value of 0, regardless of
+     * whether the page is scrolled horizontally.
+     *
+     * @return: Mapping of C{'x'} and C{'y'} to the horizontal and vertical
+     *     coordinates respectively.
+     */
+    function getClientPosition(self) {
+        return {
+            'x': self.event.clientX,
+            'y': self.event.clientY};
+    },
+
+
+    /**
+     * Get the coordinates of the event within the screen as a whole.
+     *
+     * @return: Mapping of C{'x'} and C{'y'} to the horizontal and vertical
+     *     coordinates respectively.
+     */
+    function getScreenPosition(self) {
+        return {
+            'x': self.event.screenX,
+            'y': self.event.screenY};
+    });
+
+Nevow.Athena.MouseEvent.knownEventTypes = [
+    'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup',
+    'mousewheel'];
+
+
+
+/**
+ * Mapping of event types to known event handlers.
+ */
+Nevow.Athena.Event._eventHandlerMapping = (function() {
+    var handlers = [
+        Nevow.Athena.KeyEvent,
+        Nevow.Athena.MouseEvent];
+    var mapping = {}
+    for (var i = 0; i < handlers.length; ++i) {
+        var handler = handlers[i];
+        var knownEventTypes = handler.knownEventTypes;
+        for (var j = 0; j < knownEventTypes.length; ++j) {
+            mapping[knownEventTypes[j]] = handler;
+        }
+    }
+    return mapping;
+})();
+
+
+
+/**
+ * Wrap a DOM event object with an appropriate L{Nevow.Athena.Event} subclass
+ * or L{Nevow.Athena.Event} if there is no specific handler for the event type.
+ */
+Nevow.Athena.Event.fromDOMEvent = function fromDOMEvent(event) {
+    var handler = Nevow.Athena.Event._eventHandlerMapping[event.type];
+    if (handler === undefined) {
+        handler = Nevow.Athena.Event;
+    }
+    return handler(event);
+};
+
+
+
+/**
+ * Given a node, a method name in an event handling context and an event
+ * object, dispatch the event to the named method on the widget which owns the
+ * given node.  This also sets up error handling and does return value
+ * translation as appropriate for an event handler.  It also pauses the
+ * outgoing message queue to allow multiple messages from the event handler to
+ * be batched up into a single request.
+ */
+Nevow.Athena.Widget.handleEvent = function handleEvent(node, eventName,
+                                                       handlerName, event) {
     var widget = Nevow.Athena.Widget.get(node);
     var method = widget[handlerName];
     var result = false;
@@ -1223,7 +1442,8 @@
         result = Nevow.Athena.Widget.dispatchEvent(
             widget, eventName, handlerName,
             function() {
-                return method.call(widget, node);
+                return method.call(
+                    widget, node, Nevow.Athena.Event.fromDOMEvent(event));
             });
     }
     return result;

=== added file 'Nevow/nevow/js/Nevow/Test/TestEvent.js'
--- Nevow/nevow/js/Nevow/Test/TestEvent.js	1970-01-01 00:00:00 +0000
+++ Nevow/nevow/js/Nevow/Test/TestEvent.js	2011-07-10 13:41:23 +0000
@@ -0,0 +1,504 @@
+// import Divmod
+// import Divmod.UnitTest
+// import Nevow.Athena
+
+
+
+/**
+ * A mock DOM event that behaves according to the W3C guide that most browsers
+ * follow.
+ *
+ * @ivar type: Event type.
+ *
+ * @ivar target: Element to which the DOM event was originally dispatched.
+ *
+ * @ivar cancelled: Has the default action been cancelled?
+ *
+ * @ivar stopped: Has event propagation been stopped?
+ */
+Divmod.Class.subclass(Nevow.Test.TestEvent, 'W3CEvent').methods(
+    function __init__(self, type, target) {
+        self.type = type;
+        self.target = target;
+        self.cancelled = false;
+        self.stopped = false;
+    },
+
+
+    /**
+     * If an event is cancelable, the preventDefault method is used to signify
+     * that the event is to be canceled, meaning any default action normally
+     * taken by the implementation as a result of the event will not occur.
+     */
+    function preventDefault(self) {
+        self.cancelled = true;
+    },
+
+
+    /**
+     * The stopPropagation method is used prevent further propagation of an
+     * event during event flow.
+     */
+    function stopPropagation(self) {
+        self.stopped = true;
+    });
+
+
+
+/**
+ * A mock DOM event that behaves according to IE before IE9.
+ *
+ * @ivar type: Event type.
+ *
+ * @ivar srcElement: Element to which the DOM event was originally dispatched.
+ *
+ * @ivar returnValue: Has the default action been cancelled?
+ *
+ * @ivar cancelBubble: Has event propagation been stopped?
+ */
+Divmod.Class.subclass(Nevow.Test.TestEvent, 'IEEvent').methods(
+    function __init__(self, type, srcElement) {
+        self.type = type;
+        self.srcElement = srcElement;
+        self.returnValue = true;
+        self.cancelBubble = false;
+    });
+
+
+
+/**
+ * Tests for L{Nevow.Athena.Event} when using W3C-style event objects.
+ */
+Divmod.UnitTest.TestCase.subclass(Nevow.Test.TestEvent, 'TestEventW3C').methods(
+    function createDOMEvent(self, type, target) {
+        return Nevow.Test.TestEvent.W3CEvent(type, target);
+    },
+
+
+    /**
+     * L{Nevow.Athena.Event.fromDOMEvent} creates a specific I{Event} subclass
+     * if it supports the DOM event otherwise L{Nevow.Athena.Event} is used.
+     */
+    function test_fromDOMEvent(self) {
+        function assertInstanceOf(inst, type, msg) {
+            self.assertIdentical(
+                evt instanceof type,
+                true,
+                'Expected ' + inst.toString() +
+                ' to be an instance of ' + type.toString());
+        }
+
+        var handlers = [
+            Nevow.Athena.KeyEvent,
+            Nevow.Athena.MouseEvent];
+
+        for (var i = 0; i < handlers.length; ++i) {
+            var handler = handlers[i];
+            var knownEventTypes = handler.knownEventTypes;
+            for (var j = 0; j < knownEventTypes.length; ++j) {
+                var domEvent = self.createDOMEvent(
+                    knownEventTypes[j], null);
+                var evt = Nevow.Athena.Event.fromDOMEvent(domEvent);
+                assertInstanceOf(evt, handler);
+            }
+        }
+
+        var evt = Nevow.Athena.Event.fromDOMEvent(
+            self.createDOMEvent('definitelyunknown', null));
+        assertInstanceOf(evt, Nevow.Athena.Event);
+    },
+
+
+    /**
+     * L{Nevow.Athena.Event} extracts information from different kinds of DOM
+     * events and puts them into a universal API.
+     */
+    function test_attributes(self) {
+        var eventType = 'eventType';
+        var target = 42;
+        var domEvent = self.createDOMEvent(eventType, target);
+        var event = Nevow.Athena.Event.fromDOMEvent(domEvent);
+        self.assertIdentical(event.event, domEvent);
+        self.assertIdentical(event.type, eventType);
+        self.assertIdentical(event.target, target);
+    },
+
+
+    /**
+     * L{Nevow.Athena.Event.preventDefault} calls the method or sets the
+     * attribute on the underlying DOM event that prevents the event's default
+     * return value from being used.
+     */
+    function test_preventDefault(self) {
+        var domEvent = self.createDOMEvent('eventtype', null);
+        var event = Nevow.Athena.Event.fromDOMEvent(domEvent);
+        self.assertIdentical(domEvent.cancelled, false);
+        event.preventDefault();
+        self.assertIdentical(domEvent.cancelled, true);
+    },
+
+
+    /**
+     * L{Nevow.Athena.Event.stopPropagation} calls the method or sets the
+     * attribute on the underlying DOM event that stops the event from
+     * propogating futher along in the DOM.
+     */
+    function test_stopPropagation(self) {
+        var domEvent = self.createDOMEvent('eventtype', null);
+        var event = Nevow.Athena.Event.fromDOMEvent(domEvent);
+        self.assertIdentical(domEvent.stopped, false);
+        event.stopPropagation();
+        self.assertIdentical(domEvent.stopped, true);
+    });
+
+
+
+/**
+ * Tests for L{Nevow.Athena.Event} when using IE-style event objects.
+ */
+Nevow.Test.TestEvent.TestEventW3C.subclass(Nevow.Test.TestEvent,
+                                           'TestEventIE').methods(
+    function createDOMEvent(self, type, target) {
+        return Nevow.Test.TestEvent.IEEvent(type, target);
+    },
+
+
+    function test_preventDefault(self) {
+        var domEvent = self.createDOMEvent('eventtype', null);
+        var event = Nevow.Athena.Event.fromDOMEvent(domEvent);
+        self.assertIdentical(domEvent.returnValue, true);
+        event.preventDefault();
+        self.assertIdentical(domEvent.returnValue, false);
+    },
+
+
+    function test_stopPropagation(self) {
+        var domEvent = self.createDOMEvent('eventtype', null);
+        var event = Nevow.Athena.Event.fromDOMEvent(domEvent);
+        self.assertIdentical(domEvent.cancelBubble, false);
+        event.stopPropagation();
+        self.assertIdentical(domEvent.cancelBubble, true);
+    });
+
+
+
+/**
+ * Tests for L{Nevow.Athena.KeyEvent} when using W3C-style event objects.
+ */
+Divmod.UnitTest.TestCase.subclass(Nevow.Test.TestEvent,
+                                  'TestKeyEventW3C').methods(
+    function setUp(self) {
+        self.supportsMeta = true;
+        self.eventType = Nevow.Test.TestEvent.W3CEvent;
+    },
+
+
+    function createDOMEvent(self, type, target, args) {
+        var evt = self.eventType(type, target);
+        for (var key in args) {
+            evt[key] = args[key];
+        }
+        return evt;
+    },
+
+
+    /**
+     * Properties relating to modifier keys on the original DOM are accessible
+     * on L{Nevow.Athena.KeyEvent}.
+     */
+    function test_modifiers(self) {
+        function createEvent(altKey, ctrlKey, shiftKey, metaKey) {
+            return Nevow.Athena.Event.fromDOMEvent(
+                self.createDOMEvent('keypress', null, {
+                    'altKey': !!altKey,
+                    'ctrlKey': !!ctrlKey,
+                    'shiftKey': !!shiftKey,
+                    'metaKey': !!metaKey}));
+        }
+
+        function assertModifiers(evt, altKey, ctrlKey, shiftKey, metaKey) {
+            self.assertIdentical(evt.altKey, altKey);
+            self.assertIdentical(evt.ctrlKey, ctrlKey);
+            self.assertIdentical(evt.shiftKey, shiftKey);
+            self.assertIdentical(evt.metaKey, metaKey);
+        }
+
+        assertModifiers(
+            createEvent(true),
+            true, false, false, false);
+        assertModifiers(
+            createEvent(true, true),
+            true, true, false, false);
+        assertModifiers(
+            createEvent(true, true, true),
+            true, true, true, false);
+
+        if (self.supportsMeta) {
+            assertModifiers(
+                createEvent(true, true, true, true),
+                true, true, true, true);
+        } else {
+            assertModifiers(
+                createEvent(true, true, true, true),
+                true, true, true, false)
+        };
+    },
+
+
+    /**
+     * L{Nevow.Athena.KeyEvent.getKeyCode} returns the Unicode key code for the
+     * DOM event, preferring I{keyCode} over I{which}.
+     */
+    function test_getKeyCode(self) {
+        function createEvent(keyCode, which) {
+            return Nevow.Athena.Event.fromDOMEvent(
+                self.createDOMEvent('keypress', null, {
+                    'keyCode': keyCode,
+                    'which': which}));
+        }
+
+        function assertKeyCode(evt, keyCode) {
+            self.assertIdentical(evt.getKeyCode(), keyCode);
+        }
+
+        assertKeyCode(createEvent(65), 65);
+        assertKeyCode(createEvent(65, 97), 65);
+        assertKeyCode(createEvent(0, 65), 65);
+    },
+
+
+    /**
+     * L{Nevow.Athena.KeyEvent.retKeyCode} sets the Unicode key code for the
+     * DOM event.
+     */
+    function test_setKeyCode(self) {
+        var evt = Nevow.Athena.Event.fromDOMEvent(
+            self.createDOMEvent('keypress', null, {
+                'keyCode': 65}));
+
+        self.assertIdentical(evt.getKeyCode(), 65);
+        evt.setKeyCode(97);
+        self.assertIdentical(evt.event.keyCode, 97);
+        self.assertIdentical(evt.getKeyCode(), 97);
+    });
+
+
+
+/**
+ * Tests for L{Nevow.Athena.KeyEvent} when using IE-style event objects.
+ */
+Nevow.Test.TestEvent.TestKeyEventW3C.subclass(Nevow.Test.TestEvent,
+                                              'TestKeyEventIE').methods(
+    function setUp(self) {
+        Nevow.Test.TestEvent.TestKeyEventIE.upcall(self, 'setUp');
+        self.supportsMeta = false;
+        self.eventType = Nevow.Test.TestEvent.IEEvent;
+    },
+
+
+    function createDOMEvent(self, type, target, args) {
+        // IE < 9 doesn't support "metaKey".
+        delete args['metaKey'];
+        return Nevow.Test.TestEvent.TestKeyEventIE.upcall(
+            self, 'createDOMEvent', type, target, args);
+    });
+
+
+
+/**
+ * Tests for L{Nevow.Athena.MouseEvent} when using W3C-style event objects.
+ */
+Divmod.UnitTest.TestCase.subclass(Nevow.Test.TestEvent,
+                                  'TestMouseEventW3C').methods(
+    function setUp(self) {
+        self.eventType = Nevow.Test.TestEvent.W3CEvent;
+    },
+
+
+    function createDOMEvent(self, type, target, args) {
+        var evt = self.eventType(type, target);
+        for (var key in args) {
+            evt[key] = args[key];
+        }
+        return evt;
+    },
+
+
+    /**
+     * Get the platform-specific value for the specified mouse button
+     * configuration.
+     */
+    function getButtonValue(self, left, middle, right) {
+        if (left) {
+            return 0;
+        } else if (middle) {
+            return 1;
+        } else if (right) {
+            return 2;
+        }
+    },
+
+
+    /**
+     * Assert that L{Nevow.Athena.MouseEvent.getMouseButtons} produces a mapping
+     * that matches an expected mouse button configuration.
+     */
+    function assertMouseButtons(self, evt, left, middle, right) {
+        var buttons = evt.getMouseButtons();
+        self.assertIdentical(buttons.left, !!left);
+        self.assertIdentical(buttons.middle, !!middle);
+        self.assertIdentical(buttons.right, !!right);
+    },
+
+
+    /**
+     * L{Nevow.Athena.MouseEvent.getMouseButtons} decodes the platform-specific
+     * C{event.button} value into a mapping that expresses the mouse button
+     * configuration.
+     */
+    function test_getMouseButtons(self) {
+        function createEvent(left, middle, right) {
+            return Nevow.Athena.Event.fromDOMEvent(
+                self.createDOMEvent('mouseup', null, {
+                    'button': self.getButtonValue(left, middle, right)}));
+        }
+
+        self.assertMouseButtons(
+            createEvent(true, false, false),
+            true, false, false);
+        self.assertMouseButtons(
+            createEvent(false, true, false),
+            false, true, false);
+        self.assertMouseButtons(
+            createEvent(false, false, true),
+            false, false, true);
+    },
+
+
+    /**
+     * L{Nevow.Athena.MouseEvent.getPagePosition} gets the coordinates of the
+     * event relative to the whole document.
+     */
+    function test_getPagePosition(self) {
+        var evt = Nevow.Athena.Event.fromDOMEvent(
+            self.createDOMEvent('mouseup', null, {
+                'pageX': 51,
+                'pageY': 44}));
+        var pt = evt.getPagePosition();
+        self.assertIdentical(pt.x, 51);
+        self.assertIdentical(pt.y, 44);
+    },
+
+
+    /**
+     * L{Nevow.Athena.MouseEvent.getClientPosition} gets the coordinates of the
+     * event within the browse's client area.
+     */
+    function test_getClientPosition(self) {
+        var evt = Nevow.Athena.Event.fromDOMEvent(
+            self.createDOMEvent('mouseup', null, {
+                'clientX': 51,
+                'clientY': 44}));
+        var pt = evt.getClientPosition();
+        self.assertIdentical(pt.x, 51);
+        self.assertIdentical(pt.y, 44);
+    },
+
+
+    /**
+     * L{Nevow.Athena.MouseEvent.getScreenPosition} gets the coordinates of the
+     * event within the screen as a whole.
+     */
+    function test_getScreenPosition(self) {
+        var evt = Nevow.Athena.Event.fromDOMEvent(
+            self.createDOMEvent('mouseup', null, {
+                'screenX': 51,
+                'screenY': 44}));
+        var pt = evt.getScreenPosition();
+        self.assertIdentical(pt.x, 51);
+        self.assertIdentical(pt.y, 44);
+    });
+
+
+
+/**
+ * Tests for L{Nevow.Athena.MouseEvent} when using IE-style event objects.
+ */
+Nevow.Test.TestEvent.TestMouseEventW3C.subclass(
+        Nevow.Test.TestEvent,
+        'TestMouseEventIE').methods(
+    function setUp(self) {
+        self.eventType = Nevow.Test.TestEvent.IEEvent;
+        self.oldRuntime = Divmod.Runtime.theRuntime;
+        Divmod.Runtime.theRuntime = Divmod.Runtime.InternetExplorer();
+    },
+
+
+    function tearDown(self) {
+        Divmod.Runtime.theRuntime = self.oldRuntime;
+    },
+
+
+    /**
+     * Get the platform-specific value for the specified mouse button
+     * configuration.
+     */
+    function getButtonValue(self, left, middle, right) {
+        var button = 0;
+        if (left) {
+            button |= 1;
+        }
+        if (middle) {
+            button |= 4;
+        }
+        if (right) {
+            button |= 2;
+        }
+        return button;
+    },
+
+
+    /**
+     * Internet Explorer can express configurations where multiple mouse
+     * buttons are pushed.
+     */
+    function test_getMouseButtonsMultiple(self) {
+        function createEvent(left, middle, right) {
+            return Nevow.Athena.Event.fromDOMEvent(
+                self.createDOMEvent('mouseup', null, {
+                    'button': self.getButtonValue(left, middle, right)}));
+        }
+
+        self.assertMouseButtons(
+            createEvent(true, true, false),
+            true, true, false);
+        self.assertMouseButtons(
+            createEvent(false, true, true),
+            false, true, true);
+        self.assertMouseButtons(
+            createEvent(true, true, true),
+            true, true, true);
+    },
+
+
+    /**
+     * L{Nevow.Athena.MouseEvent.getPagePosition} gets the coordinates of the
+     * event relative to the whole document. In Internet Explorer < 9 there are
+     * no C{'pageX'} or C{'pageY'} attributes instead a page position is
+     * derived from client position and the document and body scroll offsets.
+     */
+    function test_getPagePosition(self) {
+        var evt = Nevow.Athena.Event.fromDOMEvent(
+            self.createDOMEvent('mouseup', null, {
+                'clientX': 41,
+                'clientY': 34}));
+        document.documentElement = {
+            'scrollLeft': 4,
+            'scrollTop': 6};
+        document.body.scrollLeft = 6;
+        document.body.scrollTop = 4;
+
+        var pt = evt.getPagePosition();
+        self.assertIdentical(pt.x, 51);
+        self.assertIdentical(pt.y, 44);
+    });

=== modified file 'Nevow/nevow/js/Nevow/Test/TestWidget.js'
--- Nevow/nevow/js/Nevow/Test/TestWidget.js	2008-12-31 18:44:01 +0000
+++ Nevow/nevow/js/Nevow/Test/TestWidget.js	2011-07-10 13:41:23 +0000
@@ -128,6 +128,7 @@
      * the browser, and the explicitly selected handler will be invoked.
      */
     function test_connectDOMEventCustomMethod(self) {
+        event = {};
         self.widget.connectDOMEvent("onclick", "explicitClick");
         self.node.onclick();
         self.assertIdentical(self.widget.clicked, "explicitly");
@@ -138,6 +139,7 @@
      * the browser, and the explicitly selected node will be used.
      */
     function test_connectDOMEventCustomNode(self) {
+        event = {};
         self.widget.connectDOMEvent("onclick", "explicitClick", self.otherNode);
         self.otherNode.onclick();
         self.assertIdentical(self.widget.clicked, "explicitly");

=== modified file 'Nevow/nevow/test/test_javascript.py'
--- Nevow/nevow/test/test_javascript.py	2008-07-07 08:58:45 +0000
+++ Nevow/nevow/test/test_javascript.py	2011-07-10 13:41:23 +0000
@@ -51,3 +51,7 @@
 
     def test_tabbedPane(self):
         return 'Nevow.Test.TestTabbedPane'
+
+
+    def test_event(self):
+        return 'Nevow.Test.TestEvent'