Move client code to separate project, fix context methods, remove express/axios

Major changes: panic client and server have been separated into two separate projects. This prevents a bunch of overhead if you're installing panic on each machine you're testing against. It also helps with separation of concerns and prevents assumptive tests.

Another major change: express was becoming complicated quick for what I needed. Instead of trying to re-implement websockets over http, I'm switching over to socket.io to allow for easier full duplex data streams.

Many of the patches weren't necessary when client code was removed (like Function.parse, Object.keys, Gun extensions, etc.)
This commit is contained in:
Jesse Gibson
2016-03-08 19:47:01 -07:00
parent 4ed6397658
commit e1fdf1d07b
24 changed files with 168 additions and 666 deletions

View File

@@ -1,38 +0,0 @@
/*jslint node: true*/
'use strict';
// provides `Function.parse`
require('../../lib/configuration/extensions');
function condition(obj) {
// immediate pass
if (obj.conditional === undefined) {
return true;
}
// parse the conditional
var result = Function.parse(obj.conditional);
// if it's a primitive
if (typeof result !== 'function') {
return Boolean(result);
}
// it's a function
return result();
}
module.exports = function (array) {
if (!array) {
return [];
}
return array.filter(condition).map(function (obj) {
// parse the callbacks
obj.cb = Function.parse(obj.cb);
return obj;
}).filter(function (obj) {
// filter out non-functions
return typeof obj.cb === 'function';
});
};

View File

@@ -1,24 +0,0 @@
/*jslint node: true*/
'use strict';
var assign = require('object-assign-deep');
function Context(test) {
this.env = {};
if (test instanceof Object) {
assign(this.env, test.env);
if (typeof test.timeout === 'number') {
this.timeout = test.timeout || this.timeout;
}
}
}
Context.prototype = {
constructor: Context,
// default timeout
timeout: 15000,
done: function () {}
};
module.exports = Context;

View File

@@ -1,9 +0,0 @@
/*jslint node: true*/
'use strict';
var Context = require('./Context');
module.exports = function (test) {
var ctx = new Context(test);
test.cb.call(ctx, ctx, ctx.done);
};

11
dist/index.html vendored
View File

@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>DON'T PANIC (maybe a little)</title>
<meta charset="utf-8">
</head>
<body>
<h1>Serving...</h1>
<script src="panic.min.js"></script>
</body>
</html>

View File

@@ -1,71 +0,0 @@
/*jslint regexp: true, evil: true, node: true*/
/*globals Gun, console*/
'use strict';
var Gun = require('gun/gun');
function keys(obj) {
var key, all = [];
for (key in obj) {
if (obj.hasOwnProperty(key)) {
all.push(key);
}
}
return all;
}
function values(obj) {
return keys(obj).map(function (key) {
return obj[key];
});
}
Object.keys = Object.keys || keys;
Object.values = Object.values || values;
console.log = console.log.bind(console);
Gun.log.squelch = true;
Gun.chain.each = function (cb, end) {
var count, props, gun, n = function () {};
count = 0;
props = [];
gun = this;
cb = cb || n;
end = end || n;
gun.val(function (list) {
var args = Array.prototype.slice.call(arguments);
Gun.is.node(list, function (n, prop) {
count += 1;
props.push(prop);
});
props.forEach(function (prop) {
gun.path(prop).val(function (val, key) {
count -= 1;
cb.apply(this, arguments);
if (!count) {
end.apply(this, args);
}
});
});
});
return gun;
};
Function.prototype.toJSON = Function.prototype.toString;
Function.parse = function (string) {
var val;
eval('val = ' + string);
return val;
};
// export for jasmine tests
module.exports = {
keys: keys,
values: values
};

View File

@@ -91,3 +91,57 @@ test('Client sync', function () {
});
test('Server push', function () {
'use strict';
var responses = 0;
this.ready(function (res) {
responses += 1;
return responses === 5;
});
this.all(function () {
this.env.gun = new Gun('localhost:8080/gun');
});
this.fail('Reason');
this.browser(function () {
this.times(20, function (val) {
return gun.path(val).put({
data: true
});
});
});
this.server(function () {
this.env.gun
});
});
var Context = require('./lib/framework/context');
// if the test succeeds
Context.prototype.success = function () {
var test, total = {};
test = this;
this.done(function (done) {
total[done.peer] = done.status;
var keys = Object.keys(total);
if (test.peers === keys.length) {
var success = !Object.values(total).find(function (win) {
return !win;
});
}
});
};

View File

@@ -5,7 +5,7 @@
"main": "index.js",
"scripts": {
"test": "jasmine",
"start": "node server/server.js",
"start": "node server",
"build": "webpack --watch"
},
"repository": {
@@ -26,11 +26,8 @@
},
"homepage": "https://github.com/PsychoLlama/panic#readme",
"dependencies": {
"axios": "^0.9.1",
"body-parser": "^1.15.0",
"express": "^4.13.3",
"gun": "^0.3.1",
"object-assign-deep": "0.0.4"
"object-assign-deep": "0.0.4",
"socket.io": "^1.4.5"
},
"devDependencies": {
"jasmine": "^2.4.1",

View File

@@ -1,33 +1,14 @@
/*jslint node: true, nomen: true*/
'use strict';
var express = require('express');
var server = express();
var server = require('http').createServer();
var port = process.argv[2] || 8080;
var host = 'localhost';
var path = require('path');
var dist = path.join(__dirname, '../dist');
var parser = require('body-parser').text();
var host = process.argv[3] || 'localhost';
module.exports = {
setup: function (req, res) {
res.status(200).json('No tests running');
},
root: express['static'](dist),
progress: function (req, res) {
res.status(200).json('Recieved');
},
done: function (req, res) {
res.status(200).json('Recieved');
},
port: port,
router: server,
host: host,
// return the full URL
@@ -36,23 +17,4 @@ module.exports = {
}
};
// wrap the middleware to allow dynamic routing
server.use(parser);
server.use(function (req, res, next) {
module.exports.root.apply(this, arguments);
});
server.get('/setup', function (req, res) {
module.exports.setup.apply(this, arguments);
});
server.post('/progress', function (req, res) {
module.exports.progress.apply(this, arguments);
});
server.post('/done', function (req, res) {
module.exports.done.apply(this, arguments);
});
server.listen(port, host);

View File

@@ -1,70 +0,0 @@
/*global jasmine, describe, it, expect*/
/*jslint node: true*/
'use strict';
var parse = require('../../../client/configuration/parse');
function condition(val) {
return {
conditional: val,
cb: 'function () {}'
};
}
describe('The client callback parser', function () {
it('should be a function', function () {
expect(parse).toEqual(jasmine.any(Function));
});
it('should not throw without input', function () {
expect(parse).not.toThrow();
});
it('should return an array', function () {
var output = parse([]);
expect(output).toEqual(jasmine.any(Array));
});
it('should return a parsed objects containing callbacks', function () {
var output = parse([
condition()
]);
expect(output[0]).toEqual(jasmine.any(Object));
});
it('should filter against the conditional method', function () {
var output = parse([
condition('function () { return false }')
]);
expect(output.length).toBe(0);
});
it('should accept expressions as conditionals', function () {
var output = parse([
condition(false),
condition('false'),
condition('typeof true === "boolean"')
]);
expect(output.length).toBe(1);
});
it('should only return a list of objects', function () {
var output = parse([
condition(true),
condition(false),
condition('function () { return true }'),
condition(1)
]);
expect(output.length).toBe(3);
output.forEach(function (cb) {
expect(cb).toEqual(jasmine.any(Object));
});
});
it('should filter out bad input', function () {
var output = parse([{
cb: 5
}]);
expect(output.length).toBe(0);
});
});

View File

@@ -1,57 +0,0 @@
/*globals jasmine, describe, it, expect*/
/*jslint node: true*/
'use strict';
var Context = require('../../../client/framework/Context');
describe('The client context constructor', function () {
it('should be a function', function () {
expect(Context).toEqual(jasmine.any(Function));
});
it('should always create an "env" property', function () {
var context = new Context();
expect(context.env).toEqual(jasmine.any(Object));
});
it('should list "Context" as the constructor', function () {
expect(Context.prototype.constructor).toBe(Context);
});
it('should merge the "env" property with arg0', function () {
var context = new Context({
env: { success: true }
});
expect(context.env.success).toBe(true);
});
it('should have a "done" method', function () {
var context = new Context();
expect(context.done).toEqual(jasmine.any(Function));
});
it('should have a default timeout', function () {
var timeout = Context.prototype.timeout;
expect(timeout).toEqual(jasmine.any(Number));
});
it('should watch for a timeout property', function () {
var context = new Context({
timeout: 42
});
expect(context.timeout).toBe(42);
});
it('should validate timeouts', function () {
var context = new Context({
timeout: 'invalid'
});
expect(context.timeout).toEqual(jasmine.any(Number));
});
it('should protect against null test inputs', function () {
expect(function () {
return new Context(null);
}).not.toThrow();
});
});

View File

@@ -1,53 +0,0 @@
/*globals jasmine, describe, it, expect*/
/*jslint node: true*/
'use strict';
var runner = require('../../../client/framework/runner');
var Context = require('../../../client/framework/Context');
function run(prop, val) {
if (!val) {
val = prop;
prop = 'cb';
}
var obj = {};
obj[prop] = val;
return runner(obj);
}
describe('The client test runner', function () {
it('should be a function', function () {
expect(runner).toEqual(jasmine.any(Function));
});
it('should take a test descriptor and invoke the cb', function (done) {
run(done);
});
it('should expose the "env" in the "this" context', function () {
runner({
env: { success: true },
cb: function () {
expect(this.env.success).toBe(true);
}
});
});
it('should use a context instance as the "this" value', function () {
run(function () {
expect(this).toEqual(jasmine.any(Context));
});
});
it('should send the context as the first cb param', function () {
run(function (ctx) {
expect(ctx).toEqual(jasmine.any(Context));
});
});
it('should pass the "done" function in as the second param', function () {
run(function (ctx, done) {
expect(done).toEqual(jasmine.any(Function));
});
});
});

View File

@@ -2,7 +2,7 @@
/*jslint node: true*/
'use strict';
var Response = require('../../lib/configuration/Response');
var Response = require('../../src/configuration/Response');
describe('The Response constructor', function () {
it('should be a function', function () {

View File

@@ -2,131 +2,37 @@
/*jslint node: true*/
'use strict';
var Gun = require('gun/gun');
require('../../src/configuration/extensions');
describe('The extension', function () {
var polyfill = require('../../lib/configuration/extensions');
describe('keys method', function () {
// cannot delete Object.keys for testing
// node internals depend on it
var keys = polyfill.keys;
it('should be a function', function () {
expect(polyfill.keys).toEqual(jasmine.any(Function));
});
it('should return an array', function () {
expect(keys({})).toEqual(jasmine.any(Array));
});
// more specifically, it will replace it if it doesn't exist
it('should return an array', function () {
expect(keys({
1: 1
}).length).toBe(1);
});
it('should not fail without input', function () {
expect(keys).not.toThrow();
});
it('should survive bad inputs', function () {
keys(null); // shouldn't throw
keys(undefined); // shouldn't throw
keys(0); // shouldn't throw
keys(NaN); // shouldn't throw
keys(Infinity); // shouldn't throw
});
it('should return a list of keys', function () {
expect(keys({
name: true
})[0]).toBe('name');
});
describe('String.random', function () {
it('should be a function', function () {
expect(String.random).toEqual(jasmine.any(Function));
});
describe('values method', function () {
var values = polyfill.values;
it('should be a function', function () {
expect(values).toEqual(jasmine.any(Function));
});
it('should return an array', function () {
expect(values({})).toEqual(jasmine.any(Array));
});
it('should return a list of object values', function () {
expect(values({
1: 5
})[0]).toBe(5);
});
it('should be able to handle no input', function () {
expect(values).not.toThrow();
});
it('should be able to handle bad input', function () {
values(null); // shouldn't throw
values(undefined); // shouldn't throw
values(NaN); // shouldn't throw
values(Infinity); // shouldn't throw
values(0); // shouldn't throw
});
it('should return an empty string without input', function () {
expect(String.random()).toEqual(jasmine.any(String));
});
describe('JSON function support', function () {
it('should allow you to stringify functions', function () {
var result = JSON.stringify(function () {});
expect(result).toBeTruthy();
});
it('should provide a toJSON method', function () {
expect(Function.prototype.toJSON).toEqual(jasmine.any(Function));
});
it('should allow functions anywhere in JSON', function () {
var result = JSON.stringify({
cb: function () {
// do stuff
}
});
result = JSON.parse(result);
expect(result.cb).toBeTruthy();
});
it('should set the length by arg0', function () {
expect(String.random(10).length).toBe(10);
});
describe('parse method', function () {
it('should be a function', function () {
expect(Function.parse).toEqual(jasmine.any(Function));
});
it('should parse stringified functions', function (done) {
var string = JSON.parse(JSON.stringify(function (finished) {
finished();
}));
Function.parse(string)(done);
});
it('should parse anonymous functions', function () {
var func = JSON.parse(JSON.stringify(function () {}));
Function.parse(func); // shouldn't throw
});
it('should parse named functions', function () {
var func = JSON.parse(JSON.stringify(function test() {}));
Function.parse(func); // shouldn't throw
});
it('should return the value if not a function', function () {
expect(Function.parse('true')).toBe(true);
expect(Function.parse(true)).toBe(true);
expect(Function.parse('5')).toBe(5);
});
it('should survive negative input', function () {
expect(String.random(-5).length).toBe(0);
});
describe('gun.each method', function () {
it('should be a function', function () {
expect(Gun.chain.each).toEqual(jasmine.any(Function));
});
it('should default to 10 chars long', function () {
expect(String.random().length).toBe(10);
});
});
describe('function.toJSON', function () {
var proto = Function.prototype;
it('should be a function', function () {
expect(proto.toJSON).toEqual(jasmine.any(Function));
});
it('should just be (function).toString', function () {
expect(proto.toJSON).toBe(proto.toString);
});
});

View File

@@ -1,26 +1,72 @@
/*global jasmine, describe, it, expect*/
/*global jasmine, describe, it, expect, beforeEach*/
/*jslint node: true*/
'use strict';
var Context = require('../../lib/framework/Context');
var Context = require('../../src/framework/Context');
describe('The Context constructor', function () {
var chain = Context.prototype;
var ctx, proto = Context.prototype;
beforeEach(function () {
ctx = new Context();
});
it('should be a function', function () {
expect(Context).toEqual(jasmine.any(Function));
});
it('should have a "client" method', function () {
expect(chain.client).toEqual(jasmine.any(Function));
});
it('should have a "server" method', function () {
expect(chain.server).toEqual(jasmine.any(Function));
expect(proto.server).toEqual(jasmine.any(Function));
});
it('should have a "config" property', function () {
var ctx = new Context();
expect(ctx.config).toEqual(jasmine.any(Object));
});
it('should have a "cbs" array in the config', function () {
expect(ctx.config.cbs).toEqual(jasmine.any(Array));
});
describe('"client" method', function () {
var cbs;
beforeEach(function () {
ctx.client(function () {});
cbs = ctx.config.cbs;
});
it('should be a function', function () {
expect(proto.client).toEqual(jasmine.any(Function));
});
it('should push to the "cbs" array', function () {
expect(cbs.length).toBe(1);
});
it('should create a test descriptor', function () {
expect(cbs[0]).toEqual(jasmine.any(Object));
});
it('should create a test conditional', function () {
expect(cbs[0].conditional).toBeTruthy();
});
});
describe('"server" method', function () {
var cbs;
beforeEach(function () {
ctx.server(function () {});
cbs = ctx.config.cbs;
});
it('should be a function', function () {
expect(ctx.server).toEqual(jasmine.any(Function));
});
it('should push to the "cbs" array', function () {
expect(cbs[0]).toEqual(jasmine.any(Object));
});
it('should create a test conditional', function () {
expect(cbs[0].conditional).toEqual(jasmine.any(String));
});
});
});

View File

@@ -4,8 +4,8 @@
// should import test
var test = require('../../lib');
var Context = require('../../lib/framework/Context');
var test = require('../../src');
var Context = require('../../src/framework/Context');
describe('The test function', function () {
it('should be a function', function () {

View File

@@ -1,42 +0,0 @@
/*global jasmine, describe, it, expect*/
/*jslint node: true*/
'use strict';
var bump = require('../../server/bump');
var route = require('../../server/index');
var event = require('../../server/events');
var axios = require('axios');
var root = String(route);
describe('The bump function', function () {
it('should be a function', function () {
expect(bump).toEqual(jasmine.any(Function));
});
it('should update the "setup" route', function () {
var original = route.setup;
bump();
expect(route.setup).not.toBe(original);
});
it("should serve it's arg on the `setup` route", function (done) {
bump({ success: true });
axios.get(root + 'setup').then(function (res) {
expect(res.data.success).toBe(true);
done();
});
});
it('should emit the "begin" event', function (done) {
event.on('begin', done);
bump({});
});
it('should provide the context to the "begin" cb', function () {
var obj = {};
event.on('begin', function (ctx) {
expect(ctx).toBe(obj);
});
bump(obj);
});
});

View File

@@ -1,92 +0,0 @@
/*global jasmine, describe, it, expect*/
/*jslint node: true*/
'use strict';
var route = require('../../server/index');
var axios = require('axios');
describe('The router', function () {
var root = String(route);
it('should have a toString property', function () {
var stringy = route.hasOwnProperty('toString');
expect(stringy).toBe(true);
});
it('should export the current port', function () {
expect(route.port).toEqual(jasmine.any(Number));
});
it('should export the hostname', function () {
expect(route.host).toEqual(jasmine.any(String));
});
it('should return the full URL when `toString`ed', function () {
expect(route.toString()).toMatch(/http/i);
});
it('should export middleware methods', function () {
expect(route.root).toEqual(jasmine.any(Function));
expect(route.setup).toEqual(jasmine.any(Function));
expect(route.progress).toEqual(jasmine.any(Function));
expect(route.done).toEqual(jasmine.any(Function));
});
it('should always respond', function (done) {
var num = 0;
// Count responses. There should be 4 total
function req(type, url) {
axios[type](root + url).then(function () {
if ((num += 1) === 4) {
done();
}
});
}
req('get', '');
req('get', 'setup');
req('post', 'progress');
req('post', 'done');
});
it('should serve the homepage on root request', function (done) {
axios.get(root).then(function (res) {
expect(res.data).toMatch(/html/i);
done();
});
});
it('should expose the router interface', function () {
expect(route.router).toEqual(jasmine.any(Function));
});
it('should hot-swap routes', function (done) {
// root needs to pass off to the other functions
// with the "next" method
route.root = function (req, res, next) {
done();
next();
};
axios.get(root);
});
it('should listen for "progress" updates', function (done) {
route.progress = done;
axios.post(root + 'progress');
});
it('should listen for "done" updates', function (done) {
route.done = done;
axios.post(root + 'done');
});
it('should pass the express args through', function (done) {
route.root = function (req, res, next) {
expect(req).toEqual(jasmine.any(Object));
expect(res).toEqual(jasmine.any(Object));
expect(next).toEqual(jasmine.any(Function));
done();
};
axios.get(root);
});
});

View File

@@ -4,7 +4,6 @@
// deeply merge objects
var assign = require('object-assign-deep');
var defaults = require('./defaults');
var Gun = require('gun/gun');
/*
This is the test configuration constructor.
@@ -13,7 +12,7 @@ var Gun = require('gun/gun');
dynamically by "Context.js".
*/
function Response(obj) {
this.testID = Gun.text.random();
this.testID = String.random();
// provide defaults
assign(this, defaults);

View File

@@ -0,0 +1,22 @@
/*jslint regexp: true, node: true*/
'use strict';
console.log = console.log.bind(console);
Function.prototype.toJSON = Function.prototype.toString;
String.random = function (length) {
length = length || 10;
if (length < 0) {
return '';
}
var str, space = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
space += 'abcdefghijklmnopqrstuvwxyz';
space += '1234567890';
str = '';
while (length) {
str += space[Math.floor(Math.random() * space.length)];
length -= 1;
}
return str;
};

View File

@@ -12,16 +12,17 @@ Context.prototype = {
constructor: Context,
server: function (cb, args) {
this.config.cbs.push(cb);
this.config.cbs.push({
conditional: 'typeof global !== "undefined"',
cb: cb
});
return this;
},
client: function (cb, args) {
this.config.cbs.push({
args: args,
condition: function () {
return typeof window !== 'undefined';
},
conditional: "typeof window !== 'undefined'",
cb: cb
});
}

View File

@@ -1,18 +0,0 @@
/*jslint node: true, nomen: true*/
var path = require('path');
var webpack = require('webpack');
module.exports = {
context: path.join(__dirname, 'client'),
entry: './index.js',
devtool: 'source-map',
output: {
path: path.join(__dirname, 'dist'),
filename: 'panic.min.js'
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
minimize: true
})
]
};