From 3977e66796a415234eb0278b077fafad82d63ed2 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Fri, 12 Dec 2008 23:53:39 -0600 Subject: [PATCH 1/4] Added Element#store and Element#retrieve for safe, hash-backed storage of element metadata (no memory leaks). Also added Element#getStorage for working with the element's storage hash directly. Hat tip: Mootools. --- CHANGELOG | 2 ++ src/dom/dom.js | 38 ++++++++++++++++++++++++++++++++++++++ test/unit/dom_test.js | 17 +++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index fa101e6..71f7cea 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Added Element#store and Element#retrieve for safe, hash-backed storage of element metadata (no memory leaks). Also added Element#getStorage for working with the element's storage hash directly. Hat tip: Mootools. (ZenCocoon, Andrew Dupont) + * Fix issue where certain versions of Safari treat class names case-insensitively in Selector/$$ queries. (Andrew Dupont, kangax, Brice) * Fix issue where Function#argumentNames returned incorrect results in IE when comments were intermixed with argument names. (Christophe Porteneuve, T.J. Crowder) diff --git a/src/dom/dom.js b/src/dom/dom.js index 0b59ca1..e90766f 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -1205,3 +1205,41 @@ document.viewport = { window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); } }; + +Element.Storage = { + UID: 1 +}; + +Element.addMethods({ + getStorage: function(element) { + if (!(element = $(element))) return; + + if (Object.isUndefined(element._prototypeUID)) + element._prototypeUID = [Element.Storage.UID++]; + + var uid = element._prototypeUID[0]; + + if (!Element.Storage[uid]) + Element.Storage[uid] = $H(); + + return Element.Storage[uid]; + }, + + store: function(element, key, value) { + if (!(element = $(element))) return; + element.getStorage().set(key, value); + }, + + retrieve: function(element, key, defaultValue) { + if (!(element = $(element))) return; + + var hash = element.getStorage(), value = hash.get(key); + + if (Object.isUndefined(value)) { + hash.set(key, defaultValue); + value = defaultValue; + } + + return value; + } +}); diff --git a/test/unit/dom_test.js b/test/unit/dom_test.js index 00d695c..4145e40 100644 --- a/test/unit/dom_test.js +++ b/test/unit/dom_test.js @@ -1365,6 +1365,23 @@ new Test.Unit.Runner({ constants.each(function(pair) { this.assertEqual(Node[pair.key], pair.value); }, this); + }, + + testElementStorage: function() { + var element = $('test-empty'); + element.store('foo', 'bar'); + this.assertEqual("bar", element.retrieve("foo"), "Setting and reading a property"); + element.store('foo', 'thud'); + this.assertEqual("thud", element.retrieve("foo"), "Re-setting and reading property"); + + element.store('bar', 'narf'); + this.assertEnumEqual($w('foo bar'), element.getStorage().keys(), "Getting the storage hash"); + element.getStorage().unset('bar'); + this.assertEnumEqual($w('foo'), element.getStorage().keys(), "Getting the storage hash after unsetting a key"); + + + var clonedElement = $('test-empty').cloneNode(false); + this.assert(!('_prototypeUID' in clonedElement), "Cloning a node should not confuse the storage engine"); } }); From 77b9a2614a90bd758bf0cface597019587103e68 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 14 Dec 2008 01:35:12 -0600 Subject: [PATCH 2/4] Alter element storage API to handle the `window` object gracefully. --- CHANGELOG | 2 +- src/dom/dom.js | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 71f7cea..6933707 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -* Added Element#store and Element#retrieve for safe, hash-backed storage of element metadata (no memory leaks). Also added Element#getStorage for working with the element's storage hash directly. Hat tip: Mootools. (ZenCocoon, Andrew Dupont) +* Add Element#store and Element#retrieve for safe, hash-backed storage of element metadata (no memory leaks). Also add Element#getStorage for working with the element's storage hash directly. Hat tip: Mootools. (ZenCocoon, Andrew Dupont) * Fix issue where certain versions of Safari treat class names case-insensitively in Selector/$$ queries. (Andrew Dupont, kangax, Brice) diff --git a/src/dom/dom.js b/src/dom/dom.js index e90766f..4cfbe20 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -1214,11 +1214,15 @@ Element.addMethods({ getStorage: function(element) { if (!(element = $(element))) return; - if (Object.isUndefined(element._prototypeUID)) - element._prototypeUID = [Element.Storage.UID++]; - - var uid = element._prototypeUID[0]; - + var uid; + if (element === window) { + uid = 0; + } else { + if (Object.isUndefined(element._prototypeUID)) + element._prototypeUID = [Element.Storage.UID++]; + uid = element._prototypeUID[0]; + } + if (!Element.Storage[uid]) Element.Storage[uid] = $H(); @@ -1232,8 +1236,7 @@ Element.addMethods({ retrieve: function(element, key, defaultValue) { if (!(element = $(element))) return; - - var hash = element.getStorage(), value = hash.get(key); + var hash = Element.getStorage(element), value = hash.get(key); if (Object.isUndefined(value)) { hash.set(key, defaultValue); From 3b211052370923ab6af73d7c440b4a0aeb0c788f Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 14 Dec 2008 01:35:49 -0600 Subject: [PATCH 3/4] Alter event system to use new element storage API rather than have its own global hashtable. --- CHANGELOG | 2 + src/dom/event.js | 186 ++++++++++++++++++++++++---------------- test/unit/event_test.js | 18 ++-- 3 files changed, 123 insertions(+), 83 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6933707..58ab25d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Alter event system to use new element storage API rather than have its own global hashtable. (Andrew Dupont) + * Add Element#store and Element#retrieve for safe, hash-backed storage of element metadata (no memory leaks). Also add Element#getStorage for working with the element's storage hash directly. Hat tip: Mootools. (ZenCocoon, Andrew Dupont) * Fix issue where certain versions of Safari treat class names case-insensitively in Selector/$$ queries. (Andrew Dupont, kangax, Brice) diff --git a/src/dom/event.js b/src/dom/event.js index c090fa2..86da978 100644 --- a/src/dom/event.js +++ b/src/dom/event.js @@ -162,67 +162,61 @@ Event.extend = Prototype.K; } - function _getEventID(element) { - if (element._prototypeEventID) return element._prototypeEventID[0]; - return element._prototypeEventID = [++_getEventID.id]; - } - _getEventID.id = 1; - - function _getDOMEventName(eventName) { - if (eventName && eventName.include(':')) return 'dataavailable'; - return eventName; - } - - function _getCacheForID(id) { - return Event.cache[id] = Event.cache[id] || { }; - } - - function _getRespondersForEvent(id, eventName) { - var c = _getCacheForID(id); - return c[eventName] = c[eventName] || []; - } - function _createResponder(element, eventName, handler) { - var id = _getEventID(element), r = _getRespondersForEvent(id, eventName); + // We don't set a default on the call to Element#retrieve so that we can + // handle the element's "virgin" state. + var registry = Element.retrieve(element, 'prototype_event_registry'); + + if (Object.isUndefined(registry)) { + // First time we've handled this element. Put it into the cache. + CACHE.push(element); + registry = Element.retrieve(element, 'prototype_event_registry', $H()); + } + var respondersForEvent = registry.get(eventName); + if (Object.isUndefined()) { + respondersForEvent = []; + registry.set(eventName, respondersForEvent); + } + // Work around the issue that permits a handler to be attached more than // once to the same element & event type. - if (r.pluck('handler').include(handler)) return false; - - var responder = function(event) { - if (!Event || !Event.extend || - // If it's a custom event, but not the _correct_ custom event, ignore it. - (!Object.isUndefined(event.eventName) && event.eventName !== eventName)) - return false; - - Event.extend(event); - handler.call(element, event); - }; + if (respondersForEvent.pluck('handler').include(handler)) return false; + + var responder; + if (eventName.include(":")) { + // Custom event. + responder = function(event) { + // If it's not a custom event, ignore it. + if (Object.isUndefined(event.eventName)) + return false; + + // If it's a custom event, but not the _correct_ custom event, ignore it. + if (event.eventName !== eventName) + return false; + + Event.extend(event); + handler.call(element, event); + }; + } else { + // Ordinary event. + responder = function(event) { + Event.extend(event); + handler.call(element, event); + }; + } responder.handler = handler; - r.push(responder); + respondersForEvent.push(responder); return responder; } - - function _findResponder(id, eventName, handler) { - var r = _getRespondersForEvent(id, eventName); - return r.find(function(responder) { - return responder.handler === handler; - }); - } - - function _destroyResponder(id, eventName, handler) { - var c = _getCacheForID(id); - if (Object.isUndefined(c[eventName])) return false; - c[eventName] = c[eventName].without(_findResponder(id, eventName, handler)); - } - - function _destroyCache() { - for (var id in Event.cache) { - for (var eventName in Event.cache[id]) - Event.cache[id][eventName] = null; - } + + function _destroyCache() { + for (var i = 0, length = CACHE.length; i < length; i++) + Event.stopObserving(CACHE[i]); } + + var CACHE = []; // Internet Explorer needs to remove event handlers on page unload // in order to avoid memory leaks. @@ -238,54 +232,94 @@ function observe(element, eventName, handler) { element = $(element); - var name = _getDOMEventName(eventName), - responder = _createResponder(element, eventName, handler); - + + var responder = _createResponder(element, eventName, handler); + if (!responder) return element; - if (element.addEventListener) - element.addEventListener(name, responder, false); - else - element.attachEvent("on" + name, responder); + if (eventName.include(':')) { + // Custom event. + if (element.addEventListener) + element.addEventListener("dataavailable", responder, false); + else { + // We observe two IE-proprietarty events: one for custom events that + // bubble and one for custom events that do not bubble. + element.attachEvent("ondataavailable", responder); + element.attachEvent("onfilterchange", responder); + } + } else { + // Ordinary event. + if (element.addEventListener) + element.addEventListener(eventName, responder, false); + else + element.attachEvent("on" + eventName, responder); + } return element; } function stopObserving(element, eventName, handler) { element = $(element); - var id = _getEventID(element), name = _getDOMEventName(eventName); + + var registry = Element.retrieve(element, 'prototype_event_registry'); + + if (Object.isUndefined(registry)) return element; if (eventName && !handler) { // If an event name is passed without a handler, we stop observing all // handlers of that type. - _getRespondersForEvent(id, eventName).each(function(r) { - element.stopObserving(eventName, r.handler); + var responders = registry.get(eventName); + + if (Object.isUndefined(responders)) return element; + + responders.each( function(r) { + Element.stopObserving(element, eventName, r.handler); }); return element; } else if (!eventName) { // If both the event name and the handler are omitted, we stop observing // _all_ handlers on the element. - Object.keys(_getCacheForID(id)).each(function(eventName) { - element.stopObserving(eventName); + registry.each( function(pair) { + var eventName = pair.key, responders = pair.value; + + responders.each( function(r) { + Element.stopObserving(element, eventName, r.handler); + }); }); return element; } - - var responder = _findResponder(id, eventName, handler); + + var responders = registry.get(eventName); + var responder = responders.find( function(r) { return r.handler === handler; }); if (!responder) return element; - - if (element.removeEventListener) - element.removeEventListener(name, responder, false); - else - element.detachEvent('on' + name, responder); - - _destroyResponder(id, eventName, handler); + + if (eventName.include(':')) { + // Custom event. + if (element.removeEventListener) + element.removeEventListener("dataavailable", responder, false); + else { + element.detachEvent("ondataavailable", responder); + element.detachEvent("onfilterchange", responder); + } + } else { + // Ordinary event. + if (element.removeEventListener) + element.removeEventListener(eventName, responder, false); + else + element.detachEvent('on' + eventName, responder); + } + + registry.set(eventName, responders.without(responder)); return element; } - function fire(element, eventName, memo) { + function fire(element, eventName, memo, bubble) { element = $(element); + + if (Object.isUndefined(bubble)) + bubble = true; + if (element == document && document.createEvent && !element.dispatchEvent) element = document.documentElement; @@ -295,7 +329,7 @@ event.initEvent('dataavailable', true, true); } else { event = document.createEventObject(); - event.eventType = 'ondataavailable'; + event.eventType = bubble ? 'ondataavailable' : 'onfilterchange'; } event.eventName = eventName; @@ -332,7 +366,7 @@ }); // Export to the global scope. - if (window.Event) Object.extend(window.Event, Event) + if (window.Event) Object.extend(window.Event, Event); else window.Event = Event; })(); diff --git a/test/unit/event_test.js b/test/unit/event_test.js index a0ab793..49abde3 100644 --- a/test/unit/event_test.js +++ b/test/unit/event_test.js @@ -141,16 +141,20 @@ new Test.Unit.Runner({ var span = $("span"), observer = function() { }, eventID; span.observe("test:somethingHappened", observer); - eventID = span._prototypeEventID; - this.assert(Event.cache[eventID]); - this.assert(Object.isArray(Event.cache[eventID]["test:somethingHappened"])); - this.assertEqual(1, Event.cache[eventID]["test:somethingHappened"].length); + var registry = span.getStorage().get('prototype_event_registry'); + + this.assert(registry); + this.assert(Object.isArray(registry.get('test:somethingHappened'))); + this.assertEqual(1, registry.get('test:somethingHappened').length); span.stopObserving("test:somethingHappened", observer); - this.assert(Event.cache[eventID]); - this.assert(Object.isArray(Event.cache[eventID]["test:somethingHappened"])); - this.assertEqual(0, Event.cache[eventID]["test:somethingHappened"].length); + + registry = span.getStorage().get('prototype_event_registry'); + + this.assert(registry); + this.assert(Object.isArray(registry.get('test:somethingHappened'))); + this.assertEqual(0, registry.get('test:somethingHappened').length); }, testObserveAndStopObservingAreChainable: function() { From bc899339a9c7ab58f910a58572a889fe5a8ed9d7 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 14 Dec 2008 03:40:15 -0600 Subject: [PATCH 4/4] Remove redundant tests from base_test.js. --- test/unit/base_test.js | 491 +---------------------------------------- 1 file changed, 1 insertion(+), 490 deletions(-) diff --git a/test/unit/base_test.js b/test/unit/base_test.js index c995793..6ab91c7 100644 --- a/test/unit/base_test.js +++ b/test/unit/base_test.js @@ -1,364 +1,4 @@ new Test.Unit.Runner({ - testFunctionArgumentNames: function() { - this.assertEnumEqual([], (function() {}).argumentNames()); - this.assertEnumEqual(["one"], (function(one) {}).argumentNames()); - this.assertEnumEqual(["one", "two", "three"], (function(one, two, three) {}).argumentNames()); - this.assertEnumEqual(["one", "two", "three"], (function( one , two - , three ) {}).argumentNames()); - this.assertEqual("$super", (function($super) {}).argumentNames().first()); - - function named1() {}; - this.assertEnumEqual([], named1.argumentNames()); - function named2(one) {}; - this.assertEnumEqual(["one"], named2.argumentNames()); - function named3(one, two, three) {}; - this.assertEnumEqual(["one", "two", "three"], named3.argumentNames()); - function named4(/*foo*/ foo, /* bar */ bar, /*****/ baz) {} - this.assertEnumEqual($w("foo bar baz"), named4.argumentNames()); - - function named5( - /*foo*/ foo, - /**/bar, - /* baz */ /* baz */ baz, - // Skip a line just to screw with the regex... - /* thud */ thud) {} - this.assertEnumEqual($w("foo bar baz thud"), named5.argumentNames()); - }, - - testFunctionBind: function() { - function methodWithoutArguments() { return this.hi }; - function methodWithArguments() { return this.hi + ',' + $A(arguments).join(',') }; - var func = Prototype.emptyFunction; - - this.assertIdentical(func, func.bind()); - this.assertIdentical(func, func.bind(undefined)); - this.assertNotIdentical(func, func.bind(null)); - - this.assertEqual('without', methodWithoutArguments.bind({ hi: 'without' })()); - this.assertEqual('with,arg1,arg2', methodWithArguments.bind({ hi: 'with' })('arg1','arg2')); - this.assertEqual('withBindArgs,arg1,arg2', - methodWithArguments.bind({ hi: 'withBindArgs' }, 'arg1', 'arg2')()); - this.assertEqual('withBindArgsAndArgs,arg1,arg2,arg3,arg4', - methodWithArguments.bind({ hi: 'withBindArgsAndArgs' }, 'arg1', 'arg2')('arg3', 'arg4')); - }, - - testFunctionCurry: function() { - var split = function(delimiter, string) { return string.split(delimiter); }; - var splitOnColons = split.curry(":"); - this.assertNotIdentical(split, splitOnColons); - this.assertEnumEqual(split(":", "0:1:2:3:4:5"), splitOnColons("0:1:2:3:4:5")); - this.assertIdentical(split, split.curry()); - }, - - testFunctionDelay: function() { - window.delayed = undefined; - var delayedFunction = function() { window.delayed = true; }; - var delayedFunctionWithArgs = function() { window.delayedWithArgs = $A(arguments).join(' '); }; - delayedFunction.delay(0.8); - delayedFunctionWithArgs.delay(0.8, 'hello', 'world'); - this.assertUndefined(window.delayed); - this.wait(1000, function() { - this.assert(window.delayed); - this.assertEqual('hello world', window.delayedWithArgs); - }); - }, - - testFunctionWrap: function() { - function sayHello(){ - return 'hello world'; - }; - - this.assertEqual('HELLO WORLD', sayHello.wrap(function(proceed) { - return proceed().toUpperCase(); - })()); - - var temp = String.prototype.capitalize; - String.prototype.capitalize = String.prototype.capitalize.wrap(function(proceed, eachWord) { - if(eachWord && this.include(' ')) return this.split(' ').map(function(str){ - return str.capitalize(); - }).join(' '); - return proceed(); - }); - this.assertEqual('Hello world', 'hello world'.capitalize()); - this.assertEqual('Hello World', 'hello world'.capitalize(true)); - this.assertEqual('Hello', 'hello'.capitalize()); - String.prototype.capitalize = temp; - }, - - testFunctionDefer: function() { - window.deferred = undefined; - var deferredFunction = function() { window.deferred = true; }; - deferredFunction.defer(); - this.assertUndefined(window.deferred); - this.wait(50, function() { - this.assert(window.deferred); - - window.deferredValue = 0; - var deferredFunction2 = function(arg) { window.deferredValue = arg; }; - deferredFunction2.defer('test'); - this.wait(50, function() { - this.assertEqual('test', window.deferredValue); - }); - }); - }, - - testFunctionMethodize: function() { - var Foo = { bar: function(baz) { return baz } }; - var baz = { quux: Foo.bar.methodize() }; - - this.assertEqual(Foo.bar.methodize(), baz.quux); - this.assertEqual(baz, Foo.bar(baz)); - this.assertEqual(baz, baz.quux()); - }, - - testObjectExtend: function() { - var object = {foo: 'foo', bar: [1, 2, 3]}; - this.assertIdentical(object, Object.extend(object)); - this.assertHashEqual({foo: 'foo', bar: [1, 2, 3]}, object); - this.assertIdentical(object, Object.extend(object, {bla: 123})); - this.assertHashEqual({foo: 'foo', bar: [1, 2, 3], bla: 123}, object); - this.assertHashEqual({foo: 'foo', bar: [1, 2, 3], bla: null}, - Object.extend(object, {bla: null})); - }, - - testObjectToQueryString: function() { - this.assertEqual('a=A&b=B&c=C&d=D%23', Object.toQueryString({a: 'A', b: 'B', c: 'C', d: 'D#'})); - }, - - testObjectClone: function() { - var object = {foo: 'foo', bar: [1, 2, 3]}; - this.assertNotIdentical(object, Object.clone(object)); - this.assertHashEqual(object, Object.clone(object)); - this.assertHashEqual({}, Object.clone()); - var clone = Object.clone(object); - delete clone.bar; - this.assertHashEqual({foo: 'foo'}, clone, - "Optimizing Object.clone perf using prototyping doesn't allow properties to be deleted."); - }, - - testObjectInspect: function() { - this.assertEqual('undefined', Object.inspect()); - this.assertEqual('undefined', Object.inspect(undefined)); - this.assertEqual('null', Object.inspect(null)); - this.assertEqual("'foo\\\\b\\\'ar'", Object.inspect('foo\\b\'ar')); - this.assertEqual('[]', Object.inspect([])); - this.assertNothingRaised(function() { Object.inspect(window.Node) }); - }, - - testObjectToJSON: function() { - this.assertUndefined(Object.toJSON(undefined)); - this.assertUndefined(Object.toJSON(Prototype.K)); - this.assertEqual('\"\"', Object.toJSON('')); - this.assertEqual('[]', Object.toJSON([])); - this.assertEqual('[\"a\"]', Object.toJSON(['a'])); - this.assertEqual('[\"a\", 1]', Object.toJSON(['a', 1])); - this.assertEqual('[\"a\", {\"b\": null}]', Object.toJSON(['a', {'b': null}])); - this.assertEqual('{\"a\": \"hello!\"}', Object.toJSON({a: 'hello!'})); - this.assertEqual('{}', Object.toJSON({})); - this.assertEqual('{}', Object.toJSON({a: undefined, b: undefined, c: Prototype.K})); - this.assertEqual('{\"b\": [false, true], \"c\": {\"a\": \"hello!\"}}', - Object.toJSON({'b': [undefined, false, true, undefined], c: {a: 'hello!'}})); - this.assertEqual('{\"b\": [false, true], \"c\": {\"a\": \"hello!\"}}', - Object.toJSON($H({'b': [undefined, false, true, undefined], c: {a: 'hello!'}}))); - this.assertEqual('true', Object.toJSON(true)); - this.assertEqual('false', Object.toJSON(false)); - this.assertEqual('null', Object.toJSON(null)); - var sam = new Person('sam'); - this.assertEqual('-sam', Object.toJSON(sam)); - this.assertEqual('-sam', sam.toJSON()); - var element = $('test'); - this.assertUndefined(Object.toJSON(element)); - element.toJSON = function(){return 'I\'m a div with id test'}; - this.assertEqual('I\'m a div with id test', Object.toJSON(element)); - }, - - testObjectToHTML: function() { - this.assertIdentical('', Object.toHTML()); - this.assertIdentical('', Object.toHTML('')); - this.assertIdentical('', Object.toHTML(null)); - this.assertIdentical('0', Object.toHTML(0)); - this.assertIdentical('123', Object.toHTML(123)); - this.assertEqual('hello world', Object.toHTML('hello world')); - this.assertEqual('hello world', Object.toHTML({toHTML: function() { return 'hello world' }})); - }, - - testObjectIsArray: function() { - this.assert(Object.isArray([])); - this.assert(Object.isArray([0])); - this.assert(Object.isArray([0, 1])); - this.assert(!Object.isArray({})); - this.assert(!Object.isArray($('list').childNodes)); - this.assert(!Object.isArray()); - this.assert(!Object.isArray('')); - this.assert(!Object.isArray('foo')); - this.assert(!Object.isArray(0)); - this.assert(!Object.isArray(1)); - this.assert(!Object.isArray(null)); - this.assert(!Object.isArray(true)); - this.assert(!Object.isArray(false)); - this.assert(!Object.isArray(undefined)); - }, - - testObjectIsHash: function() { - this.assert(Object.isHash($H())); - this.assert(Object.isHash(new Hash())); - this.assert(!Object.isHash({})); - this.assert(!Object.isHash(null)); - this.assert(!Object.isHash()); - this.assert(!Object.isHash('')); - this.assert(!Object.isHash(2)); - this.assert(!Object.isHash(false)); - this.assert(!Object.isHash(true)); - this.assert(!Object.isHash([])); - }, - - testObjectIsElement: function() { - this.assert(Object.isElement(document.createElement('div'))); - this.assert(Object.isElement(new Element('div'))); - this.assert(Object.isElement($('testlog'))); - this.assert(!Object.isElement(document.createTextNode('bla'))); - - // falsy variables should not mess up return value type - this.assertIdentical(false, Object.isElement(0)); - this.assertIdentical(false, Object.isElement('')); - this.assertIdentical(false, Object.isElement(NaN)); - this.assertIdentical(false, Object.isElement(null)); - this.assertIdentical(false, Object.isElement(undefined)); - }, - - testObjectIsFunction: function() { - this.assert(Object.isFunction(function() { })); - this.assert(Object.isFunction(Class.create())); - this.assert(!Object.isFunction("a string")); - this.assert(!Object.isFunction($("testlog"))); - this.assert(!Object.isFunction([])); - this.assert(!Object.isFunction({})); - this.assert(!Object.isFunction(0)); - this.assert(!Object.isFunction(false)); - this.assert(!Object.isFunction(undefined)); - }, - - testObjectIsString: function() { - this.assert(!Object.isString(function() { })); - this.assert(Object.isString("a string")); - this.assert(!Object.isString(0)); - this.assert(!Object.isString([])); - this.assert(!Object.isString({})); - this.assert(!Object.isString(false)); - this.assert(!Object.isString(undefined)); - }, - - testObjectIsNumber: function() { - this.assert(Object.isNumber(0)); - this.assert(Object.isNumber(1.0)); - this.assert(!Object.isNumber(function() { })); - this.assert(!Object.isNumber("a string")); - this.assert(!Object.isNumber([])); - this.assert(!Object.isNumber({})); - this.assert(!Object.isNumber(false)); - this.assert(!Object.isNumber(undefined)); - }, - - testObjectIsUndefined: function() { - this.assert(Object.isUndefined(undefined)); - this.assert(!Object.isUndefined(null)); - this.assert(!Object.isUndefined(false)); - this.assert(!Object.isUndefined(0)); - this.assert(!Object.isUndefined("")); - this.assert(!Object.isUndefined(function() { })); - this.assert(!Object.isUndefined([])); - this.assert(!Object.isUndefined({})); - }, - - // sanity check - testDoesntExtendObjectPrototype: function() { - // for-in is supported with objects - var iterations = 0, obj = { a: 1, b: 2, c: 3 }; - for(property in obj) iterations++; - this.assertEqual(3, iterations); - - // for-in is not supported with arrays - iterations = 0; - var arr = [1,2,3]; - for(property in arr) iterations++; - this.assert(iterations > 3); - }, - - testPeriodicalExecuterStop: function() { - var peEventCount = 0; - function peEventFired(pe) { - if (++peEventCount > 2) pe.stop(); - } - - // peEventFired will stop the PeriodicalExecuter after 3 callbacks - new PeriodicalExecuter(peEventFired, 0.05); - - this.wait(600, function() { - this.assertEqual(3, peEventCount); - }); - }, - - testBindAsEventListener: function() { - for( var i = 0; i < 10; ++i ){ - var div = document.createElement('div'); - div.setAttribute('id','test-'+i); - document.body.appendChild(div); - var tobj = new TestObj(); - var eventTest = { test: true }; - var call = tobj.assertingEventHandler.bindAsEventListener(tobj, - this.assertEqual.bind(this, eventTest), - this.assertEqual.bind(this, arg1), - this.assertEqual.bind(this, arg2), - this.assertEqual.bind(this, arg3), arg1, arg2, arg3 ); - call(eventTest); - } - }, - - testDateToJSON: function() { - this.assertEqual('\"1970-01-01T00:00:00Z\"', new Date(Date.UTC(1970, 0, 1)).toJSON()); - }, - - testRegExpEscape: function() { - this.assertEqual('word', RegExp.escape('word')); - this.assertEqual('\\/slashes\\/', RegExp.escape('/slashes/')); - this.assertEqual('\\\\backslashes\\\\', RegExp.escape('\\backslashes\\')); - this.assertEqual('\\\\border of word', RegExp.escape('\\border of word')); - - this.assertEqual('\\(\\?\\:non-capturing\\)', RegExp.escape('(?:non-capturing)')); - this.assertEqual('non-capturing', new RegExp(RegExp.escape('(?:') + '([^)]+)').exec('(?:non-capturing)')[1]); - - this.assertEqual('\\(\\?\\=positive-lookahead\\)', RegExp.escape('(?=positive-lookahead)')); - this.assertEqual('positive-lookahead', new RegExp(RegExp.escape('(?=') + '([^)]+)').exec('(?=positive-lookahead)')[1]); - - this.assertEqual('\\(\\?<\\=positive-lookbehind\\)', RegExp.escape('(?<=positive-lookbehind)')); - this.assertEqual('positive-lookbehind', new RegExp(RegExp.escape('(?<=') + '([^)]+)').exec('(?<=positive-lookbehind)')[1]); - - this.assertEqual('\\(\\?\\!negative-lookahead\\)', RegExp.escape('(?!negative-lookahead)')); - this.assertEqual('negative-lookahead', new RegExp(RegExp.escape('(?!') + '([^)]+)').exec('(?!negative-lookahead)')[1]); - - this.assertEqual('\\(\\?<\\!negative-lookbehind\\)', RegExp.escape('(?', new RegExp(RegExp.escape('
')).exec('
')[0]); - - this.assertEqual('false', RegExp.escape(false)); - this.assertEqual('undefined', RegExp.escape()); - this.assertEqual('null', RegExp.escape(null)); - this.assertEqual('42', RegExp.escape(42)); - - this.assertEqual('\\\\n\\\\r\\\\t', RegExp.escape('\\n\\r\\t')); - this.assertEqual('\n\r\t', RegExp.escape('\n\r\t')); - this.assertEqual('\\{5,2\\}', RegExp.escape('{5,2}')); - - this.assertEqual( - '\\/\\(\\[\\.\\*\\+\\?\\^\\=\\!\\:\\$\\{\\}\\(\\)\\|\\[\\\\\\]\\\\\\\/\\\\\\\\\\]\\)\\/g', - RegExp.escape('/([.*+?^=!:${}()|[\\]\\/\\\\])/g') - ); - }, - testBrowserDetection: function() { var results = $H(Prototype.Browser).map(function(engine){ return engine; @@ -399,134 +39,5 @@ new Test.Unit.Runner({ this.info('Running on Gecko'); this.assert(Prototype.Browser.Gecko); } - }, - - testClassCreate: function() { - this.assert(Object.isFunction(Animal), 'Animal is not a constructor'); - this.assertEnumEqual([Cat, Mouse, Dog, Ox], Animal.subclasses); - Animal.subclasses.each(function(subclass) { - this.assertEqual(Animal, subclass.superclass); - }, this); - - var Bird = Class.create(Animal); - this.assertEqual(Bird, Animal.subclasses.last()); - // for..in loop (for some reason) doesn't iterate over the constructor property in top-level classes - this.assertEnumEqual(Object.keys(new Animal).sort(), Object.keys(new Bird).without('constructor').sort()); - }, - - testClassInstantiation: function() { - var pet = new Animal("Nibbles"); - this.assertEqual("Nibbles", pet.name, "property not initialized"); - this.assertEqual('Nibbles: Hi!', pet.say('Hi!')); - this.assertEqual(Animal, pet.constructor, "bad constructor reference"); - this.assertUndefined(pet.superclass); - - var Empty = Class.create(); - this.assert('object', typeof new Empty); - }, - - testInheritance: function() { - var tom = new Cat('Tom'); - this.assertEqual(Cat, tom.constructor, "bad constructor reference"); - this.assertEqual(Animal, tom.constructor.superclass, 'bad superclass reference'); - this.assertEqual('Tom', tom.name); - this.assertEqual('Tom: meow', tom.say('meow')); - this.assertEqual('Tom: Yuk! I only eat mice.', tom.eat(new Animal)); - }, - - testSuperclassMethodCall: function() { - var tom = new Cat('Tom'); - this.assertEqual('Tom: Yum!', tom.eat(new Mouse)); - - // augment the constructor and test - var Dodo = Class.create(Animal, { - initialize: function($super, name) { - $super(name); - this.extinct = true; - }, - - say: function($super, message) { - return $super(message) + " honk honk"; - } - }); - - var gonzo = new Dodo('Gonzo'); - this.assertEqual('Gonzo', gonzo.name); - this.assert(gonzo.extinct, 'Dodo birds should be extinct'); - this.assertEqual("Gonzo: hello honk honk", gonzo.say("hello")); - }, - - testClassAddMethods: function() { - var tom = new Cat('Tom'); - var jerry = new Mouse('Jerry'); - - Animal.addMethods({ - sleep: function() { - return this.say('ZZZ'); - } - }); - - Mouse.addMethods({ - sleep: function($super) { - return $super() + " ... no, can't sleep! Gotta steal cheese!"; - }, - escape: function(cat) { - return this.say('(from a mousehole) Take that, ' + cat.name + '!'); - } - }); - - this.assertEqual('Tom: ZZZ', tom.sleep(), "added instance method not available to subclass"); - this.assertEqual("Jerry: ZZZ ... no, can't sleep! Gotta steal cheese!", jerry.sleep()); - this.assertEqual("Jerry: (from a mousehole) Take that, Tom!", jerry.escape(tom)); - // insure that a method has not propagated *up* the prototype chain: - this.assertUndefined(tom.escape); - this.assertUndefined(new Animal().escape); - - Animal.addMethods({ - sleep: function() { - return this.say('zZzZ'); - } - }); - - this.assertEqual("Jerry: zZzZ ... no, can't sleep! Gotta steal cheese!", jerry.sleep()); - }, - - testBaseClassWithMixin: function() { - var grass = new Plant('grass', 3); - this.assertRespondsTo('getValue', grass); - this.assertEqual('#', grass.inspect()); - }, - - testSubclassWithMixin: function() { - var snoopy = new Dog('Snoopy', 12, 'male'); - this.assertRespondsTo('reproduce', snoopy); - }, - - testSubclassWithMixins: function() { - var cow = new Ox('cow', 400, 'female'); - this.assertEqual('#', cow.inspect()); - this.assertRespondsTo('reproduce', cow); - this.assertRespondsTo('getValue', cow); - }, - - testClassWithToStringAndValueOfMethods: function() { - var Foo = Class.create({ - toString: function() { return "toString" }, - valueOf: function() { return "valueOf" } - }); - - var Parent = Class.create({ - m1: function(){ return 'm1' }, - m2: function(){ return 'm2' } - }); - var Child = Class.create(Parent, { - m1: function($super) { return 'm1 child' }, - m2: function($super) { return 'm2 child' } - }); - - this.assert(new Child().m1.toString().indexOf('m1 child') > -1); - - this.assertEqual("toString", new Foo().toString()); - this.assertEqual("valueOf", new Foo().valueOf()); - } + } }); \ No newline at end of file