0
0
Fork 0
mirror of https://github.com/liabru/matter-js.git synced 2025-01-20 17:10:11 -05:00

changed example tests to use jest

This commit is contained in:
liabru 2019-09-14 19:31:31 +01:00
parent 3c32969da2
commit 104d31902c
8 changed files with 225 additions and 936 deletions

3
.gitignore vendored
View file

@ -12,4 +12,5 @@ examples/build
test/browser/diffs
test/browser/refs
test/node/diffs
test/node/refs
test/node/refs
__snapshots__

View file

@ -81,13 +81,17 @@ gulp.task('release', callback => {
message: 'cannot build release as there are uncomitted changes'
});
} else {
sequence('release:test', 'bump', 'release:build', 'doc', 'changelog', callback);
sequence(
'release:lint', 'bump', 'release:build', 'release:test',
'doc', 'changelog', callback
);
}
});
});
gulp.task('release:test', shell('npm run test'));
gulp.task('release:lint', shell('npm run lint'));
gulp.task('release:build', shell('npm run build'));
gulp.task('release:test', shell('TEST_BUILD=true npm run test'));
gulp.task('release:push:git', shell('git push'));
gulp.task('release:push:npm', shell('npm publish'));

View file

@ -41,6 +41,9 @@
"build-examples": "webpack --config webpack.examples.config.js --mode=production --env.EDGE & webpack --config webpack.examples.config.js --mode=production --env.MINIMIZE --env.EDGE",
"lint": "eslint 'src/**/*.js' 'demo/js/Demo.js' 'examples/*.js'",
"doc": "gulp doc",
"test": "jest",
"test-snapshot": "TEST_SNAPSHOTS=true jest --ci",
"test-snapshot-update": "TEST_SNAPSHOTS=true jest -u"
},
"dependencies": {},
"files": [

118
test/Common.js Normal file
View file

@ -0,0 +1,118 @@
const { Composite, Constraint, Vertices } = require('../src/module/main');
const includeKeys = [
// Common
'id', 'label',
// Constraint
'angularStiffness', 'bodyA', 'bodyB', 'pointA', 'pointB', 'damping', 'length', 'stiffness',
// Body
'angle', 'anglePrev', 'area', 'axes', 'bounds', 'min', 'max', 'x', 'y', 'collisionFilter', 'category', 'mask',
'group', 'density', 'friction', 'frictionAir', 'frictionStatic', 'inertia', 'inverseInertia', 'inverseMass', 'isSensor',
'isSleeping', 'isStatic', 'mass', 'parent', 'parts', 'position', 'positionPrev', 'restitution', 'sleepThreshold', 'slop',
'timeScale', 'vertices'
];
const limit = (val, precision=3) => {
if (typeof val === 'number') {
return parseFloat(val.toPrecision(precision));
}
return val;
};
const engineSnapshot = (engine, extended=false) => {
const {
positionIterations, velocityIterations,
constraintIterations, world
} = engine;
const bodies = Composite.allBodies(world);
const constraints = Composite.allConstraints(world);
const composites = Composite.allComposites(world);
return {
positionIterations,
velocityIterations,
constraintIterations,
bodyCount: bodies.length,
constraintCount: constraints.length,
compositeCount: composites.length,
averageBodyPosition: Vertices.mean(bodies.map(body => body.position)),
averageBodyPositionPrev: Vertices.mean(bodies.map(body => body.positionPrev)),
averageBodyAngle: bodies.reduce((angle, body) => angle + body.angle, 0) / bodies.length,
averageBodyAnglePrev: bodies.reduce((angle, body) => angle + body.anglePrev, 0) / bodies.length,
averageConstraintPosition: Vertices.mean(
constraints.reduce((positions, constraint) => {
positions.push(
Constraint.pointAWorld(constraint),
Constraint.pointBWorld(constraint)
);
return positions;
}, []).concat({ x: 0, y: 0 })
),
world: extended ? worldSnapshotExtended(engine.world) : worldSnapshot(engine.world)
};
};
const worldSnapshot = world => ({
...Composite.allBodies(world).reduce((bodies, body) => {
bodies[`${body.id} ${body.label}`] =
`${limit(body.position.x)} ${limit(body.position.y)} ${limit(body.angle)}`
+ ` ${limit(body.position.x - body.positionPrev.x)} ${limit(body.position.y - body.positionPrev.y)}`
+ ` ${limit(body.angle - body.anglePrev)}`;
return bodies;
}, {}),
...Composite.allConstraints(world).reduce((constraints, constraint) => {
const positionA = Constraint.pointAWorld(constraint);
const positionB = Constraint.pointBWorld(constraint);
constraints[`${constraint.id} ${constraint.label}`] =
`${limit(positionA.x)} ${limit(positionA.y)} ${limit(positionB.x)} ${limit(positionB.y)}`
+ ` ${constraint.bodyA ? constraint.bodyA.id : null} ${constraint.bodyB ? constraint.bodyB.id : null}`;
return constraints;
}, {})
});
const worldSnapshotExtended = world => worldSnapshotExtendedBase({
...Composite.allBodies(world).reduce((bodies, body) => {
bodies[body.id] = body;
return bodies;
}, {}),
...Composite.allConstraints(world).reduce((constraints, constraint) => {
constraints[constraint.id] = constraint;
return constraints;
}, {})
});
const worldSnapshotExtendedBase = (obj, depth=0) => {
if (typeof obj === 'number') {
return limit(obj);
}
if (Array.isArray(obj)) {
return obj.map(item => worldSnapshotExtendedBase(item, depth + 1));
}
if (typeof obj !== 'object') {
return obj;
}
return Object.entries(obj)
.filter(([key]) => depth === 0 || includeKeys.includes(key))
.reduce((cleaned, [key, val]) => {
if (val && val.id && String(val.id) !== key) {
val = val.id;
}
if (Array.isArray(val)) {
val = `[${val.length}]`;
}
return { ...cleaned, [key]: worldSnapshotExtendedBase(val, depth + 1) };
}, {});
};
module.exports = { engineSnapshot };

96
test/Examples.spec.js Normal file
View file

@ -0,0 +1,96 @@
"use strict";
const Common = require('./Common');
const fs = require('fs');
const execSync = require('child_process').execSync;
const useSnapshots = process.env.TEST_SNAPSHOTS === 'true';
const useBuild = process.env.TEST_BUILD === 'true';
console.info(`Testing Matter from ${useBuild ? `build '../build/matter'.` : `source '../src/module/main'.`}`);
// mock modules
if (useBuild) {
jest.mock('matter-js', () => require('../build/matter'), { virtual: true });
} else {
jest.mock('matter-js', () => require('../src/module/main'), { virtual: true });
}
jest.mock('matter-wrap', () => require('../demo/lib/matter-wrap'), { virtual: true });
jest.mock('poly-decomp', () => require('../demo/lib/decomp'), { virtual: true });
// import mocked Matter and plugins
const Matter = global.Matter = require('matter-js');
Matter.Plugin.register(require('matter-wrap'));
// import Examples after Matter
const Example = require('../examples/index');
// stub out browser-only functions
const noop = () => ({ collisionFilter: {}, mouse: {} });
Matter.Render.create = () => ({ options: {}, bounds: { min: { x: 0, y: 0 }, max: { x: 800, y: 600 }}});
Matter.Render.run = Matter.Render.lookAt = noop;
Matter.Runner.create = Matter.Runner.run = noop;
Matter.MouseConstraint.create = Matter.Mouse.create = noop;
Matter.Common.log = Matter.Common.info = Matter.Common.warn = noop;
// check initial snapshots if enabled (experimental)
if (useSnapshots && !fs.existsSync('./test/__snapshots__')) {
const gitState = execSync('git log -n 1 --pretty=%d HEAD').toString().trim();
const gitIsClean = execSync('git status --porcelain').toString().trim().length === 0;
const gitIsMaster = gitState.startsWith('(HEAD -> master, origin/master');
if (!gitIsMaster || !gitIsClean) {
throw `Snapshots are experimental and are not currently committed due to size.
Stash changes and switch to HEAD on origin/master.
Use 'npm run test-snapshot-update' to generate initial snapshots.
Then run 'npm run test-snapshot' to test against these snapshots.
Currently on ${gitState}.
`;
}
}
// prevent examples from logging
const consoleOriginal = console;
beforeEach(() => { global.console = { log: noop }; });
afterEach(() => { global.console = consoleOriginal; });
// list the examples to test
const examplesExtended = ['constraints'];
const examples = [
'airFriction', 'ballPool', 'bridge', 'broadphase', 'car', 'catapult', 'chains', 'circleStack',
'cloth', 'collisionFiltering', 'compositeManipulation', 'compound', 'compoundStack', 'concave',
'constraints', 'doublePendulum', 'events', 'friction', 'gravity', 'gyro', 'manipulation', 'mixed',
'newtonsCradle', 'ragdoll', 'pyramid', 'raycasting', 'restitution', 'rounded', 'sensors', 'sleeping',
'slingshot', 'softBody', 'sprites', 'stack', 'staticFriction', 'timescale', 'views', 'wreckingBall'
];
// perform integration tests using listed examples
const testName = `Example.%s simulates without throwing${useSnapshots ? ' and matches snapshot' : ''}`;
test.each(examples.map(key => [key]))(testName, exampleName => {
let engine, startSnapshot, endSnapshot;
const simulate = () => {
const example = Example[exampleName]();
const extended = examplesExtended.includes(exampleName);
engine = example.engine;
startSnapshot = Common.engineSnapshot(engine, extended);
for (let i = 0; i < 100; i += 1) {
Matter.Engine.update(engine, 1000 / 60);
}
endSnapshot = Common.engineSnapshot(engine, extended);
};
// simulate and assert nothing is thrown
expect(simulate).not.toThrow();
// assert there has been some change to the world
expect(startSnapshot.world).not.toEqual(endSnapshot.world);
// compare to stored snapshot (experimental)
if (useSnapshots) {
expect(endSnapshot).toMatchSnapshot();
}
}
);

View file

@ -1,206 +0,0 @@
var page = require('webpage').create();
var fs = require('fs');
var Resurrect = require('../lib/resurrect');
var compare = require('fast-json-patch').compare;
var system = require('system');
var demo,
frames = 10,
testUrl = 'http://localhost:8000/demo/index.html',
refsPath = 'test/browser/refs',
diffsPath = 'test/browser/diffs';
var update = arg('--update'),
updateAll = typeof arg('--updateAll') !== 'undefined',
diff = arg('--diff');
var resurrect = new Resurrect({ cleanup: true, revive: false }),
created = [],
changed = [];
var test = function(status) {
if (status === 'fail') {
console.log('failed to load', testUrl);
console.log('check dev server is running!');
console.log('use `grunt dev`');
phantom.exit(1);
return;
}
var demos = page.evaluate(function() {
var demoSelect = document.getElementById('demo-select'),
options = Array.prototype.slice.call(demoSelect);
return options.map(function(o) { return o.value; });
});
fs.removeTree(diffsPath);
if (diff) {
fs.makeDirectory(diffsPath);
}
for (var i = 0; i < demos.length; i += 1) {
demo = demos[i];
var hasChanged = false,
hasCreated = false,
forceUpdate = update === demo || updateAll,
worldStartPath = refsPath + '/' + demo + '/' + demo + '-0.json',
worldEndPath = refsPath + '/' + demo + '/' + demo + '-' + frames + '.json',
worldStartDiffPath = diffsPath + '/' + demo + '/' + demo + '-0.json',
worldEndDiffPath = diffsPath + '/' + demo + '/' + demo + '-' + frames + '.json';
var worldStart = page.evaluate(function(demo) {
if (!(demo in Matter.Example)) {
throw '\'' + demo + '\' is not defined in Matter.Demo';
}
var _demo = Matter.Demo.create(),
engine = Matter.Example.engine(_demo),
runner = Matter.Runner.create();
Matter.Demo._demo = _demo;
_demo.engine = engine;
_demo.engine.render = {};
_demo.engine.render.options = {};
_demo.runner = runner;
_demo.render = { options: {} };
_demo.mouseConstraint = Matter.MouseConstraint.create(engine);
Matter.Demo.reset(_demo);
Matter.Example[demo](_demo);
return engine.world;
}, demo);
var worldEnd = page.evaluate(function(demo, frames) {
var engine = Matter.Demo._demo.engine,
runner = Matter.Demo._demo.runner;
for (var j = 0; j <= frames; j += 1) {
Matter.Runner.tick(runner, engine, j * runner.delta);
}
return engine.world;
}, demo, frames);
worldEnd = JSON.parse(resurrect.stringify(worldEnd, precisionLimiter));
worldStart = JSON.parse(resurrect.stringify(worldStart, precisionLimiter));
if (fs.exists(worldStartPath)) {
var worldStartRef = JSON.parse(fs.read(worldStartPath));
var worldStartDiff = compare(worldStartRef, worldStart);
if (worldStartDiff.length !== 0) {
if (diff) {
fs.write(worldStartDiffPath, JSON.stringify(worldStartDiff, precisionLimiter, 2), 'w');
}
if (forceUpdate) {
hasCreated = true;
fs.write(worldStartPath, JSON.stringify(worldStart, precisionLimiter, 2), 'w');
} else {
hasChanged = true;
}
}
} else {
hasCreated = true;
fs.write(worldStartPath, JSON.stringify(worldStart, precisionLimiter, 2), 'w');
}
if (fs.exists(worldEndPath)) {
var worldEndRef = JSON.parse(fs.read(worldEndPath));
var worldEndDiff = compare(worldEndRef, worldEnd);
if (worldEndDiff.length !== 0) {
if (diff) {
fs.write(worldEndDiffPath, JSON.stringify(worldEndDiff, precisionLimiter, 2), 'w');
}
if (forceUpdate) {
hasCreated = true;
fs.write(worldEndPath, JSON.stringify(worldEnd, precisionLimiter, 2), 'w');
} else {
hasChanged = true;
}
}
} else {
hasCreated = true;
fs.write(worldEndPath, JSON.stringify(worldEnd, precisionLimiter, 2), 'w');
}
if (hasChanged) {
changed.push("'" + demo + "'");
system.stdout.write('x');
} else if (hasCreated) {
created.push("'" + demo + "'");
system.stdout.write('+');
} else {
system.stdout.write('.');
}
}
if (created.length > 0) {
console.log('\nupdated', created.join(', '));
}
var isOk = changed.length === 0 ? 1 : 0;
console.log('');
if (isOk) {
console.log('ok');
} else {
console.log('\nchanges detected on:');
console.log(changed.join(', '));
console.log('\nreview, then --update [name] or --updateAll');
console.log('use --diff for diff log');
}
phantom.exit(!isOk);
};
var precisionLimiter = function(key, value) {
if (typeof value === 'number') {
return parseFloat(value.toFixed(5));
}
return value;
};
function arg(name) {
var index = system.args.indexOf(name);
if (index >= 0) {
return system.args[index + 1] || true;
}
return undefined;
}
page.onError = function(msg, trace) {
setTimeout(function() {
var msgStack = ['testing \'' + demo + '\'', msg];
if (trace && trace.length) {
trace.forEach(function(t) {
msgStack.push(' at ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (fn: ' + t.function +')' : ''));
});
}
console.log(msgStack.join('\n'));
phantom.exit(1);
}, 0);
};
page.onResourceReceived = function(res) {
setTimeout(function() {
if (res.stage === 'end'
&& (res.status !== 304 && res.status !== 200 && res.status !== null)) {
console.log('error', res.status, res.url);
phantom.exit(1);
}
}, 0);
};
phantom.onError = page.onError;
page.open(testUrl, test);

View file

@ -1,542 +0,0 @@
/**
* # 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;
};

View file

@ -1,185 +0,0 @@
var fs = require('fs');
var mkdirp = require('mkdirp').sync;
var removeDir = require('rimraf').sync;
var Resurrect = require('../lib/resurrect');
var compare = require('fast-json-patch').compare;
var path = require('path');
var $ = require('cheerio');
var Matter = require('../../build/matter-dev.js');
Matter.Example = require('../../demo/js/Examples.js');
Matter.Demo = require('../../demo/js/Demo.js');
var demo,
frames = 10,
refsPath = 'test/node/refs',
diffsPath = 'test/node/diffs';
var update = arg('--update'),
updateAll = typeof arg('--updateAll') !== 'undefined',
diff = arg('--diff');
var resurrect = new Resurrect({ cleanup: true, revive: false }),
created = [],
changed = [];
var test = function() {
var demos = getDemoNames();
removeDir(diffsPath);
if (diff) {
mkdirp(diffsPath);
}
for (var i = 0; i < demos.length; i += 1) {
demo = demos[i];
var hasChanged = false,
hasCreated = false,
forceUpdate = update === demo || updateAll,
worldStartPath = refsPath + '/' + demo + '/' + demo + '-0.json',
worldEndPath = refsPath + '/' + demo + '/' + demo + '-' + frames + '.json',
worldStartDiffPath = diffsPath + '/' + demo + '/' + demo + '-0.json',
worldEndDiffPath = diffsPath + '/' + demo + '/' + demo + '-' + frames + '.json';
var _demo = Matter.Demo.create(),
engine = Matter.Example.engine(_demo),
runner = Matter.Runner.create();
_demo.engine = engine;
_demo.engine.render = {};
_demo.engine.render.options = {};
_demo.runner = runner;
_demo.render = { options: {} };
if (!(demo in Matter.Example)) {
throw '\'' + demo + '\' is not defined in Matter.Example';
}
Matter.Demo.reset(_demo);
Matter.Example[demo](_demo);
var worldStart = JSON.parse(resurrect.stringify(engine.world, precisionLimiter));
for (var j = 0; j <= frames; j += 1) {
Matter.Runner.tick(runner, engine, j * runner.delta);
}
var worldEnd = JSON.parse(resurrect.stringify(engine.world, precisionLimiter));
if (fs.existsSync(worldStartPath)) {
var worldStartRef = JSON.parse(fs.readFileSync(worldStartPath));
var worldStartDiff = compare(worldStartRef, worldStart);
if (worldStartDiff.length !== 0) {
if (diff) {
writeFile(worldStartDiffPath, JSON.stringify(worldStartDiff, precisionLimiter, 2));
}
if (forceUpdate) {
hasCreated = true;
writeFile(worldStartPath, JSON.stringify(worldStart, precisionLimiter, 2));
} else {
hasChanged = true;
}
}
} else {
hasCreated = true;
writeFile(worldStartPath, JSON.stringify(worldStart, precisionLimiter, 2));
}
if (fs.existsSync(worldEndPath)) {
var worldEndRef = JSON.parse(fs.readFileSync(worldEndPath));
var worldEndDiff = compare(worldEndRef, worldEnd);
if (worldEndDiff.length !== 0) {
if (diff) {
writeFile(worldEndDiffPath, JSON.stringify(worldEndDiff, precisionLimiter, 2));
}
if (forceUpdate) {
hasCreated = true;
writeFile(worldEndPath, JSON.stringify(worldEnd, precisionLimiter, 2));
} else {
hasChanged = true;
}
}
} else {
hasCreated = true;
writeFile(worldEndPath, JSON.stringify(worldEnd, precisionLimiter, 2));
}
if (hasChanged) {
changed.push("'" + demo + "'");
process.stdout.write('x');
} else if (hasCreated) {
created.push("'" + demo + "'");
process.stdout.write('+');
} else {
process.stdout.write('.');
}
}
if (created.length > 0) {
console.log('\nupdated', created.join(', '));
}
var isOk = changed.length === 0 ? 1 : 0;
console.log('');
if (isOk) {
console.log('ok');
} else {
console.log('\nchanges detected on:');
console.log(changed.join(', '));
console.log('\nreview, then --update [name] or --updateAll');
console.log('use --diff for diff log');
}
setTimeout(function() {
process.exit(!isOk);
}, 100);
};
var precisionLimiter = function(key, value) {
if (typeof value === 'number') {
return parseFloat(value.toFixed(5));
}
return value;
};
function arg(name) {
var index = process.argv.indexOf(name);
if (index >= 0) {
return process.argv[index + 1] || true;
}
return undefined;
}
var getDemoNames = function() {
var demos = [],
skip = [
'terrain', 'svg', 'concave',
'slingshot', 'views', 'raycasting',
'events', 'collisionFiltering', 'sleeping',
'attractors'
];
$('#demo-select option', fs.readFileSync('demo/index.html').toString())
.each(function() {
var name = $(this).val();
if (skip.indexOf(name) === -1) {
demos.push(name);
}
});
return demos;
};
var writeFile = function(filePath, string) {
mkdirp(path.dirname(filePath));
fs.writeFileSync(filePath, string);
};
test();