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:
parent
3c32969da2
commit
104d31902c
8 changed files with 225 additions and 936 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -12,4 +12,5 @@ examples/build
|
|||
test/browser/diffs
|
||||
test/browser/refs
|
||||
test/node/diffs
|
||||
test/node/refs
|
||||
test/node/refs
|
||||
__snapshots__
|
|
@ -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'));
|
||||
|
||||
|
|
|
@ -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
118
test/Common.js
Normal 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
96
test/Examples.spec.js
Normal 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();
|
||||
}
|
||||
}
|
||||
);
|
|
@ -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);
|
|
@ -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;
|
||||
};
|
|
@ -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();
|
Loading…
Add table
Reference in a new issue