prototypejs / prototype

Prototype JavaScript framework
http://prototypejs.org/
Other
3.54k stars 639 forks source link

Event.fire to support all events, not only custom ones #266

Open jwestbrook opened 9 years ago

jwestbrook commented 9 years ago

previous lighthouse ticket #697 by Radoslav Stankov


Today I have been trying to create tests for Event.delegate, and come to the conclusion that prototype needs Event.fire to fire not only custom events but custom too. I digg a lot today for this issue, I created a Event.fire (looking mainly on old Event.fire and YUI's one)

http://github.com/RStankov/javascript-playground/tree/550554caf705a231a5aa545ab9b2014008dc6a8b/Event.fire

http://gist.github.com/121011

This version now pass all prototype.js tests in - event_test.js + basic tests I created. Tested in FF3 / Safary / IE. But I think there are not enough and I'm not very good at testing.

jwestbrook commented 9 years ago

Juriy Zaytsev June 1st, 2009 @ 06:23 AM

I've been using Event.simulate (http://github.com/kangax/protolicious/blob/889a60d6d22cd59e93664ae04866e245c89b35aa/event.simulate.js) for some time now and it's been working fine so far. I know few other people are using it in their applications as well. It's probably too simplistic though (e.g. supports only HTMLEvents and MouseEvents)

jwestbrook commented 9 years ago

Juriy Zaytsev June 1st, 2009 @ 06:50 AM

A couple of comments about Event.fire:

isKeyEvent = Enumerable.include.bind($w('keydown keyup keypress'));

This looks cool, but should probably be replaced with regex :) It's much faster that way.

if (options.relatedTarget && !event.relatedTarget){ if (evenName == 'mouseout'){ event.toElement = options.relatedTarget;

evenName looks like a typo, doesn't it? What is the purpose of this assignment? Shouldn't there be a check for existence of toElement before trying to assign to it? Is there a guarantee that toElement can be assigned to (it's a host object we are talking about, after all)? I would rather avoid event augmentation.

} catch(e) {
   try {
// if initKeyEvent() is not , to create generic event - // will fail in Safari 2.x
     return createEvent('Events', eventName, bubble, options);
   } catch(e){

Why so many try/catch? try/catch are slow and should be avoided when possible. Instead, why not detect any deficiencies at load time and use boolean checks? For example:

var IS_KEY_EVENTS_CREATION_SUPPORTED = (function(){
  if (!document.createEvent) return false;
  try {
    var e = document.createEvent('KeyEvents');
    return (typeof e != 'undefined');
  } catch(e) {
    return false;
  }
})();

This snippet gives me true in FF 3.0.10 and false in nightly WebKit. Also, what if I want to fire a non-standard proprietary event, such as "mouseenter"? Do we want to allow that?

jwestbrook commented 9 years ago

Radoslav Stankov June 1st, 2009 @ 11:47 AM

10x for the helpful comments, This is an very early draft. I wanted to make the basic test cases first than to refactor. I will try to skip all ifs and try your approach with IS_KEY_EVENTS_CREATION_SUPPORTED + something like this at load time or make it lazi loaded:

if (document.createEvent){
   function createMouseEvent(){}
   function createKeyEvent(){}
   // ...
} else if (document.createEventObject){
   function createMouseEvent(){}
   function createKeyEvent(){}
   // ...
}

I'm not very experienced with cross-browsers behaviors so, please excuse if some parts of this are not perfect. The main reason I want this functionally is for testing ( especially Event.delegate ) p.s. I think that mouseenter / mouseleave should be in.

jwestbrook commented 9 years ago

Samuel Lebeau June 1st, 2009 @ 01:45 PM

@Radoslav : due to how JS scoping works, the construct you just used in your comment wouldn't work as expected, and you'd end up with last version of functions whatever host methods available.

jwestbrook commented 9 years ago

Radoslav Stankov June 1st, 2009 @ 01:55 PM

@Samuel Lebeau :10x, I know that, but every time forget about it. I generally overcome this with just variable functions :) I have make some basic changes http://gist.github.com/121011 but still have work to do, there are several code duplication patterns (+ toElement/fromElement setting / try catch) and more tests. The version http://github.com/RStankov/javascript-playground/tree/4f653f2a84e7773be33a0713b23231b4f298ec01/Event.fire works in FF3/Safary on MacOsX ( latter today will test against IE )

jwestbrook commented 9 years ago

Radoslav Stankov June 4th, 2009 @ 02:24 PM

I think I'm near to the final version http://github.com/RStankov/javascript-playground/blob/28a1f4a0abc4bdc1f8fb6dc21b138433595c65ed/Event.fire/js/event_fire.js This version pass all test on Firefox 2/3, Safari 3, IE 6/7/8, Chrome, Opera 9.2/9.6 The problems I still have are 1) mouseenter/mouseleave 2) document.fireEvent on IE have some problems on load, unload, abort, error, select, change, submit, reset,... events 3) event.cancelBubble = true, on IE 3) focus, blur bubbling on IE 4) more sophisticated tests Possible solutions: 1) for mouseenter/mouseleave I will probably use getDOMEventName, but on browsers where the isn't mouseenter/mouseleave, mouseover/mouseout event will be fired 2 and 3) I used a little work around, but probably there is more elegant solution 3) https://prototype.lighthouseapp.com/projects/8886/tickets/666-make-focus-blur-bubbles this will be very useful solution + getDOMEventName (probably will make patch for this too) I tend to use _getDOMEventName to turn focus/blur in IE to focusin/focusout on the background 4) on this I have more work, since I'm not very experienced with js tests Code reviews and suggestions are welcomed :) In the next couple of days I will try to make a patch in the next couple of day

jwestbrook commented 9 years ago

Juriy Zaytsev June 4th, 2009 @ 04:25 PM

Radoslav, don't forget to test Safari 2.x and Opera 8.54+

jwestbrook commented 9 years ago

Radoslav Stankov June 4th, 2009 @ 06:13 PM

Juriy, I always forgot those. When I expand the test suite I'll definitely test in them too

jwestbrook commented 9 years ago

Radoslav Stankov July 19th, 2009 @ 10:37 PM

I think I'm ready, finally :) Here is the patch it includes:

jwestbrook commented 9 years ago

Radoslav Stankov July 21st, 2009 @ 05:38 PM

Strange but the patch I have attached wasn't accessible.

From 07c93e9d392c8198f6411ac218c4618fa2174267 Mon Sep 17 00:00:00 2001
From: RStankov <rstankov@gmail.com>
Date: Sun, 19 Jul 2009 23:26:21 +0300
Subject: [PATCH] Event.fire can now fire dom events too and all event functional tests are added as unit tests via Event.fire

---
 src/dom/event.js              |  213 ++++++++++++++++---
 test/unit/event_test.js       |  472 ++++++++++++++++++++++++++++++++++++++++-
 test/unit/fixtures/event.html |   38 ++++
 3 files changed, 692 insertions(+), 31 deletions(-)

diff --git a/src/dom/event.js b/src/dom/event.js
index bbdb4d1..e68e490 100644
--- a/src/dom/event.js
+++ b/src/dom/event.js
@@ -67,7 +67,8 @@
     _isButton = function(event, code) {
       switch (code) {
         case 0: return event.which == 1 && !event.metaKey;
-        case 1: return event.which == 1 && event.metaKey;
+        case 1: return event.which == 2 && !event.metaKey;
+        case 2: return event.which == 3 && !event.metaKey;
         default: return false;
       }
     };
@@ -247,10 +248,14 @@
   if (Prototype.Browser.IE) {
     function _relatedTarget(event) {
       var element;
-      switch (event.type) {
-        case 'mouseover': element = event.fromElement; break;
-        case 'mouseout':  element = event.toElement;   break;
-        default: return null;
+      if (event.relatedTarget){
+        element = event.relatedTarget;
+      } else { 
+        switch (event.type) {
+          case 'mouseover': element = event.fromElement; break;
+          case 'mouseout':  element = event.toElement;   break;
+          default: return null;
+        }
       }
       return Element.extend(element);
     }
@@ -494,44 +499,192 @@
   }

   /**
-   *  Event.fire(element, eventName[, memo[, bubble = true]]) -> Event
+   *  Event.fire(element, customEventName[, memo]) -> Event
    *  - memo (?): Metadata for the event. Will be accessible through the
    *    event's `memo` property.
-   *  - bubble (Boolean): Whether the event will bubble.
-   *
-   *  Fires a custom event of name `eventName` with `element` as its target.
+   * Event.fire(element, eventName[, options]) -> Event
+   *  - options (?): Object with options for the event.
+   *    For all events options - bubbles, cancelable, view, ctrlKey, altKey, shiftKey, metaKey.
+   *    For mouse event options could be also - detail, screenX, screenY, clientX, clientY, button, relatedTarget
+   *    For key events - keyCode, charCode
+   *  
+   *  Fires a event of name `eventName` with `element` as its target.
    *
    *  Custom events must include a colon (`:`) in their names.
   **/
-  function fire(element, eventName, memo, bubble) {
-    element = $(element);
+  var fire = (function(){
+    var mouseEvent    = /^(click|dblclick|mouseover|mouseout|mousedown|mouseup|mousemove|mouseenter|mouseleave|contextmenu)$/,
+        keyEvent      = /^(keydown|keyup|keypress)$/;
+
+    var defaultOptions = {
+      event: {
+        bubbles:    true,
+        cancelable: true,
+        view:       document.defaultView,
+        ctrlKey:    false,
+        altKey:     false,
+        shiftKey:   false,
+        metaKey:    false
+      },
+      mouse: {
+        detail:         1,
+        screenX:        0,
+        screenY:        0,
+        clientX:        0,
+        clientY:        0,
+        button:         0,
+        relatedTarget:  null
+      },
+      key: {
+        keyCode:  0,
+        charCode: 0
+      }
+    };

-    if (Object.isUndefined(bubble))
-      bubble = true;
+    var createEvent, dispatchEvent;
+    if (document.createEvent){
+      createEvent = (function(){
+        var createEvent = function(name, eventName, options){
+          var event = document.createEvent(name);

-    if (element == document && document.createEvent && !element.dispatchEvent)
-      element = document.documentElement;
+          event.initEvent(eventName, options.bubbles, options.cancelable);

-    var event;
-    if (document.createEvent) {
-      event = document.createEvent('HTMLEvents');
-      event.initEvent('dataavailable', true, true);
-    } else {
-      event = document.createEventObject();
-      event.eventType = bubble ? 'ondataavailable' : 'onfilterchange';
+          delete(options.bubbles);
+          delete(options.cancelable);
+
+          return Object.extend(event, options);
+        };
+
+        var createKeyEvent = (function(){
+          try { // only Firefox supports KeyEvents
+            if (typeof document.createEvent('KeyEvents') != 'undefined')
+              return function(eventName, options){
+                var event = document.createEvent('KeyEvents');
+                  event.initKeyEvent(eventName, options.bubbles, options.cancelable, options.view, 
+                    options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
+                    options.keyCode, options.charCode);
+                return event;
+              };
+          } catch(e){}
+
+          try { // try to use generic event (will fail in Safari 2.x)
+            if (typeof document.createEvent('Events') != 'undefined')
+              return createEvent.curry('Events');
+          } catch(e){}
+
+          // generic event fails, use UIEvent for Safari 2.x
+          return createEvent.curry('UIEvents');
+        }());
+
+        return function(eventName, options){
+          if (eventName.include(':')){
+            return createEvent('HTMLEvents', 'dataavailable', options);
+          }
+
+          if (mouseEvent.test(eventName)){
+            options = Object.extend(Object.clone(defaultOptions.mouse), options);
+            var event = document.createEvent('MouseEvents');
+
+            if (event.initMouseEvent){
+              event.initMouseEvent(eventName, options.bubbles, options.cancelable, options.view, 
+                options.detail, options.screenX, options.screenY, options.clientX, options.clientY, 
+                options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,  
+                options.button, options.relatedTarget);
+
+              return event;
+            }
+
+            // Safari 2.x doesn't implement initMouseEvent(), the closest thing available is UIEvents
+            return createEvent('UIEvents', eventName, options);
+          }
+
+          if (keyEvent.test(eventName)){
+            return createKeyEvent(eventName, Object.extend(Object.clone(defaultOptions.key), options));
+          }
+
+          return createEvent('HTMLEvents', eventName, options);
+        };
+      })();
+
+      dispatchEvent = function(element, event){
+        if (element == document && !element.dispatchEvent) element = document.documentElement;
+
+        element.dispatchEvent(event);
+      };
+    } else /* if (document.createEventObject()) */ {
+      createEvent = function(eventName, options){
+        if (eventName.include(':')){
+          eventName = options.bubbles ? 'dataavailable' : 'filterchange';
+        } else if (mouseEvent.test(eventName)){
+          options = Object.extend(Object.clone(defaultOptions.mouse), options);
+
+          // fix options, IE button property
+          switch(options.button){
+            case 0:  options.button = 1; break;
+            case 1:  options.button = 4; break;
+            case 2:  /* no change */     break;
+            default: options.button = 0;                    
+          }
+        } else if (keyEvent.test(eventName)){
+          options = Object.extend(Object.clone(defaultOptions.key), options);
+
+          if (options.charCode > 0)
+            options.keyCode = options.charCode;
+
+          delete(options.charCode);
+        }
+
+        options.eventType = 'on' + eventName;
+
+        return Object.extend(document.createEventObject(), options);
+      };
+
+      dispatchEvent = function(element, event){
+        // for some reason event.cancelBubble doesn't work
+        // and document.fireEvent doesn't support several events
+        // in both cases we could just take all events form 'prototype_event_registry'
+        if (!event.bubbles || (element == document && event.eventType in element)){
+          if (Object.isFunction(element[event.eventType])) element[event.eventType]();
+          var registry = Element.retrieve(element, 'prototype_event_registry');
+          if (registry){
+            var handlers = registry.get(event.eventName);
+            if (handlers){
+              handlers.each(function(responder){
+                responder(event);
+              });
+            }
+          }
+        } else {
+          element.fireEvent(event.eventType, event);
+        } 
+      };
     }

-    event.eventName = eventName;
-    event.memo = memo || { };
+    return function(element, eventName, options){
+      var memo;

-    if (document.createEvent)
-      element.dispatchEvent(event);
-    else
-      element.fireEvent(event.eventType, event);
+      // custom events take (element, eventName[, memo[, bubbles]]) arguments
+      if (eventName.include(':')){
+        memo    = options;
+        options = {bubbles: Object.isUndefined(arguments[3]) ? true : arguments[3] };
+      } else {
+        eventName = _getDOMEventName(eventName);
+        options   = Object.extend(Object.clone(defaultOptions.event), options);
+        memo      = options.memo;

-    return Event.extend(event);
-  }
+        delete(options.memo);
+      }
+
+      var event = createEvent(eventName, options);

+      event.eventName = eventName;
+      event.memo      = memo;
+
+      dispatchEvent($(element), event);
+
+      return Event.extend(event);
+    };
+  })();

   Object.extend(Event, Event.Methods);

diff --git a/test/unit/event_test.js b/test/unit/event_test.js
index fc6df25..967c54f 100644
--- a/test/unit/event_test.js
+++ b/test/unit/event_test.js
@@ -1,7 +1,35 @@
 var documentLoaded = document.loaded;

+// helpers for functional simulation test
+Element.addMethods({
+  passed: function(el, message) {
+    el = $(el);
+    el.className = 'passed';
+    (el.down('span') || el).update(message || 'Test passed!');
+  },
+
+  failed: function(el, message) {
+    el = $(el);
+    el.className = 'failed';
+    (el.down('span') || el).update(message || 'Test failed');
+  },
+
+  clear: function(el, message) {
+    el = $(el);
+    el.className = '';
+    (el.down('span') || el).update(message || '');
+  },
+
+  isPassed: function(el){
+    return $(el).className == 'passed';
+  }
+});
+
 new Test.Unit.Runner({
-  
+/*
+ * Prototype Unit tests
+ */
+
   // test firing an event and observing it on the element it's fired from
   testCustomEventFiring: function() {
     var span = $("span"), fired = false, observer = function(event) {
@@ -235,6 +263,448 @@ new Test.Unit.Runner({
     $('container').down().observe("test:somethingHappened", Prototype.emptyFunction);
     $('container').innerHTML += $('container').innerHTML;
     this.assertUndefined($('container').down(1)._prototypeEventID);
+  },
+
+/*
+ * Prototype current Functional tests
+ */
+ 
+    testBasicClickCallback: function(){
+     var runned = 0;
+     $('basic').observe('click', function(e){
+        $('basic').passed();
+
+        runned++;
+
+        if ($('basic_remove')){
+          $('basic_remove').show();
+        } else {
+          this.fail();
+          $('basic').failed();
+        }
+      }.bind(this));
+
+      $('basic_remove').observe('click', function(e){
+        el = $('basic');
+        el.passed('This test should now be inactive (try clicking)');
+        el.stopObserving('click');
+
+        $('basic_remove').remove();
+      }).hide();
+
+      $('basic').fire('click');
+
+      this.assertEqual(runned, 1);
+      this.assert($('basic_remove').visible());
+
+      $('basic_remove').fire('click');
+
+      $('basic').fire('click');
+
+      this.assertEqual(runned, 1);
+      this.assertNull($('basic_remove'));
+    },
+
+    testInlineEvents: function(){
+      // #inline_test onclick="Event.stop(event); $(this).passed();"
+      this.assert(!$('inline_test').isPassed());
+
+      $('inline_test').fire('click');
+
+      this.assert($('inline_test').isPassed());
+    },
+
+    testScopeOfTheHandler: function(){
+      var runned = false;
+
+      $('basic2').observe('click', function(e) {
+        runned = true;
+
+        if(this === window){
+          $('basic2').failed('Window scope! (needs scope correction)');
+        } else {
+          this.passed();
+        }
+      });
+
+      this.assert(!$('basic2').isPassed());
+
+      $('basic2').fire('click');
+
+      this.assert(runned);
+      this.assert($('basic2').isPassed());
+    },
+
+    testEventObjectAsFirstArgument: function(){
+      $('basic3').observe('click', function(evt) {
+         el = $('basic3');
+         if (typeof evt != 'object') this.failed('Expected event object for first argument');
+         else this.passed('Good first argument');
+       });
+
+       $('basic3').fire('click');
+
+       this.assert($('basic3').isPassed());
+    },
+
+    testLeftMouseClick: function(){
+      $w('left middle right').each(function(button){
+        Event.observe(button, 'mousedown', function(e){
+          if (Event['is' + button.capitalize() + 'Click'](e)){
+            this.passed('Squeak!')
+          } else {
+            this.failed('OH NO!');
+            }
+        });
+      });
+      var BUTTONS = {
+        left: 0,
+        middle: 1,
+        right: 2
+      };
+
+      $w('left middle right').each(function(id){
+        var element = $(id);
+
+        for(var button in BUTTONS){
+          element.clear();
+          element.fire('mousedown', { button: BUTTONS[button] });
+          this.assertEqual(id == button, element.isPassed(), id + " on " + button);
+        }
+      }.bind(this));
+
+      $('left').fire('mousedown');
+      this.assert($('left').isPassed());
+
+      $('middle').fire('mousedown', { button: BUTTONS.middle });
+      this.assert($('middle').isPassed());
+
+      $('right').fire('mousedown', { button: BUTTONS.right });
+      this.assert($('right').isPassed());
+    },
+
+    testContextMenuEvent: function(){
+      $('context').observe('contextmenu', function(e){
+          this.passed();
+          Event.stop(e);
+      });
+
+      $('context').fire('contextmenu');
+      this.assert($('context').isPassed());
+    },
+
+    testEventElementMethod: function(){
+      $('target').observe('click', function(e) {
+        if (e.element() == this && e.target == this) this.passed();
+        else this.failed();
+      });
+
+      $('target').fire('click');
+      this.assert($('target').isPassed());
+    },
+
+    testCurrentTarget: function(){
+      $('currentTarget').observe('click', function(e){
+        this[ e.element() == this ? 'passed' : 'failed']();
+      });
+
+      Element.Methods.clear('currentTarget');
+
+      $('currentTarget').fire('click');
+      this.assert($('currentTarget').isPassed());
+    },
+
+    testEventFindElementAfterClickEvent: function(){
+      $('findElement').observe('click', function(e){
+        if(e.findElement('p') == this && e.findElement('body') == document.body &&
+           e.findElement('foo') == null) this.passed();
+        else this.failed();
+      });
+
+      $('findElement').fire('click');
+      this.assert($('findElement').isPassed());
+    },
+
+    testObjectInspect: function(){
+      $('obj_inspect').observe('click', function(e){
+        el = $('obj_inspect')
+        try { el.passed(Object.inspect(e)) }
+        catch (err) { el.failed('Failed! Error thrown') }
+      });
+
+      $('obj_inspect').fire('click');
+      this.assert($('obj_inspect').isPassed());
+    },
+
+    testBindAsEventListener: function(){
+      $('bind').observe('click', function(e, str, arr){
+        el = $('bind')
+        try {
+          if (arguments.length != 3) throw arguments.length + ' arguments: ' + $A(arguments).inspect()
+          if (str != 'foo') throw 'wrong string: ' + str
+          if (arr.constructor != Array) throw '3rd parameter is not an array'
+          el.passed();
+        }
+        catch (err) { el.failed(err.toString()) }
+      }.bindAsEventListener(document.body, 'foo', [1,2,3]));
+
+      $('bind').fire('click');
+      this.assert($('bind').isPassed());
+    },
+
+    testStopPropagation: function(){
+      $('stop').observe('click', function(e){
+        e.stop();
+        this.passed();
+      });
+      $('container').observe('click', function(e){
+        $('stop').failed();
+      });
+
+
+      this.assert(!$('stop').isPassed());
+
+      $('stop').fire('click');
+
+      this.assert($('stop').isPassed());
+    },
+
+    testPreventDefault: function(){
+        $('checkbox').checked = false;
+        $('checkbox').observe('click', function(e){
+          $('prevent_default').passed();
+          e.preventDefault();
+        });
+
+        $('checkbox').fire('click');
+
+        this.assert(!$('checkbox').checked);
+        this.assert($('prevent_default').isPassed());
+    },
+
+    testNotStopingPropagation: function(){
+      $('container2').observe('click', function(e){
+        $('don_not_stop').passed();
+      });
+
+      this.assert(!$('don_not_stop').isPassed());
+
+      $('don_not_stop').fire('click');
+
+      this.assert($('don_not_stop').isPassed());
+    },
+
+    testKeyUp: function(){
+      $('keyup').observe('keyup', function(e){
+         this.assertEqual(65, e.keyCode);
+         this.assert(!e.charCode);
+
+         el = $('keyup_log');
+         el.passed('Key captured: the length is ' + $('keyup').value.length);
+       }.bind(this));
+
+       $('keyup').fire('keyup', {keyCode: 65, charCode: 0}); // 'a' char
+       this.assert($('keyup_log').isPassed());
+    },
+
+    testMouseEnterMouseLeave: function(){
+       var element = $('mouseenter'), 
+            child   = element.down(), 
+            parent  = element.up();
+
+      element.observe('mouseenter', function(event) {
+        if ($(event.relatedTarget).descendantOf(element)){
+          this.fail();
+          element.failed('<code id="mouseenter_child">mouseenter</code> failed');
+        } else {
+          element.passed('<code id="mouseenter_child">mouseenter</code> passed');
+        }
+      }.bind(this));
+
+      element.observe('mouseleave', function(event) {
+        if ($(event.relatedTarget).descendantOf($('mouseenter'))){
+          this.fail();
+          element.failed('<code id="mouseenter_child">mouseleave</code> failed');
+        } else {
+          element.passed('<code id="mouseenter_child">mouseleave</code> passed');
+        }
+      }.bind(this));
+
+      // for browser who do not support natively mouseenter/mouseleave make extra checks
+      if (!('onmouseenter' in document.documentElement && 'onmouseleave' in document.documentElement)){
+        element.fire('mouseover', { relatedTarget: child });
+        element.fire('mouseout', { relatedTarget: child });
+
+        this.assert(! element.className );
+
+        element.fire('mouseout', { relatedTarget: parent });
+        this.assert(element.isPassed());
+      }
+
+      Element.Methods.clear(element, '<code id="mouseenter_child">mouseenter/mouseleave</code> test');
+
+      element.fire('mouseenter', { relatedTarget: parent });
+
+      this.assert(element.isPassed());
+
+      Element.Methods.clear(element, '<code id="mouseenter_child">mouseenter/mouseleave</code> test');
+
+      element.fire('mouseleave', { relatedTarget: parent });
+
+      this.assert(element.isPassed());
+    },
+
+    testUnloadEvent: function(){
+      var runned = 0;
+
+      document.onunload = function(){ ++runned; }
+      Event.observe(document, 'unload', function(event){
+        if (!event.target) {
+          this.fail('event.target should not be null!');
+        }
+        ++runned;
+      }.bind(this));
+
+      Event.fire(document, 'unload');
+
+      this.assertEqual(2, runned);
+    },
+
+/*
+ * Prototype Event.fire tests
+ */
+ 
+  testFiringClickEvent: function(){
+    var doc = 0, body = 0, main = 0, main = 0, inner = 0;
+
+    document.observe('click',               function(e){ if (e.element() == document) ++doc; });
+    $$('body').first().observe('click',     function(e){ ++body;   });
+    $('test-bubble').observe('click',       function(e){ ++main;   });
+    $('test-bubble-inner').observe('click', function(e){ ++inner;  });
+
+    Event.fire(document, 'click');
+    this.assertEqual(1, doc);
+
+    Event.fire($$('body').first(), 'click');
+    this.assertEqual(1, doc);
+    this.assertEqual(1, body);
+
+    Event.fire('test-bubble', 'click');
+    this.assertEqual(1, doc);
+    this.assertEqual(2, body);
+    this.assertEqual(1, main);
+
+    Event.fire('test-bubble-inner', 'click');
+    this.assertEqual(1, doc);
+    this.assertEqual(3, body);
+    this.assertEqual(2, main);
+    this.assertEqual(1, inner);
+
+    // just in case re-test
+    Event.fire('test-bubble-inner', 'click');
+    this.assertEqual(1, doc);
+    this.assertEqual(4, body);
+    this.assertEqual(3, main);
+    this.assertEqual(2, inner);
+
+    Event.fire('test-bubble-inner', 'click', {bubbles: false});
+    this.assertEqual(1, doc);
+    this.assertEqual(4, body);
+    this.assertEqual(3, main);
+    this.assertEqual(3, inner);
+
+    document.stopObserving('click');
+    document.body.stopObserving('click');
+    $('test-bubble').stopObserving('click');
+    $('test-bubble-inner').stopObserving('click');
+  },
+
+  testStropingFiredEvent: function(){
+    var main = 0, inner = 0;
+
+    $('test-event-stop').observe('click',         function(){ ++main; });
+    $('test-event-stop-inner').observe('click',   function(e){ ++inner; e.stop(); });
+
+    Event.fire('test-event-stop-inner', 'click');
+    this.assertEqual(0, main);
+    this.assertEqual(1, inner);
+  },
+
+  testFiringKeyUpEvent: function(){
+    var doc = 0, body = 0, main = 0, main = 0, inner = 0;
+
+    document.observe('keyup',                 function(e){ if (e.element() == document) ++doc; });
+    $$('body').first().observe('keyup',       function(e){ ++body; });
+    $('test-bubble2').observe('keyup',        function(e){ ++main; });
+    $('test-bubble2-inner').observe('keyup',  function(e){ ++inner;    });
+
+    Event.fire(document, 'keyup');
+    this.assertEqual(1, doc);
+
+    Event.fire($$('body').first(), 'keyup');
+    this.assertEqual(1, doc);
+    this.assertEqual(1, body);
+
+    Event.fire('test-bubble2', 'keyup');
+    this.assertEqual(1, doc);
+    this.assertEqual(2, body);
+    this.assertEqual(1, main);
+
+    Event.fire('test-bubble2-inner', 'keyup');
+    this.assertEqual(1, doc);
+    this.assertEqual(3, body);
+    this.assertEqual(2, main);
+    this.assertEqual(1, inner);
+
+    // just in case re-test
+    Event.fire('test-bubble2-inner', 'keyup');
+    this.assertEqual(1, doc);
+    this.assertEqual(4, body);
+    this.assertEqual(3, main);
+    this.assertEqual(2, inner);
+
+    Event.fire('test-bubble2-inner', 'keyup', {bubbles: false});
+    this.assertEqual(1, doc);
+    this.assertEqual(4, body);
+    this.assertEqual(3, main);
+    this.assertEqual(3, inner);
+
+    document.stopObserving('keyup');
+    document.body.stopObserving('keyup');
+    $('test-bubble2').stopObserving('keyup');
+    $('test-bubble2-inner').stopObserving('keyup');
+  },
+  // focus event in IE's don't bubble (hope Prototype will fix this in next versions)
+  testFiringFocusInEvent: function(){
+    var doc = 0, body = 0, main = 0, main = 0, inner = 0;
+
+    document.observe('focusin',                   function(e){ if (e.element() == document) ++doc;});
+    $$('body').first().observe('focusin',         function(e){ ++body; });
+    $('test-html-event').observe('focusin',       function(e){ ++main; });
+    $('test-html-event-text').observe('focusin',  function(e){ ++inner;    });
+
+    Event.fire(document, 'focusin');
+    this.assertEqual(1, doc);
+
+    Event.fire($$('body').first(), 'focusin');
+    this.assertEqual(1, doc);
+    this.assertEqual(1, body);
+
+    Event.fire('test-html-event', 'focusin');
+    this.assertEqual(1, doc);
+    this.assertEqual(2, body);
+    this.assertEqual(1, main);
+
+    Event.fire('test-html-event-text', 'focusin');
+    this.assertEqual(1, doc);
+    this.assertEqual(3, body);
+    this.assertEqual(2, main);
+    this.assertEqual(1, inner);
+
+    document.stopObserving('focusin');
+    document.body.stopObserving('focusin');
+    $('test-html-event').stopObserving('focusin');
+    $('test-html-event-text').stopObserving('focusin');
   }
 });

diff --git a/test/unit/fixtures/event.html b/test/unit/fixtures/event.html
index baa88a7..3ecad09 100644
--- a/test/unit/fixtures/event.html
+++ b/test/unit/fixtures/event.html
@@ -2,3 +2,41 @@
   <p id="inner">One two three <span id="span">four</span></p>
 </div>
 <div id="container"><div></div></div>
+<div id="test-bubble"><span id="test-bubble-inner"></span></div>
+<div id="test-event-stop"><span id="test-event-stop-inner"></span></div>
+<div id="test-bubble2"><span id="test-bubble2-inner"></span></div>
+<div id="test-html-event"><input type="text" id="test-html-event-text" value="" /></div>
+<p id="basic">A basic event test - <strong>click here</strong></p>
+<p id="basic_remove" class="subtest"><strong>click</strong> to stop observing the first test</p><p id="inline_test" onclick="Event.stop(event); $(this).passed();"><strong>click</strong> to ensure generic Event methods work on inline handlers</p>
+
+<p id="basic2"><strong>Scope</strong> test - scope of the handler should be this element</p>
+
+<p id="basic3"><strong>Event object</strong> test - should be present as a first argument</p>
+
+<p>Mouse click:  <span class="button" id="left">left</span> <span class="button" id="middle">middle</span> <span class="button" id="right">right</span></p>
+
+<p id="context">Context menu event (tries to prevent default)</p>
+
+<p id="target">Event.element() test</p>
+
+<p id="currentTarget"><span>Event.currentTarget test</span></p>
+
+<p id="findElement"><span>Event.findElement() test</span></p>
+
+<p id="obj_inspect"><code>Object.inspect(event)</code> test</p>
+
+<p id="bind"><code>bindAsEventListener()</code> test</p>
+
+<div id="container"><p id="stop"><strong>Stop propagation</strong> test (bubbling)</p></div>
+
+<div id="container2"><p id="don_not_stop"><strong>Propagation</strong> test (bubbling)</p></div>
+
+<div>
+  <p id="keyup_log"><strong>Keyup</strong> test - focus on the textarea and type</p>
+  <textarea id="keyup" class="subtest"></textarea>
+</div>
+
+<div>
+  <p id="mouseenter"><code>mouseenter/mouseleave</code> test</p>
+</div>
+<p id="prevent_default"><span>Test - Event.preventDefault()</span><input type="checkbox" name="checkbox" id="checkbox"></p>
\ No newline at end of file
-- 
1.6.2.1