From 6a882568b9fac11b352ee37c44e14b30d70c1a51 Mon Sep 17 00:00:00 2001 From: liabru Date: Mon, 22 Jun 2015 10:58:09 +0100 Subject: [PATCH] initial work on browser tests --- .gitignore | 2 + demo/js/Demo.js | 3 + tests/browser/TestDemo.js | 168 ++++++++++ tests/browser/lib/lodash.js | 98 ++++++ tests/browser/lib/resurrect.js | 542 +++++++++++++++++++++++++++++++++ 5 files changed, 813 insertions(+) create mode 100644 tests/browser/TestDemo.js create mode 100644 tests/browser/lib/lodash.js create mode 100644 tests/browser/lib/resurrect.js diff --git a/.gitignore b/.gitignore index ed24ce21..6ec1a08d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ matter-doc-theme build/matter-dev.js build/matter-dev.min.js demo/js/lib/matter-dev.js +tests/browser/diffs +tests/browser/worlds \ No newline at end of file diff --git a/demo/js/Demo.js b/demo/js/Demo.js index 2d23dcd3..8e75d60e 100644 --- a/demo/js/Demo.js +++ b/demo/js/Demo.js @@ -34,6 +34,8 @@ _sceneEvents = [], _useInspector = window.location.hash.indexOf('-inspect') !== -1, _isMobile = /(ipad|iphone|ipod|android)/gi.test(navigator.userAgent); + + window.Matter.Demo = Demo; // initialise the demo @@ -51,6 +53,7 @@ // create a Matter engine // NOTE: this is actually Matter.Engine.create(), see the aliases at top of this file _engine = Engine.create(container, options); + window.Matter.Demo._engine = _engine; // add a mouse controlled constraint _mouseConstraint = MouseConstraint.create(_engine); diff --git a/tests/browser/TestDemo.js b/tests/browser/TestDemo.js new file mode 100644 index 00000000..3d63cf9b --- /dev/null +++ b/tests/browser/TestDemo.js @@ -0,0 +1,168 @@ +var page = require('webpage').create(); +var fs = require('fs'); +var Resurrect = require('./lib/resurrect'); +var _ = require('./lib/lodash'); + +page.onConsoleMessage = function(msg) { + console.log(msg); +}; + +page.onError = function(msg) { + console.log(msg); +}; + +phantom.onError = function(msg, trace) { + var msgStack = ['PHANTOM ERROR: ' + msg]; + if (trace && trace.length) { + msgStack.push('TRACE:'); + trace.forEach(function(t) { + msgStack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function +')' : '')); + }); + } + console.error(msgStack.join('\n')); + phantom.exit(1); +}; + +var log = function(msg) { + console.log(JSON.stringify(msg)); +} + +var type = function(obj) { + // https://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/ + if (obj === global) { + return "global"; + } + return ({}).toString.call(obj).match(/\s([a-z|A-Z]+)/)[1].toLowerCase(); +}; + +var compare = function(objectA, objectB) { + if (objectA === objectB) { + return { equal: true }; + } + + if ((type(objectA) === 'undefined' && type(objectB) === 'null') + || (type(objectA) === 'null' && type(objectB) === 'undefined')) { + return { equal: true }; + } + + if (type(objectA) !== type(objectB)) { + return { equal: false, expected: type(objectA), actual: type(objectB) }; + } + + if (_.isNumber(objectA)) { + if (objectA.toFixed(5) === objectB.toFixed(5)) { + return { equal: true }; + } else { + return { equal: false, expected: objectA, actual: objectB }; + } + } + + if (_.isArray(objectA)) { + var arrayDelta = [], + isEqual = true; + + for (var i = 0; i < Math.max(objectA.length, objectB.length); i++) { + var diff = compare(objectA[i], objectB[i]); + arrayDelta[i] = diff; + + if (diff.equal !== true) { + isEqual = false; + } + } + + return isEqual ? { equal: true } : arrayDelta; + } + + if (_.isObject(objectA)) { + var keys = _.union(_.keys(objectA), _.keys(objectB)), + objectDelta = { equal: true }; + + for (var i = 0; i < keys.length; i++) { + var key = keys[i], + diff = compare(objectA[key], objectB[key]); + + if (diff.equal !== true) { + objectDelta[key] = diff; + objectDelta.equal = false; + } + } + + return objectDelta.equal ? { equal: true } : objectDelta; + } + + return { equal: false, expected: objectA, actual: objectB }; +}; + +page.open('http://localhost:9000/demo/dev.html', function(status) { + var demos = page.evaluate(function() { + var options = Array.prototype.slice.call(document.getElementById('demo-select').options); + return options.map(function(o) { return o.value }); + }); + + var worldsPath = 'tests/browser/worlds', + diffsPath = 'tests/browser/diffs' + resurrect = new Resurrect({ cleanup: true }), + frames = 10; + + fs.removeTree(diffsPath); + fs.makeDirectory(diffsPath); + + console.log(demos); + + for (var i = 0; i < demos.length; i += 1) { + var demo = demos[i], + worldStartPath = worldsPath + '/' + demo + '/' + demo + '-0.json', + worldEndPath = worldsPath + '/' + demo + '/' + demo + '-' + frames + '.json', + worldStartDiffPath = diffsPath + '/' + demo + '/' + demo + '-0.json', + worldEndDiffPath = diffsPath + '/' + demo + '/' + demo + '-' + frames + '.json'; + + var worldStart = page.evaluate(function(demo) { + var engine = Matter.Demo._engine; + Matter.Runner.stop(engine); + Matter.Demo[demo](); + return engine.world; + }, demo); + + var worldEnd = page.evaluate(function(demo, frames) { + var engine = Matter.Demo._engine; + for (var j = 0; j <= frames; j += 1) { + Matter.Events.trigger(engine, 'tick', { timestamp: engine.timing.timestamp }); + Matter.Engine.update(engine, engine.timing.delta); + Matter.Events.trigger(engine, 'afterTick', { timestamp: engine.timing.timestamp }); + } + return engine.world; + }, demo, frames); + + if (fs.exists(worldStartPath)) { + var worldStartRef = resurrect.resurrect(fs.read(worldStartPath)); + var worldStartDiff = compare(worldStart, worldStartRef); + + if (!worldStartDiff.equal) { + fs.write(worldStartDiffPath, JSON.stringify(worldStartDiff, null, 2), 'w'); + console.log(demo, 'start equal:', worldStartDiff.equal); + } + } else { + console.warn('no existing start reference world for', demo); + fs.write(worldStartPath, resurrect.stringify(worldStart), 'w'); + console.log('wrote', worldEndPath); + } + + if (fs.exists(worldEndPath)) { + var worldEndRef = resurrect.resurrect(fs.read(worldEndPath)); + var worldEndDiff = compare(worldEnd, worldEndRef); + + if (!worldEndDiff.equal) { + fs.write(worldEndDiffPath, JSON.stringify(worldEndDiff, null, 2), 'w'); + console.log(demo, 'end equal:', worldEndDiff.equal); + } + } else { + console.warn('no existing end reference world for', demo); + fs.write(worldEndPath, resurrect.stringify(worldEnd), 'w'); + console.log('wrote', worldEndPath); + } + } + + console.log('done'); + + phantom.exit(); +}); \ No newline at end of file diff --git a/tests/browser/lib/lodash.js b/tests/browser/lib/lodash.js new file mode 100644 index 00000000..4d6e9ff0 --- /dev/null +++ b/tests/browser/lib/lodash.js @@ -0,0 +1,98 @@ +/** + * @license + * lodash 3.9.3 (Custom Build) lodash.com/license | Underscore.js 1.8.3 underscorejs.org/LICENSE + * Build: `lodash modern -o ./lodash.js` + */ +;(function(){function n(n,t){if(n!==t){var r=null===n,e=n===m,u=n===n,i=null===t,o=t===m,f=t===t;if(n>t&&!i||!u||r&&!o&&f||e&&f)return 1;if(n=n&&9<=n&&13>=n||32==n||160==n||5760==n||6158==n||8192<=n&&(8202>=n||8232==n||8233==n||8239==n||8287==n||12288==n||65279==n); +}function _(n,t){for(var r=-1,e=n.length,u=-1,i=[];++ro(t,l,0)&&u.push(l);return u}function at(n,t){var r=true;return Mu(n,function(n,e,u){return r=!!t(n,e,u)}),r}function ct(n,t,r,e){var u=e,i=u;return Mu(n,function(n,o,f){o=+t(n,o,f),(r(o,u)||o===e&&o===i)&&(u=o,i=n)}),i}function st(n,t){var r=[];return Mu(n,function(n,e,u){t(n,e,u)&&r.push(n); +}),r}function pt(n,t,r,e){var u;return r(n,function(n,r,i){return t(n,r,i)?(u=e?r:n,false):void 0}),u}function ht(n,t,r){for(var e=-1,u=n.length,i=-1,o=[];++et&&(t=-t>u?0:u+t),r=r===m||r>u?u:+r||0,0>r&&(r+=u),u=t>r?0:r-t>>>0,t>>>=0,r=Me(u);++eu(l,s,0)&&((t||f)&&l.push(s),a.push(c))}return a}function Ft(n,t){for(var r=-1,e=t.length,u=Me(e);++r>>1,o=n[i];(r?o<=t:ou?m:i,u=1);++earguments.length;return typeof e=="function"&&i===m&&Ti(r)?n(r,e,u,o):Et(r,mr(e,i,4),u,o,t)}}function sr(n,t,r,e,u,i,o,f,l,a){function c(){for(var w=arguments.length,A=w,j=Me(w);A--;)j[A]=arguments[A];if(e&&(j=qt(j,e,u)),i&&(j=Dt(j,i,o)),v||y){var A=c.placeholder,k=_(j,A),w=w-k.length; +if(wt?0:t)):[]}function qr(n,t,r){var e=n?n.length:0;return e?((r?Cr(n,t,r):null==t)&&(t=1), +t=e-(+t||0),Ct(n,0,0>t?0:t)):[]}function Dr(n){return n?n[0]:m}function Kr(n,t,e){var u=n?n.length:0;if(!u)return-1;if(typeof e=="number")e=0>e?ku(u+e,0):e;else if(e)return e=zt(n,t),n=n[e],(t===t?t===n:n!==n)?e:-1;return r(n,t,e||0)}function Vr(n){var t=n?n.length:0;return t?n[t-1]:m}function Yr(n){return Pr(n,1)}function Zr(n,t,e,u){if(!n||!n.length)return[];null!=t&&typeof t!="boolean"&&(u=e,e=Cr(n,t,u)?null:t,t=false);var i=mr();if((null!=e||i!==it)&&(e=i(e,u,3)),t&&br()==r){t=e;var o;e=-1,u=n.length; +for(var i=-1,f=[];++er?ku(u+r,0):r||0,typeof n=="string"||!Ti(n)&&me(n)?rt?0:+t||0,e);++r=n&&(t=null),r}}function fe(n,t,r){function e(){var r=t-(wi()-a);0>=r||r>t?(f&&cu(f),r=p,f=s=p=m,r&&(h=wi(),l=n.apply(c,o),s||f||(o=c=null))):s=gu(e,r)}function u(){s&&cu(s),f=s=p=m,(v||_!==t)&&(h=wi(),l=n.apply(c,o),s||f||(o=c=null))}function i(){if(o=arguments,a=wi(),c=this,p=v&&(s||!g),false===_)var r=g&&!s;else{f||g||(h=a);var i=_-(a-h),y=0>=i||i>_;y?(f&&(f=cu(f)),h=a,l=n.apply(c,o)):f||(f=gu(u,i))}return y&&s?s=cu(s):s||t===_||(s=gu(e,t)),r&&(y=true,l=n.apply(c,o)), +!y||s||f||(o=c=null),l}var o,f,l,a,c,s,p,h=0,_=false,v=true;if(typeof n!="function")throw new Je(N);if(t=0>t?0:+t||0,true===r)var g=true,v=false;else ve(r)&&(g=r.leading,_="maxWait"in r&&ku(+r.maxWait||0,t),v="trailing"in r?r.trailing:v);return i.cancel=function(){s&&cu(s),f&&cu(f),f=s=p=m},i}function le(n,t){function r(){var e=arguments,u=t?t.apply(this,e):e[0],i=r.cache;return i.has(u)?i.get(u):(e=n.apply(this,e),r.cache=i.set(u,e),e)}if(typeof n!="function"||t&&typeof t!="function")throw new Je(N);return r.cache=new le.Cache, +r}function ae(n,t){if(typeof n!="function")throw new Je(N);return t=ku(t===m?n.length-1:+t||0,0),function(){for(var r=arguments,e=-1,u=ku(r.length-t,0),i=Me(u);++et}function se(n){return p(n)&&Ir(n)&&uu.call(n)==z}function pe(n){return!!n&&1===n.nodeType&&p(n)&&-1t||!n||!Au(t))return r;do t%2&&(r+=n),t=su(t/2),n+=n;while(t);return r}function Se(n,t,r){var e=n;return(n=u(n))?(r?Cr(e,t,r):null==t)?n.slice(v(n),g(n)+1):(t+="",n.slice(i(n,t),o(n,t)+1)):n}function Te(n,t,r){ +return r&&Cr(n,t,r)&&(t=null),n=u(n),n.match(t||Wn)||[]}function Ue(n,t,r){return r&&Cr(n,t,r)&&(t=null),p(n)?Ne(n):it(n,t)}function $e(n){return function(){return n}}function Fe(n){return n}function Ne(n){return xt(ot(n,true))}function Le(n,t,r){if(null==r){var e=ve(t),u=e?Ki(t):null;((u=u&&u.length?yt(t,u):null)?u.length:e)||(u=false,r=t,t=n,n=this)}u||(u=yt(t,Ki(t)));var i=true,e=-1,o=$i(n),f=u.length;false===r?i=false:ve(r)&&"chain"in r&&(i=r.chain);for(;++e=S)return r}else n=0;return Ku(r,e)}}(),Ju=ae(function(n,t){return Ir(n)?lt(n,ht(t,false,true)):[]}),Xu=tr(),Hu=tr(true),Qu=ae(function(n){for(var t=n.length,e=t,u=Me(c),i=br(),o=i==r,f=[];e--;){var l=n[e]=Ir(l=n[e])?l:[];u[e]=o&&120<=l.length?Vu(e&&l):null}var o=n[0],a=-1,c=o?o.length:0,s=u[0]; +n:for(;++a(s?qn(s,l):i(f,l,0))){for(e=t;--e;){var p=u[e];if(0>(p?qn(p,l):i(n[e],l,0)))continue n}s&&s.push(l),f.push(l)}return f}),ni=ae(function(t,r){r=ht(r);var e=et(t,r);return Rt(t,r.sort(n)),e}),ti=_r(),ri=_r(true),ei=ae(function(n){return $t(ht(n,false,true))}),ui=ae(function(n,t){return Ir(n)?lt(n,t):[]}),ii=ae(Gr),oi=ae(function(n){var t=n.length,r=2--n?t.apply(this,arguments):void 0}},Nn.ary=function(n,t,r){return r&&Cr(n,t,r)&&(t=null),t=n&&null==t?n.length:ku(+t||0,0),vr(n,I,null,null,null,null,t)},Nn.assign=Ni,Nn.at=fi,Nn.before=oe,Nn.bind=bi,Nn.bindAll=xi,Nn.bindKey=Ai,Nn.callback=Ue,Nn.chain=Hr,Nn.chunk=function(n,t,r){t=(r?Cr(n,t,r):null==t)?1:ku(+t||1,1),r=0;for(var e=n?n.length:0,u=-1,i=Me(au(e/t));rr&&(r=-r>u?0:u+r),e=e===m||e>u?u:+e||0,0>e&&(e+=u),u=r>e?0:e>>>0,r>>>=0;rt?0:t)):[]},Nn.takeRight=function(n,t,r){var e=n?n.length:0;return e?((r?Cr(n,t,r):null==t)&&(t=1),t=e-(+t||0),Ct(n,0>t?0:t)):[]},Nn.takeRightWhile=function(n,t,r){return n&&n.length?Nt(n,mr(t,r,3),false,true):[]},Nn.takeWhile=function(n,t,r){return n&&n.length?Nt(n,mr(t,r,3)):[]; +},Nn.tap=function(n,t,r){return t.call(r,n),n},Nn.throttle=function(n,t,r){var e=true,u=true;if(typeof n!="function")throw new Je(N);return false===r?e=false:ve(r)&&(e="leading"in r?!!r.leading:e,u="trailing"in r?!!r.trailing:u),Fn.leading=e,Fn.maxWait=+t,Fn.trailing=u,fe(n,t,Fn)},Nn.thru=Qr,Nn.times=function(n,t,r){if(n=su(n),1>n||!Au(n))return[];var e=-1,u=Me(Ou(n,4294967295));for(t=Mt(t,r,1);++ee?u[e]=t(e):t(e);return u},Nn.toArray=xe,Nn.toPlainObject=Ae,Nn.transform=function(n,t,r,e){var u=Ti(n)||we(n); +return t=mr(t,e,4),null==r&&(u||ve(n)?(e=n.constructor,r=u?Ti(n)?new e:[]:Bu($i(e)?e.prototype:null)):r={}),(u?Kn:vt)(n,function(n,e,u){return t(r,n,e,u)}),r},Nn.union=ei,Nn.uniq=Zr,Nn.unzip=Gr,Nn.unzipWith=Jr,Nn.values=Re,Nn.valuesIn=function(n){return Ft(n,ke(n))},Nn.where=function(n,t){return te(n,xt(t))},Nn.without=ui,Nn.wrap=function(n,t){return t=null==t?Fe:t,vr(t,O,null,[n],[])},Nn.xor=function(){for(var n=-1,t=arguments.length;++nr?0:+r||0,e),r-=t.length,0<=r&&n.indexOf(t,r)==r},Nn.escape=function(n){return(n=u(n))&&pn.test(n)?n.replace(cn,a):n},Nn.escapeRegExp=Ee,Nn.every=ne,Nn.find=ai,Nn.findIndex=Xu,Nn.findKey=zi,Nn.findLast=ci,Nn.findLastIndex=Hu,Nn.findLastKey=Bi,Nn.findWhere=function(n,t){return ai(n,xt(t))},Nn.first=Dr,Nn.get=function(n,t,r){ +return n=null==n?m:dt(n,Br(t),t+""),n===m?r:n},Nn.gt=ce,Nn.gte=function(n,t){return n>=t},Nn.has=function(n,t){if(null==n)return false;var r=ru.call(n,t);if(!r&&!Wr(t)){if(t=Br(t),n=1==t.length?n:dt(n,Ct(t,0,-1)),null==n)return false;t=Vr(t),r=ru.call(n,t)}return r||Tr(n.length)&&Er(t,n.length)&&(Ti(n)||se(n))},Nn.identity=Fe,Nn.includes=re,Nn.indexOf=Kr,Nn.inRange=function(n,t,r){return t=+t||0,"undefined"===typeof r?(r=t,t=0):r=+r||0,n>=Ou(t,r)&&nr?ku(e+r,0):Ou(r||0,e-1))+1;else if(r)return u=zt(n,t,true)-1,n=n[u],(t===t?t===n:n!==n)?u:-1;if(t!==t)return s(n,u,true);for(;u--;)if(n[u]===t)return u;return-1},Nn.lt=be,Nn.lte=function(n,t){return n<=t},Nn.max=oo,Nn.min=fo,Nn.noConflict=function(){return h._=iu,this},Nn.noop=ze,Nn.now=wi, +Nn.pad=function(n,t,r){n=u(n),t=+t;var e=n.length;return er?0:+r||0,n.length),n.lastIndexOf(t,r)==r},Nn.sum=function(n,t,r){r&&Cr(n,t,r)&&(t=null);var e=mr(),u=null==t;if(u&&e===it||(u=false, +t=e(t,r,3)),u){for(n=Ti(n)?n:Lr(n),t=n.length,r=0;t--;)r+=+n[t]||0;n=r}else n=Ut(n,t);return n},Nn.template=function(n,t,r){var e=Nn.templateSettings;r&&Cr(n,t,r)&&(t=r=null),n=u(n),t=tt(rt({},r||t),e,nt),r=tt(rt({},t.imports),e.imports,nt);var i,o,f=Ki(r),l=Ft(r,f),a=0;r=t.interpolate||En;var s="__p+='";r=Ze((t.escape||En).source+"|"+r.source+"|"+(r===vn?An:En).source+"|"+(t.evaluate||En).source+"|$","g");var p="sourceURL"in t?"//# sourceURL="+t.sourceURL+"\n":"";if(n.replace(r,function(t,r,e,u,f,l){ +return e||(e=u),s+=n.slice(a,l).replace(Cn,c),r&&(i=true,s+="'+__e("+r+")+'"),f&&(o=true,s+="';"+f+";\n__p+='"),e&&(s+="'+((__t=("+e+"))==null?'':__t)+'"),a=l+t.length,t}),s+="';",(t=t.variable)||(s="with(obj){"+s+"}"),s=(o?s.replace(on,""):s).replace(fn,"$1").replace(ln,"$1;"),s="function("+(t||"obj")+"){"+(t?"":"obj||(obj={});")+"var __t,__p=''"+(i?",__e=_.escape":"")+(o?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+s+"return __p}",t=eo(function(){return De(f,p+"return "+s).apply(m,l); +}),t.source=s,_e(t))throw t;return t},Nn.trim=Se,Nn.trimLeft=function(n,t,r){var e=n;return(n=u(n))?n.slice((r?Cr(e,t,r):null==t)?v(n):i(n,t+"")):n},Nn.trimRight=function(n,t,r){var e=n;return(n=u(n))?(r?Cr(e,t,r):null==t)?n.slice(0,g(n)+1):n.slice(0,o(n,t+"")+1):n},Nn.trunc=function(n,t,r){r&&Cr(n,t,r)&&(t=null);var e=C;if(r=W,null!=t)if(ve(t)){var i="separator"in t?t.separator:i,e="length"in t?+t.length||0:e;r="omission"in t?u(t.omission):r}else e=+t||0;if(n=u(n),e>=n.length)return n;if(e-=r.length, +1>e)return r;if(t=n.slice(0,e),null==i)return t+r;if(de(i)){if(n.slice(e).search(i)){var o,f=n.slice(0,e);for(i.global||(i=Ze(i.source,(jn.exec(i)||"")+"g")),i.lastIndex=0;n=i.exec(f);)o=n.index;t=t.slice(0,null==o?e:o)}}else n.indexOf(i,e)!=e&&(i=t.lastIndexOf(i),-1u.__dir__?"Right":"")}),u},Bn.prototype[n+"Right"]=function(t){return this.reverse()[n](t).reverse(); +},Bn.prototype[n+"RightWhile"]=function(n,t){return this.reverse()[r](n,t).reverse()}}),Kn(["first","last"],function(n,t){var r="take"+(t?"Right":"");Bn.prototype[n]=function(){return this[r](1).value()[0]}}),Kn(["initial","rest"],function(n,t){var r="drop"+(t?"":"Right");Bn.prototype[n]=function(){return this[r](1)}}),Kn(["pluck","where"],function(n,t){var r=t?"filter":"map",e=t?xt:Be;Bn.prototype[n]=function(n){return this[r](e(n))}}),Bn.prototype.compact=function(){return this.filter(Fe)},Bn.prototype.reject=function(n,t){ +return n=mr(n,t,1),this.filter(function(t){return!n(t)})},Bn.prototype.slice=function(n,t){n=null==n?0:+n||0;var r=this;return 0>n?r=this.takeRight(-n):n&&(r=this.drop(n)),t!==m&&(t=+t||0,r=0>t?r.dropRight(-t):r.take(t-n)),r},Bn.prototype.toArray=function(){return this.drop(0)},vt(Bn.prototype,function(n,t){var r=Nn[t];if(r){var e=/^(?:filter|map|reject)|While$/.test(t),u=/^(?:first|last)$/.test(t);Nn.prototype[t]=function(){function t(n){return n=[n],_u.apply(n,i),r.apply(Nn,n)}var i=arguments,o=this.__chain__,f=this.__wrapped__,l=!!this.__actions__.length,a=f instanceof Bn,c=i[0],s=a||Ti(f); +return s&&e&&typeof c=="function"&&1!=c.length&&(a=s=false),a=a&&!l,u&&!o?a?n.call(f):r.call(Nn,this.value()):s?(f=n.apply(a?f:new Bn(this),i),u||!l&&!f.__actions__||(f.__actions__||(f.__actions__=[])).push({func:Qr,args:[t],thisArg:Nn}),new zn(f,o)):this.thru(t)}}}),Kn("concat join pop push replace shift sort splice split unshift".split(" "),function(n){var t=(/^(?:replace|split)$/.test(n)?Qe:Xe)[n],r=/^(?:push|sort|unshift)$/.test(n)?"tap":"thru",e=/^(?:join|pop|replace|shift)$/.test(n);Nn.prototype[n]=function(){ +var n=arguments;return e&&!this.__chain__?t.apply(this.value(),n):this[r](function(r){return t.apply(r,n)})}}),vt(Bn.prototype,function(n,t){var r=Nn[t];if(r){var e=r.name;(Lu[e]||(Lu[e]=[])).push({name:t,func:r})}}),Lu[sr(null,x).name]=[{name:"wrapper",func:null}],Bn.prototype.clone=function(){var n=this.__actions__,t=this.__iteratees__,r=this.__views__,e=new Bn(this.__wrapped__);return e.__actions__=n?Dn(n):null,e.__dir__=this.__dir__,e.__filtered__=this.__filtered__,e.__iteratees__=t?Dn(t):null, +e.__takeCount__=this.__takeCount__,e.__views__=r?Dn(r):null,e},Bn.prototype.reverse=function(){if(this.__filtered__){var n=new Bn(this);n.__dir__=-1,n.__filtered__=true}else n=this.clone(),n.__dir__*=-1;return n},Bn.prototype.value=function(){var n=this.__wrapped__.value();if(!Ti(n))return Lt(n,this.__actions__);var t,r=this.__dir__,e=0>r;t=n.length;for(var u=this.__views__,i=0,o=-1,f=u?u.length:0;++op.index:u=_:!h(s))))continue n}else if(p=h(s),_==F)s=p;else if(!p){if(_==$)continue n;break n}}a[l++]=s}return a},Nn.prototype.chain=function(){ +return Hr(this)},Nn.prototype.commit=function(){return new zn(this.value(),this.__chain__)},Nn.prototype.plant=function(n){for(var t,r=this;r instanceof Ln;){var e=Mr(r);t?u.__wrapped__=e:t=e;var u=e,r=r.__wrapped__}return u.__wrapped__=n,t},Nn.prototype.reverse=function(){var n=this.__wrapped__;return n instanceof Bn?(this.__actions__.length&&(n=new Bn(this)),new zn(n.reverse(),this.__chain__)):this.thru(function(n){return n.reverse()})},Nn.prototype.toString=function(){return this.value()+""},Nn.prototype.run=Nn.prototype.toJSON=Nn.prototype.valueOf=Nn.prototype.value=function(){ +return Lt(this.__wrapped__,this.__actions__)},Nn.prototype.collect=Nn.prototype.map,Nn.prototype.head=Nn.prototype.first,Nn.prototype.select=Nn.prototype.filter,Nn.prototype.tail=Nn.prototype.rest,Nn}var m,w="3.9.3",b=1,x=2,A=4,j=8,k=16,O=32,R=64,I=128,E=256,C=30,W="...",S=150,T=16,U=0,$=1,F=2,N="Expected a function",L="__lodash_placeholder__",z="[object Arguments]",B="[object Array]",M="[object Boolean]",P="[object Date]",q="[object Error]",D="[object Function]",K="[object Number]",V="[object Object]",Y="[object RegExp]",Z="[object String]",G="[object ArrayBuffer]",J="[object Float32Array]",X="[object Float64Array]",H="[object Int8Array]",Q="[object Int16Array]",nn="[object Int32Array]",tn="[object Uint8Array]",rn="[object Uint8ClampedArray]",en="[object Uint16Array]",un="[object Uint32Array]",on=/\b__p\+='';/g,fn=/\b(__p\+=)''\+/g,ln=/(__e\(.*?\)|\b__t\))\+'';/g,an=/&(?:amp|lt|gt|quot|#39|#96);/g,cn=/[&<>"'`]/g,sn=RegExp(an.source),pn=RegExp(cn.source),hn=/<%-([\s\S]+?)%>/g,_n=/<%([\s\S]+?)%>/g,vn=/<%=([\s\S]+?)%>/g,gn=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\n\\]|\\.)*?\1)\]/,yn=/^\w*$/,dn=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\n\\]|\\.)*?)\2)\]/g,mn=/[.*+?^${}()|[\]\/\\]/g,wn=RegExp(mn.source),bn=/[\u0300-\u036f\ufe20-\ufe23]/g,xn=/\\(\\)?/g,An=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,jn=/\w*$/,kn=/^0[xX]/,On=/^\[object .+?Constructor\]$/,Rn=/^\d+$/,In=/[\xc0-\xd6\xd8-\xde\xdf-\xf6\xf8-\xff]/g,En=/($^)/,Cn=/['\n\r\u2028\u2029\\]/g,Wn=RegExp("[A-Z\\xc0-\\xd6\\xd8-\\xde]+(?=[A-Z\\xc0-\\xd6\\xd8-\\xde][a-z\\xdf-\\xf6\\xf8-\\xff]+)|[A-Z\\xc0-\\xd6\\xd8-\\xde]?[a-z\\xdf-\\xf6\\xf8-\\xff]+|[A-Z\\xc0-\\xd6\\xd8-\\xde]+|[0-9]+","g"),Sn=" \t\x0b\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000",Tn="Array ArrayBuffer Date Error Float32Array Float64Array Function Int8Array Int16Array Int32Array Math Number Object RegExp Set String _ clearTimeout document isFinite parseFloat parseInt setTimeout TypeError Uint8Array Uint8ClampedArray Uint16Array Uint32Array WeakMap window".split(" "),Un={}; +Un[J]=Un[X]=Un[H]=Un[Q]=Un[nn]=Un[tn]=Un[rn]=Un[en]=Un[un]=true,Un[z]=Un[B]=Un[G]=Un[M]=Un[P]=Un[q]=Un[D]=Un["[object Map]"]=Un[K]=Un[V]=Un[Y]=Un["[object Set]"]=Un[Z]=Un["[object WeakMap]"]=false;var $n={};$n[z]=$n[B]=$n[G]=$n[M]=$n[P]=$n[J]=$n[X]=$n[H]=$n[Q]=$n[nn]=$n[K]=$n[V]=$n[Y]=$n[Z]=$n[tn]=$n[rn]=$n[en]=$n[un]=true,$n[q]=$n[D]=$n["[object Map]"]=$n["[object Set]"]=$n["[object WeakMap]"]=false;var Fn={leading:false,maxWait:0,trailing:false},Nn={"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A", +"\xe0":"a","\xe1":"a","\xe2":"a","\xe3":"a","\xe4":"a","\xe5":"a","\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u", +"\xfb":"u","\xfc":"u","\xdd":"Y","\xfd":"y","\xff":"y","\xc6":"Ae","\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss"},Ln={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},zn={"&":"&","<":"<",">":">",""":'"',"'":"'","`":"`"},Bn={"function":true,object:true},Mn={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Pn=Bn[typeof exports]&&exports&&!exports.nodeType&&exports,qn=Bn[typeof module]&&module&&!module.nodeType&&module,Dn=Bn[typeof self]&&self&&self.Object&&self,Kn=Bn[typeof window]&&window&&window.Object&&window,Vn=qn&&qn.exports===Pn&&Pn,Yn=Pn&&qn&&typeof global=="object"&&global&&global.Object&&global||Kn!==(this&&this.window)&&Kn||Dn||this,Zn=d(); +typeof define=="function"&&typeof define.amd=="object"&&define.amd?(Yn._=Zn, define(function(){return Zn})):Pn&&qn?Vn?(qn.exports=Zn)._=Zn:Pn._=Zn:Yn._=Zn}).call(this); \ No newline at end of file diff --git a/tests/browser/lib/resurrect.js b/tests/browser/lib/resurrect.js new file mode 100644 index 00000000..8e56e278 --- /dev/null +++ b/tests/browser/lib/resurrect.js @@ -0,0 +1,542 @@ +/** + * # ResurrectJS + * @version 1.0.3 + * @license Public Domain + * + * ResurrectJS preserves object behavior (prototypes) and reference + * circularity with a special JSON encoding. Unlike regular JSON, + * Date, RegExp, DOM objects, and `undefined` are also properly + * preserved. + * + * ## Examples + * + * function Foo() {} + * Foo.prototype.greet = function() { return "hello"; }; + * + * // Behavior is preserved: + * var necromancer = new Resurrect(); + * var json = necromancer.stringify(new Foo()); + * var foo = necromancer.resurrect(json); + * foo.greet(); // => "hello" + * + * // References to the same object are preserved: + * json = necromancer.stringify([foo, foo]); + * var array = necromancer.resurrect(json); + * array[0] === array[1]; // => true + * array[1].greet(); // => "hello" + * + * // Dates are restored properly + * json = necromancer.stringify(new Date()); + * var date = necromancer.resurrect(json); + * Object.prototype.toString.call(date); // => "[object Date]" + * + * ## Options + * + * Options are provided to the constructor as an object with these + * properties: + * + * prefix ('#'): A prefix string used for temporary properties added + * to objects during serialization and deserialization. It is + * important that you don't use any properties beginning with this + * string. This option must be consistent between both + * serialization and deserialization. + * + * cleanup (false): Perform full property cleanup after both + * serialization and deserialization using the `delete` + * operator. This may cause performance penalties (breaking hidden + * classes in V8) on objects that ResurrectJS touches, so enable + * with care. + * + * revive (true): Restore behavior (__proto__) to objects that have + * been resurrected. If this is set to false during serialization, + * resurrection information will not be encoded. You still get + * circularity and Date support. + * + * resolver (Resurrect.NamespaceResolver(window)): Converts between + * a name and a prototype. Create a custom resolver if your + * constructors are not stored in global variables. The resolver + * has two methods: getName(object) and getPrototype(string). + * + * For example, + * + * var necromancer = new Resurrect({ + * prefix: '__#', + * cleanup: true + * }); + * + * ## Caveats + * + * * With the default resolver, all constructors must be named and + * stored in the global variable under that name. This is required + * so that the prototypes can be looked up and reconnected at + * resurrection time. + * + * * The wrapper objects Boolean, String, and Number will be + * unwrapped. This means extra properties added to these objects + * will not be preserved. + * + * * Functions cannot ever be serialized. Resurrect will throw an + * error if a function is found when traversing a data structure. + * + * @see http://nullprogram.com/blog/2013/03/28/ + */ + +/** + * @param {Object} [options] See options documentation. + * @namespace + * @constructor + */ +function Resurrect(options) { + this.table = null; + this.prefix = '#'; + this.cleanup = false; + this.revive = true; + for (var option in options) { + if (options.hasOwnProperty(option)) { + this[option] = options[option]; + } + } + this.refcode = this.prefix + 'id'; + this.origcode = this.prefix + 'original'; + this.buildcode = this.prefix + '.'; + this.valuecode = this.prefix + 'v'; +} + +if (module) + module.exports = Resurrect; + +/** + * Portable access to the global object (window, global). + * Uses indirect eval. + * @constant + */ +Resurrect.GLOBAL = (0, eval)('this'); + +/** + * Escape special regular expression characters in a string. + * @param {string} string + * @returns {string} The string escaped for exact matches. + * @see http://stackoverflow.com/a/6969486 + */ +Resurrect.escapeRegExp = function (string) { + return string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); +}; + +/* Helper Objects */ + +/** + * @param {string} [message] + * @constructor + */ +Resurrect.prototype.Error = function ResurrectError(message) { + this.message = message || ''; + this.stack = new Error().stack; +}; +Resurrect.prototype.Error.prototype = Object.create(Error.prototype); +Resurrect.prototype.Error.prototype.name = 'ResurrectError'; + +/** + * Resolves prototypes through the properties on an object and + * constructor names. + * @param {Object} scope + * @constructor + */ +Resurrect.NamespaceResolver = function(scope) { + this.scope = scope; +}; + +/** + * Gets the prototype of the given property name from an object. If + * not found, it throws an error. + * @param {string} name + * @returns {Object} + * @method + */ +Resurrect.NamespaceResolver.prototype.getPrototype = function(name) { + var constructor = this.scope[name]; + if (constructor) { + return constructor.prototype; + } else { + throw new Resurrect.prototype.Error('Unknown constructor: ' + name); + } +}; + +/** + * Get the prototype name for an object, to be fetched later with getPrototype. + * @param {Object} object + * @returns {?string} Null if the constructor is Object. + * @method + */ +Resurrect.NamespaceResolver.prototype.getName = function(object) { + var constructor = object.constructor.name; + if (constructor == null) { // IE + var funcPattern = /^\s*function\s*([A-Za-z0-9_$]*)/; + constructor = funcPattern.exec(object.constructor)[1]; + } + + if (constructor === '') { + var msg = "Can't serialize objects with anonymous constructors."; + throw new Resurrect.prototype.Error(msg); + } else if (constructor === 'Object' || constructor === 'Array') { + return null; + } else { + return constructor; + } +}; + +/* Set the default resolver searches the global object. */ +Resurrect.prototype.resolver = + new Resurrect.NamespaceResolver(Resurrect.GLOBAL); + +/** + * Create a DOM node from HTML source; behaves like a constructor. + * @param {string} html + * @constructor + */ +Resurrect.Node = function(html) { + var div = document.createElement('div'); + div.innerHTML = html; + return div.firstChild; +}; + +/* Type Tests */ + +/** + * @param {string} type + * @returns {Function} A function that tests for type. + */ +Resurrect.is = function(type) { + var string = '[object ' + type + ']'; + return function(object) { + return Object.prototype.toString.call(object) === string; + }; +}; + +Resurrect.isArray = Resurrect.is('Array'); +Resurrect.isString = Resurrect.is('String'); +Resurrect.isBoolean = Resurrect.is('Boolean'); +Resurrect.isNumber = Resurrect.is('Number'); +Resurrect.isFunction = Resurrect.is('Function'); +Resurrect.isDate = Resurrect.is('Date'); +Resurrect.isRegExp = Resurrect.is('RegExp'); +Resurrect.isObject = Resurrect.is('Object'); + +Resurrect.isAtom = function(object) { + return !Resurrect.isObject(object) && !Resurrect.isArray(object); +}; + +/** + * @param {*} object + * @returns {boolean} True if object is a primitive or a primitive wrapper. + */ +Resurrect.isPrimitive = function(object) { + return object == null || + Resurrect.isNumber(object) || + Resurrect.isString(object) || + Resurrect.isBoolean(object); +}; + +/* Methods */ + +/** + * Create a reference (encoding) to an object. + * @param {(Object|undefined)} object + * @returns {Object} + * @method + */ +Resurrect.prototype.ref = function(object) { + var ref = {}; + if (object === undefined) { + ref[this.prefix] = -1; + } else { + ref[this.prefix] = object[this.refcode]; + } + return ref; +}; + +/** + * Lookup an object in the table by reference object. + * @param {Object} ref + * @returns {(Object|undefined)} + * @method + */ +Resurrect.prototype.deref = function(ref) { + return this.table[ref[this.prefix]]; +}; + +/** + * Put a temporary identifier on an object and store it in the table. + * @param {Object} object + * @returns {number} The unique identifier number. + * @method + */ +Resurrect.prototype.tag = function(object) { + if (this.revive) { + var constructor = this.resolver.getName(object); + if (constructor) { + var proto = Object.getPrototypeOf(object); + if (this.resolver.getPrototype(constructor) !== proto) { + throw new this.Error('Constructor mismatch!'); + } else { + object[this.prefix] = constructor; + } + } + } + object[this.refcode] = this.table.length; + this.table.push(object); + return object[this.refcode]; +}; + +/** + * Create a builder object (encoding) for serialization. + * @param {string} name The name of the constructor. + * @param value The value to pass to the constructor. + * @returns {Object} + * @method + */ +Resurrect.prototype.builder = function(name, value) { + var builder = {}; + builder[this.buildcode] = name; + builder[this.valuecode] = value; + return builder; +}; + +/** + * Build a value from a deserialized builder. + * @param {Object} ref + * @returns {Object} + * @method + * @see http://stackoverflow.com/a/14378462 + * @see http://nullprogram.com/blog/2013/03/24/ + */ +Resurrect.prototype.build = function(ref) { + var type = ref[this.buildcode].split(/\./).reduce(function(object, name) { + return object[name]; + }, Resurrect.GLOBAL); + /* Brilliant hack by kybernetikos: */ + var args = [null].concat(ref[this.valuecode]); + var factory = type.bind.apply(type, args); + var result = new factory(); + if (Resurrect.isPrimitive(result)) { + return result.valueOf(); // unwrap + } else { + return result; + } +}; + +/** + * Dereference or build an object or value from an encoding. + * @param {Object} ref + * @returns {(Object|undefined)} + * @method + */ +Resurrect.prototype.decode = function(ref) { + if (this.prefix in ref) { + return this.deref(ref); + } else if (this.buildcode in ref) { + return this.build(ref); + } else { + throw new this.Error('Unknown encoding.'); + } +}; + +/** + * @param {Object} object + * @returns {boolean} True if the provided object is tagged for serialization. + * @method + */ +Resurrect.prototype.isTagged = function(object) { + return (this.refcode in object) && (object[this.refcode] != null); +}; + +/** + * Visit root and all its ancestors, visiting atoms with f. + * @param {*} root + * @param {Function} f + * @param {Function} replacer + * @returns {*} A fresh copy of root to be serialized. + * @method + */ +Resurrect.prototype.visit = function(root, f, replacer) { + if (Resurrect.isAtom(root)) { + return f(root); + } else if (!this.isTagged(root)) { + var copy = null; + if (Resurrect.isArray(root)) { + copy = []; + root[this.refcode] = this.tag(copy); + for (var i = 0; i < root.length; i++) { + copy.push(this.visit(root[i], f, replacer)); + } + } else { /* Object */ + copy = Object.create(Object.getPrototypeOf(root)); + root[this.refcode] = this.tag(copy); + for (var key in root) { + var value = root[key]; + if (root.hasOwnProperty(key)) { + if (replacer && value !== undefined) { + // Call replacer like JSON.stringify's replacer + value = replacer.call(root, key, root[key]); + if (value === undefined) { + continue; // Omit from result + } + } + copy[key] = this.visit(value, f, replacer); + } + } + } + copy[this.origcode] = root; + return this.ref(copy); + } else { + return this.ref(root); + } +}; + +/** + * Manage special atom values, possibly returning an encoding. + * @param {*} atom + * @returns {*} + * @method + */ +Resurrect.prototype.handleAtom = function(atom) { + var Node = Resurrect.GLOBAL.Node || function() {}; + if (Resurrect.isFunction(atom)) { + throw new this.Error("Can't serialize functions."); + } else if (atom instanceof Node) { + var xmls = new XMLSerializer(); + return this.builder('Resurrect.Node', [xmls.serializeToString(atom)]); + } else if (Resurrect.isDate(atom)) { + return this.builder('Date', [atom.toISOString()]); + } else if (Resurrect.isRegExp(atom)) { + var args = atom.toString().match(/\/(.+)\/([gimy]*)/).slice(1); + return this.builder('RegExp', args); + } else if (atom === undefined) { + return this.ref(undefined); + } else if (Resurrect.isNumber(atom) && (isNaN(atom) || !isFinite(atom))) { + return this.builder('Number', [atom.toString()]); + } else { + return atom; + } +}; + +/** + * Hides intrusive keys from a user-supplied replacer. + * @param {Function} replacer function of two arguments (key, value) + * @returns {Function} A function that skips the replacer for intrusive keys. + * @method + */ +Resurrect.prototype.replacerWrapper = function(replacer) { + var skip = new RegExp('^' + Resurrect.escapeRegExp(this.prefix)); + return function(k, v) { + if (skip.test(k)) { + return v; + } else { + return replacer(k, v); + } + }; +}; + +/** + * Serialize an arbitrary JavaScript object, carefully preserving it. + * @param {*} object + * @param {(Function|Array)} replacer + * @param {string} space + * @method + */ +Resurrect.prototype.stringify = function(object, replacer, space) { + if (Resurrect.isFunction(replacer)) { + replacer = this.replacerWrapper(replacer); + } else if (Resurrect.isArray(replacer)) { + var acceptKeys = replacer; + replacer = function(k, v) { + return acceptKeys.indexOf(k) >= 0 ? v : undefined; + }; + } + if (Resurrect.isAtom(object)) { + return JSON.stringify(this.handleAtom(object), replacer, space); + } else { + this.table = []; + this.visit(object, this.handleAtom.bind(this), replacer); + for (var i = 0; i < this.table.length; i++) { + if (this.cleanup) { + delete this.table[i][this.origcode][this.refcode]; + } else { + this.table[i][this.origcode][this.refcode] = null; + } + delete this.table[i][this.refcode]; + delete this.table[i][this.origcode]; + } + var table = this.table; + this.table = null; + return JSON.stringify(table, null, space); + } +}; + +/** + * Restore the __proto__ of the given object to the proper value. + * @param {Object} object + * @returns {Object} Its argument, or a copy, with the prototype restored. + * @method + */ +Resurrect.prototype.fixPrototype = function(object) { + if (this.prefix in object) { + var name = object[this.prefix]; + var prototype = this.resolver.getPrototype(name); + if ('__proto__' in object) { + object.__proto__ = prototype; + if (this.cleanup) { + delete object[this.prefix]; + } + return object; + } else { // IE + var copy = Object.create(prototype); + for (var key in object) { + if (object.hasOwnProperty(key) && key !== this.prefix) { + copy[key] = object[key]; + } + } + return copy; + } + } else { + return object; + } +}; + +/** + * Deserialize an encoded object, restoring circularity and behavior. + * @param {string} string + * @returns {*} The decoded object or value. + * @method + */ +Resurrect.prototype.resurrect = function(string) { + var result = null; + var data = JSON.parse(string); + if (Resurrect.isArray(data)) { + this.table = data; + /* Restore __proto__. */ + if (this.revive) { + for (var i = 0; i < this.table.length; i++) { + this.table[i] = this.fixPrototype(this.table[i]); + } + } + /* Re-establish object references and construct atoms. */ + for (i = 0; i < this.table.length; i++) { + var object = this.table[i]; + for (var key in object) { + if (object.hasOwnProperty(key)) { + if (!(Resurrect.isAtom(object[key]))) { + object[key] = this.decode(object[key]); + } + } + } + } + result = this.table[0]; + } else if (Resurrect.isObject(data)) { + this.table = []; + result = this.decode(data); + } else { + result = data; + } + this.table = null; + return result; +};