Skip to content

Commit

Permalink
changed example tests to use jest
Browse files Browse the repository at this point in the history
  • Loading branch information
liabru committed Sep 14, 2019
1 parent 3c32969 commit 104d319
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 936 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ examples/build
test/browser/diffs
test/browser/refs
test/node/diffs
test/node/refs
test/node/refs
__snapshots__
8 changes: 6 additions & 2 deletions Gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
118 changes: 118 additions & 0 deletions test/Common.js
Original file line number Diff line number Diff line change
@@ -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 changes: 96 additions & 0 deletions test/Examples.spec.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
);
Loading

0 comments on commit 104d319

Please sign in to comment.