From f970512f7acdebbed75d7958d951dc0088881999 Mon Sep 17 00:00:00 2001 From: MaxArt2501 Date: Wed, 11 Feb 2015 13:38:44 +0100 Subject: [PATCH] Functions now observable; more tests; closes #1 --- changelog.md | 8 + dist/object-observe-lite.js | 50 +++-- dist/object-observe-lite.min.js | 18 +- dist/object-observe.js | 50 +++-- dist/object-observe.min.js | 24 +-- doc/index.md | 4 +- readme.md | 20 +- test/test.css | 30 --- test/tests.js | 317 ++++++++++++++++++++++++++++---- 9 files changed, 393 insertions(+), 128 deletions(-) delete mode 100644 test/test.css diff --git a/changelog.md b/changelog.md index ef0dba9..d664bb4 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,11 @@ +## v0.2.2 + +2015-02-11 + +* Uses `Object.getOwnPropertyNames` instead of `Object.keys` (issue #1) +* Functions can now be observed +* More tests + ## v0.2.1 2015-01-28 diff --git a/dist/object-observe-lite.js b/dist/object-observe-lite.js index 53f412f..87a740d 100644 --- a/dist/object-observe-lite.js +++ b/dist/object-observe-lite.js @@ -1,5 +1,5 @@ /*! - * Object.observe "lite" polyfill - v0.2.1 + * Object.observe "lite" polyfill - v0.2.2 * by Massimo Artizzu (MaxArt2501) * * https://github.com/MaxArt2501/object-observe @@ -164,18 +164,42 @@ Object.observe || (function(O, A, root) { } : function() { return new Map(); }, /** - * Simple shim for Object.keys is not available - * Misses checks on object, don't use as a replacement of Object.keys - * @function getKeys + * Simple shim for Object.getOwnPropertyNames when is not available + * Misses checks on object, don't use as a replacement of Object.keys/getOwnPropertyNames + * @function getProps * @param {Object} object * @returns {String[]} */ - getKeys = O.keys || function(object) { - var keys = [], prop; + getProps = O.getOwnPropertyNames ? (function() { + var func = O.getOwnPropertyNames; + try { + arguments.callee; + } catch (e) { + // Strict mode is supported + + // In strict mode, we can't access to "arguments", "caller" and + // "callee" properties of functions. Object.getOwnPropertyNames + // returns [ "prototype", "length", "name" ] in Firefox; it returns + // "caller" and "arguments" too in Chrome and in Internet + // Explorer, so those values must be filtered. + var avoid = (func(inArray).join(" ") + " ").replace(/prototype |length |name /g, "").slice(0, -1).split(" "); + if (avoid.length) func = function(object) { + var props = O.getOwnPropertyNames(object); + if (typeof object === "function") + for (var i = 0, j; i < avoid.length;) + if ((j = inArray(avoid[i++], props)) > -1) + props.splice(j, 1); + + return props; + }; + } + return func; + })() : function(object) { + var props = [], prop; for (prop in object) if (object.hasOwnProperty(prop)) - keys.push(prop); - return keys; + props.push(prop); + return props; }, /** @@ -225,7 +249,7 @@ Object.observe || (function(O, A, root) { * @param {Object} object */ createObjectData = function(object, data) { - var props = isNode(object) ? [] : getKeys(object), + var props = isNode(object) ? [] : getProps(object), values = [], descs, i = 0, data = { handlers: createMap(), @@ -262,7 +286,7 @@ Object.observe || (function(O, A, root) { props = data.properties.slice(); proplen = props.length; - keys = getKeys(object); + keys = getProps(object); // Check for value additions/changes while (i < keys.length) { @@ -460,7 +484,7 @@ Object.observe || (function(O, A, root) { * @returns {Object} The observed object */ O.observe = function observe(object, handler, acceptList) { - if (object === null || typeof object !== "object") + if (object === null || typeof object !== "object" && typeof object !== "function") throw new TypeError("Object.observe cannot observe non-object"); if (typeof handler !== "function") @@ -486,7 +510,7 @@ Object.observe || (function(O, A, root) { * @returns {Object} The given object */ O.unobserve = function unobserve(object, handler) { - if (object === null || typeof object !== "object") + if (object === null || typeof object !== "object" && typeof object !== "function") throw new TypeError("Object.unobserve cannot unobserve non-object"); if (typeof handler !== "function") @@ -524,7 +548,7 @@ Object.observe || (function(O, A, root) { * @returns {Notifier} */ O.getNotifier = function getNotifier(object) { - if (object === null || typeof object !== "object") + if (object === null || typeof object !== "object" && typeof object !== "function") throw new TypeError("Object.getNotifier cannot getNotifier non-object"); if (O.isFrozen && O.isFrozen(object)) return null; diff --git a/dist/object-observe-lite.min.js b/dist/object-observe-lite.min.js index f801d46..50f31d2 100644 --- a/dist/object-observe-lite.min.js +++ b/dist/object-observe-lite.min.js @@ -1,9 +1,9 @@ -Object.observe||function(d,x,p){var h,g,C="add update delete reconfigure setPrototype preventExtensions".split(" "),D=p.Node?function(a){return a&&a instanceof p.Node}:function(a){return a&&"object"===typeof a&&"number"===typeof a.nodeType&&"string"===typeof a.nodeName},E=x.isArray||function(a){return function(b){return"[object Array]"===a.call(b)}}(d.prototype.toString),l=x.prototype.indexOf?function(a,b,c){return b.indexOf(a,c)}:function(a,b,c){for(c=c||0;carguments.length&&(b=h.get(a));return b&&b.notifier||{notify:function(c){c.type;var b=h.get(a);if(b){var f={object:a},k;for(k in c)"object"!==k&&(f[k]=c[k]);q(a,b,f)}},performChange:function(c, -b,f){if("string"!==typeof c)throw new TypeError("Invalid non-string changeType");if("function"!==typeof b)throw new TypeError("Cannot perform non-function");var k=h.get(a),d;b=b.call(f);k&&t(k,a,c);if(k&&b&&"object"===typeof b){c={object:a,type:c};for(d in b)"object"!==d&&"type"!==d&&(c[d]=b[d]);q(a,k,c)}}}},B=function(a,b,c,e){var f=g.get(c);f||g.set(c,f={observed:r(),changeRecords:[]});f.observed.set(a,{acceptList:e.slice(),data:b});b.handlers.set(c,f)},q=function(a,b,c,e){b.handlers.forEach(function(b){var d= -b.observed.get(a).acceptList;("string"!==typeof e||-1===l(e,d))&&-1arguments.length&&(b=k.get(a));return b&&b.notifier||{notify:function(c){c.type;var b=k.get(a);if(b){var e={object:a},g;for(g in c)"object"!==g&&(e[g]=c[g]);q(a,b,e)}},performChange:function(c,b,e){if("string"!==typeof c)throw new TypeError("Invalid non-string changeType");if("function"!==typeof b)throw new TypeError("Cannot perform non-function");var g=k.get(a),d;b=b.call(e);g&& +t(g,a,c);if(g&&b&&"object"===typeof b){c={object:a,type:c};for(d in b)"object"!==d&&"type"!==d&&(c[d]=b[d]);q(a,g,c)}}}},B=function(a,b,c,f){var e=h.get(c);e||h.set(c,e={observed:r(),changeRecords:[]});e.observed.set(a,{acceptList:f.slice(),data:b});b.handlers.set(c,e)},q=function(a,b,c,f){b.handlers.forEach(function(b){var d=b.observed.get(a).acceptList;("string"!==typeof f||-1===l(f,d))&&-1 -1) + props.splice(j, 1); + + return props; + }; + } + return func; + })() : function(object) { + var props = [], prop; for (prop in object) if (object.hasOwnProperty(prop)) - keys.push(prop); - return keys; + props.push(prop); + return props; }, /** @@ -248,7 +272,7 @@ Object.observe || (function(O, A, root) { * @param {Object} object */ createObjectData = function(object, data) { - var props = isNode(object) ? [] : getKeys(object), + var props = isNode(object) ? [] : getProps(object), values = [], descs, i = 0, data = { handlers: createMap(), @@ -387,7 +411,7 @@ Object.observe || (function(O, A, root) { props = data.properties.slice(); proplen = props.length; - keys = getKeys(object); + keys = getProps(object); if (descs) { while (i < keys.length) { @@ -623,7 +647,7 @@ Object.observe || (function(O, A, root) { * @returns {Object} The observed object */ O.observe = function observe(object, handler, acceptList) { - if (object === null || typeof object !== "object") + if (object === null || typeof object !== "object" && typeof object !== "function") throw new TypeError("Object.observe cannot observe non-object"); if (typeof handler !== "function") @@ -649,7 +673,7 @@ Object.observe || (function(O, A, root) { * @returns {Object} The given object */ O.unobserve = function unobserve(object, handler) { - if (object === null || typeof object !== "object") + if (object === null || typeof object !== "object" && typeof object !== "function") throw new TypeError("Object.unobserve cannot unobserve non-object"); if (typeof handler !== "function") @@ -687,7 +711,7 @@ Object.observe || (function(O, A, root) { * @returns {Notifier} */ O.getNotifier = function getNotifier(object) { - if (object === null || typeof object !== "object") + if (object === null || typeof object !== "object" && typeof object !== "function") throw new TypeError("Object.getNotifier cannot getNotifier non-object"); if (O.isFrozen && O.isFrozen(object)) return null; diff --git a/dist/object-observe.min.js b/dist/object-observe.min.js index be3ac0b..ab78941 100644 --- a/dist/object-observe.min.js +++ b/dist/object-observe.min.js @@ -1,12 +1,12 @@ -Object.observe||function(d,C,w){var k,q,H="add update delete reconfigure setPrototype preventExtensions".split(" "),I=w.Node?function(b){return b&&b instanceof w.Node}:function(b){return b&&"object"===typeof b&&"number"===typeof b.nodeType&&"string"===typeof b.nodeName},J=C.isArray||function(b){return function(c){return"[object Array]"===b.call(c)}}(d.prototype.toString),t=C.prototype.indexOf?function(b,c,a){return c.indexOf(b,a)}:function(b,c,a){for(a=a||0;aarguments.length&&(c=k.get(b));return c&&c.notifier||{notify:function(a){a.type;var e=k.get(b);if(e){var c={object:b},d;for(d in a)"object"!==d&&(c[d]=a[d]);r(b,e,c)}},performChange:function(a,c,f){if("string"!==typeof a)throw new TypeError("Invalid non-string changeType");if("function"!==typeof c)throw new TypeError("Cannot perform non-function"); -var d=k.get(b),h;c=c.call(f);d&&z(d,b,a);if(d&&c&&"object"===typeof c){a={object:b,type:a};for(h in c)"object"!==h&&"type"!==h&&(a[h]=c[h]);r(b,d,a)}}}},G=function(b,c,a,e){var d=q.get(a);d||q.set(a,d={observed:x(),changeRecords:[]});d.observed.set(b,{acceptList:e.slice(),data:c});c.handlers.set(a,d)},r=function(b,c,a,d){c.handlers.forEach(function(c){var l=c.observed.get(b).acceptList;("string"!==typeof d||-1===t(d,l))&&-1arguments.length&&(c=k.get(a)); +return c&&c.notifier||{notify:function(b){b.type;var c=k.get(a);if(c){var e={object:a},h;for(h in b)"object"!==h&&(e[h]=b[h]);r(a,c,e)}},performChange:function(b,c,e){if("string"!==typeof b)throw new TypeError("Invalid non-string changeType");if("function"!==typeof c)throw new TypeError("Cannot perform non-function");var h=k.get(a),f;c=c.call(e);h&&z(h,a,b);if(h&&c&&"object"===typeof c){b={object:a,type:b};for(f in c)"object"!==f&&"type"!==f&&(b[f]=c[f]);r(a,h,b)}}}},G=function(a,c,b,d){var e=l.get(b); +e||l.set(b,e={observed:x(),changeRecords:[]});e.observed.set(a,{acceptList:d.slice(),data:c});c.handlers.set(b,e)},r=function(a,c,b,d){c.handlers.forEach(function(c){var f=c.observed.get(a).acceptList;("string"!==typeof d||-1===t(d,f))&&-1 .outcome-normal { - color: gray; - color: rgba(0, 0, 0, .4); - font-style: italic; - border-bottom: 1px solid; - margin-bottom: .25em; -} -.outcome-success, .outcome-fail { - font-family: "Courier New", Courier, Monospace; -} \ No newline at end of file diff --git a/test/tests.js b/test/tests.js index 731bdd2..e7ca18f 100644 --- a/test/tests.js +++ b/test/tests.js @@ -80,6 +80,143 @@ describe("Object.observe", function() { }, 30); }); + it("should observe plain objects", function(done) { + function handler(changes) { + try { + expect(tested).to.be(false); + expect(changes).to.have.length(1); + tested = true; + } catch (e) { done(e); } + } + + var obj = {}, tested = false; + Object.observe(obj, handler); + + obj.foo = 42; + + Object.unobserve(obj, handler); + + setTimeout(function() { + try { + expect(tested).to.be(true); + done(); + } catch (e) { done(e); } + }, 30); + }); + + it("should observe arrays", function(done) { + function handler(changes) { + try { + expect(tested).to.be(false); + expect(changes).to.have.length(2) + .and.to.looselyContain({ type: "add", name: "0", object: array }) + .and.to.looselyContain({ type: "update", name: "length", object: array, oldValue: 0 }); + tested = true; + } catch (e) { done(e); } + } + + var array = [], tested = false; + Object.observe(array, handler); + + array.push(42); + + Object.unobserve(array, handler); + + setTimeout(function() { + try { + expect(tested).to.be(true); + done(); + } catch (e) { done(e); } + }, 30); + }); + + it("should observe functions", function(done) { + function handler(changes) { + try { + expect(tested).to.be(false); + expect(changes).to.have.length(1) + expect(changes[0]).to.eql({ type: "add", name: "foo", object: observeMe }); + tested = true; + } catch (e) { done(e); } + } + function observeMe() {} + + var tested = false; + Object.observe(observeMe, handler); + + observeMe.foo = "bar"; + + Object.unobserve(observeMe, handler); + + setTimeout(function() { + try { + expect(tested).to.be(true); + done(); + } catch (e) { done(e); } + }, 30); + }); + + it("should observe all kinds of objects, including instances of user defined classes", function(done) { + function handler(changes) { + try { + expect(tested).to.be(false); + expect(changes).to.have.length(3) + .and.to.looselyContain({ type: "update", name: "message", object: error, oldValue: "Message" }) + .and.to.looselyContain({ type: "add", name: "TAU", object: Math }) + .and.to.looselyContain({ type: "add", name: "foo", object: inst }); + tested = true; + } catch (e) { done(e); } + } + + function Class() {}; + + var error = new Error("Message"), + inst = new Class(), + tested = false; + + Object.observe(error, handler); + Object.observe(Math, handler); + Object.observe(inst, handler); + + error.message = "New message"; + Math.TAU = Math.PI * 2; + inst.foo = "bar"; + + Object.unobserve(error, handler); + Object.unobserve(Math, handler); + Object.unobserve(inst, handler); + + delete Math.TAU; + + setTimeout(function() { + try { + expect(tested).to.be(true); + done(); + } catch (e) { done(e); } + }, 30); + }); + + it("should not observe any non-object", function() { + expect(Object.observe).to.throwError(); + expect(Object.observe).withArgs(undefined, function() {}).to.throwError(); + expect(Object.observe).withArgs(null, function() {}).to.throwError(); + expect(Object.observe).withArgs(1, function() {}).to.throwError(); + expect(Object.observe).withArgs("foo", function() {}).to.throwError(); + expect(Object.observe).withArgs(true, function() {}).to.throwError(); + }); + + it("should not observe when not given a handler function", function() { + expect(Object.observe).withArgs({}).to.throwError(); + expect(Object.observe).withArgs({}, "foo").to.throwError(); + expect(Object.observe).withArgs({}, 42).to.throwError(); + }); + + if (Object.freeze) it("should not observe with a frozen handler function", function() { + function handler() {} + Object.freeze(handler); + expect(Object.observe).withArgs({}, handler).to.throwError(); + }); + it("should deliver changes asynchronously", function(done) { function handler(changes) { try { @@ -303,52 +440,99 @@ describe("Object.unobserve", function() { } catch (e) { done(e); } }, 30); }); -}); -describe("Object.deliverChangeRecords", function() { - it("should deliver changes synchronously", function(done) { - function updateHandler(changes) { + it("should unobserve one handler at time", function(done) { + function handler1(changes) { try { - expect(updated).to.be(false); - expect(added).to.be(true); + expect(tested).to.be(false); expect(changes).to.have.length(1); - expect(changes[0]).to.eql({ type: "update", name: "foo", object: obj, oldValue: 0 }); - - updated = true; + expect(changes[0]).to.eql({ type: "add", name: "foo", object: obj }); + tested = true; } catch (e) { done(e); } } - function addHandler(changes) { - try { - expect(updated).to.be(false); - expect(added).to.be(false); - expect(changes).to.have.length(1); - expect(changes[0]).to.eql({ type: "add", name: "bar", object: obj }); - - added = true; - } catch (e) { done(e); } + function handler2(changes) { + done(new Error("This shouldn't have be called")); } + var obj = { foo: "bar" }, + tested = false; + Object.observe(obj, handler1, [ "add" ]); + Object.observe(obj, handler2, [ "update" ]); - var obj = { foo: 0 }, - updated = false, added = false; - Object.observe(obj, updateHandler, [ "update" ]); - Object.observe(obj, addHandler, [ "add" ]); + delete obj.foo; - obj.foo = 42; - obj.bar = "Hi"; - Object.deliverChangeRecords(addHandler); + Object.unobserve(obj, handler2); + + obj.foo = "bar"; - Object.unobserve(obj, updateHandler); - Object.unobserve(obj, addHandler); + Object.unobserve(obj, handler1); setTimeout(function() { try { - expect(updated).to.be(true); - expect(added).to.be(true); + expect(tested).to.be(true); done(); } catch (e) { done(e); } }, 30); }); + it("should not throw errors on any non-observed object", function() { + Object.unobserve({}, function() {}); + }); + + it("should not throw errors on a not previously used handler", function() { + var obj = {}, + handler = function() {}; + Object.observe(obj, handler); + Object.unobserve(obj, function() {}); + Object.unobserve(obj, handler); + }); + + it("should not unobserve any non-object", function() { + expect(Object.unobserve).withArgs(null, function() {}).to.throwError(); + expect(Object.unobserve).withArgs(undefined, function() {}).to.throwError(); + expect(Object.unobserve).withArgs(42, function() {}).to.throwError(); + expect(Object.unobserve).withArgs("foo", function() {}).to.throwError(); + expect(Object.unobserve).withArgs(NaN, function() {}).to.throwError(); + expect(Object.unobserve).withArgs(true, function() {}).to.throwError(); + }); + + it("should not unobserve when not given a handler function", function() { + expect(Object.unobserve).withArgs({}).to.throwError(); + expect(Object.unobserve).withArgs({}, "foo").to.throwError(); + expect(Object.unobserve).withArgs({}, 42).to.throwError(); + }); +}); + +describe("Object.deliverChangeRecords", function() { + it("should not deliver to non-functions", function() { + expect(Object.deliverChangeRecords).to.throwError(); + expect(Object.deliverChangeRecords).withArgs(undefined).to.throwError(); + expect(Object.deliverChangeRecords).withArgs(null).to.throwError(); + expect(Object.deliverChangeRecords).withArgs(1).to.throwError(); + expect(Object.deliverChangeRecords).withArgs("foo").to.throwError(); + expect(Object.deliverChangeRecords).withArgs(true).to.throwError(); + }); + + it("should deliver changes synchronously", function(done) { + function handler(changes) { + try { + expect(tested).to.be(false); + expect(changes).to.have.length(1); + expect(changes[0]).to.eql({ type: "add", name: "foo", object: obj }); + tested = true; + } catch (e) { done(e); } + } + + var obj = {}, tested = false; + Object.observe(obj, handler); + + obj.foo = 42; + Object.deliverChangeRecords(handler); + expect(tested).to.be(true); + + Object.unobserve(obj, handler); + done(); + }); + it("should deliver changes to an observer for multiple objects", function(done) { function handler(changes) { try { @@ -382,9 +566,27 @@ describe("Object.deliverChangeRecords", function() { }); describe("Object.getNotifier", function() { - it("should deliver custom notifications", function(done) { + it("should provide a notifier for objects", function() { + var obj = { }, + notifier = Object.getNotifier(obj); + expect(notifier).to.be.an("object"); + expect(notifier.notify).to.be.a("function"); + expect(notifier.performChange).to.be.a("function"); + }); + + it("should not provide a notifier for non-objects", function() { + expect(Object.getNotifier).to.throwError(); + expect(Object.getNotifier).withArgs(undefined).to.throwError(); + expect(Object.getNotifier).withArgs(null).to.throwError(); + expect(Object.getNotifier).withArgs(1).to.throwError(); + expect(Object.getNotifier).withArgs("foo").to.throwError(); + expect(Object.getNotifier).withArgs(true).to.throwError(); + }); + + it("should deliver custom notifications asynchronously", function(done) { function handler(changes) { try { + expect(async).to.be(true); expect(tested).to.be(false); expect(changes).to.have.length(1) expect(changes[0]).to.eql({ type: "test", object: obj, message: "Hello" }); @@ -392,17 +594,13 @@ describe("Object.getNotifier", function() { } catch (e) { done(e); } } - var obj = { }, - tested = false, - notifier; - Object.observe(obj, handler, [ "test" ]); + var obj = {}, + tested = false, async = false; - notifier = Object.getNotifier(obj); - expect(notifier).to.be.an("object"); - expect(notifier.notify).to.be.a("function"); - expect(notifier.performChange).to.be.a("function"); + Object.observe(obj, handler, [ "test" ]); - notifier.notify({ type: "test", message: "Hello" }); + Object.getNotifier(obj).notify({ type: "test", message: "Hello" }); + async = true; Object.unobserve(obj, handler); @@ -474,6 +672,47 @@ describe("Object.getNotifier", function() { } catch (e) { done(e); } }, 30); }); + + it("should perform custom changes synchronously", function() { + var obj = {}; + + Object.getNotifier(obj).performChange("test", function() { + obj.foo = "bar"; + }); + + expect(obj.foo).to.be("bar"); + }); + + it("should deliver notifications asynchronously after performing custom changes", function(done) { + function handler(changes) { + try { + expect(async).to.be(true); + expect(tested).to.be(false); + expect(changes).to.have.length(1); + expect(changes[0]).to.eql({ type: "test", object: obj }); + tested = true; + } catch (e) { done(e); } + } + + var obj = { foo: 0 }, + tested = false, async = false; + Object.observe(obj, handler, [ "test" ]); + + Object.getNotifier(obj).performChange("test", function() { + obj.foo = "bar"; + return {}; + }); + async = true; + + Object.unobserve(obj, handler); + + setTimeout(function() { + try { + expect(tested).to.be(true); + done(); + } catch (e) { done(e); } + }, 30); + }); }); }); \ No newline at end of file