Skip to content

Commit

Permalink
Adds a full test suite
Browse files Browse the repository at this point in the history
Also includes pertinent changes to make the tests pass :-)
  • Loading branch information
hlapp committed Feb 27, 2017
1 parent 5d42dd8 commit 8189942
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 11 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ jspm_packages

# Optional REPL history
.node_repl_history

# Node-RED artifacts
test/.config.json
test/.node-red
30 changes: 20 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,38 @@ function waitNodesStarted(RED, timeout, result) {
}

var REDstart = module.exports = function(RED, timeout, result) {
let genFunc = true;
if (arguments.length === 1 && (RED === undefined || ! RED.start)) {
if (arguments.length > 0) result = arguments[arguments.length-1];
let genFunc = arguments.length <= 2;
// is the first argument not RED?
if (RED === undefined || ! RED.start) {
// as a default, load RED object from modules
RED = require('node-red');
genFunc = false;
// if there is only one argument but it isn't RED, assume we are being
// used directly as the wait function
if (arguments.length === 1) genFunc = false;
}
if (arguments.length < 3 || timeout === undefined) timeout = FLOWS_TIMEOUT;
// if we are being used directly, the last argument is the result value to be passed through
if (arguments.length > 0) result = arguments[arguments.length-1];
// timeout can only be provided in the 2 and 3-argument form
if (arguments.length < 2 || timeout === undefined) timeout = FLOWS_TIMEOUT;
if (genFunc) {
return function(value) { waitNodesStarted(RED, timeout, value) };
return function(value) { return waitNodesStarted(RED, timeout, value) };
}
return waitNodesStarted(RED, timeout, result);
};

var injected = new Map();

REDstart.inject = function(RED, timeout) {
if (! RED) RED = require('node-red');
let start = RED.start;
// injected previously already?
if (injected.get(RED) === start) return;
// if not, inject ourselves
RED.start = injectedStart;
injected.set(RED, injectedStart);

// the function we will be injecting
function injectedStart() {
return start(arguments).then(REDstart(RED, timeout));
}
// injected previously already?
if (start === injectedStart) return;
// if not, inject ourselves
RED.start = injectedStart;
};
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "Enables waiting for flows to have started for embedded Node-RED applications",
"main": "index.js",
"scripts": {
"test": "tape test/**.js",
"test": "tape test/01.js && tape test/02.js",
"lint": "eslint .",
"pretest": "npm run lint"
},
Expand All @@ -23,8 +23,10 @@
},
"homepage": "https://github.com/hlapp/node-red-embedded-start#readme",
"devDependencies": {
"delay": "^1.3.1",
"eslint": "^3.16.1",
"node-red": "^0.16.2",
"sinon": "^1.17.7",
"tape": "^4.6.3"
}
}
6 changes: 6 additions & 0 deletions test/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"rules": {
"no-console": "off",
"no-process-env": "off"
}
}
197 changes: 197 additions & 0 deletions test/01.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
"use strict";

var test = require('tape'),
sinon = require('sinon');
var path = require('path');
var embeddedStart = require('../');

const REDsettings = {
httpAdminRoot: false,
httpNodeRoot: false,
functionGlobalContext: {},
disableEditor: true,
userDir: path.join(__dirname, '.node-red'),
logging: {
console: {
level: process.env.NODE_RED_LOGLEVEL || "info"
}
}
};

var RED;
var REDevents;
var testFlow;

function failAndEnd(t) {
return (err) => {
t.fail(err);
t.end();
};
}

test.onFinish(function() {

function closeUp() {
let prom = testFlow ? RED.nodes.removeFlow(testFlow) : Promise.resolve();
if (RED) {
prom = prom.then(() => RED.stop());
}
prom.catch((e) => {
console.error("stopping Node-RED failed:");
console.error(e.stack ? e.stack : e);
});
}

closeUp();
});

test('can create and initialize Node-RED runtime', function(t) {
t.plan(3);

t.doesNotThrow(() => {
RED = require('node-red');
}, undefined, 'instantiates Node-RED runtime without error');
t.doesNotThrow(RED.init.bind(RED, undefined, REDsettings),
undefined,
'initializes Node-RED runtime without error');
t.doesNotThrow(() => {
REDevents = require('node-red/red/runtime/events');
}, undefined, 'loads RED events object without erorr');
});

test('can be used to generate wait function', function(t) {
t.plan(3);

let f;
t.timeoutAfter(20);
f = embeddedStart();
t.equal(typeof f, "function", "generates function with default runtime and timeout");
f = embeddedStart(RED);
t.equal(typeof f, "function", "generates function with default timeout");
// ensure that the following don't result in executing a wait function
f = embeddedStart(RED, 10000);
t.equal(typeof f, "function", "generates function with given timeout");
});

test('can be used to promise waiting for \'nodes-started\'', function(t) {

let REDresult = {}, spy = sinon.spy();

function testWaiting(p, tt) {
tt.ok(p instanceof Promise, 'returns promise if waiting');
spy.reset();
p.then((result) => {
spy();
tt.pass('resolves once nodes-started event fires');
tt.equal(result, REDresult, 'passes result through');
}).catch(failAndEnd(tt));
setTimeout(() => {
tt.ok(spy.notCalled, 'does not resolve before node-started event fires');
REDevents.emit('nodes-started');
}, 10);
}

t.test('using defaults', function(subtest) {
subtest.plan(4);
subtest.timeoutAfter(40);
testWaiting(embeddedStart(REDresult), subtest);
});
t.test('using full signature', function(subtest) {
subtest.plan(4);
subtest.timeoutAfter(40);
testWaiting(embeddedStart(RED, 30, REDresult), subtest);
});
t.end();
});

test('generated function promises to wait for \'nodes-started\'', function(t) {
t.plan(4);

let REDresult = {}, spy = sinon.spy();
t.timeoutAfter(30);
let waitFunc = embeddedStart(RED);
let p = waitFunc(REDresult);
t.ok(p instanceof Promise, 'returns promise if waiting');
p.then((result) => {
spy();
t.pass('resolves once nodes-started event fires');
t.equal(result, REDresult, 'passes result through');
}).catch(failAndEnd(t));
setTimeout(() => {
t.ok(spy.notCalled, 'does not resolve before node-started event fires');
REDevents.emit('nodes-started');
}, 10);
});

test('generated function rejects if waiting times out', function(t) {
t.plan(5);

let REDresult = {}, spy = sinon.spy();
t.timeoutAfter(30);
let waitFunc = embeddedStart(RED, 20);
let p = waitFunc(REDresult);
t.ok(p instanceof Promise, 'returns promise if waiting');
p.then(() => {
t.fail('resolves despite timeout once nodes-started event fires');
t.skip('therefore no error object either');
}).catch((err) => {
spy();
t.pass('returned promise rejects if timeout before event fires');
t.ok(err instanceof Error, 'rejects with an Error object');
});
setTimeout(() => {
t.ok(spy.notCalled, 'does not resolve before node-started event fires');
}, 10);
setTimeout(() => {
t.ok(spy.calledOnce, 'has rejected due to timeout before event');
REDevents.emit('nodes-started');
}, 25);
});

test('waiting for \'nodes-started\' results in flow API ready', function(t) {
t.plan(6);

let flow = {
label: "Test Flow",
nodes: [{
id: RED.util.generateId(),
type: 'comment',
name: 'test',
wires: []
}]
};

function addFlow(tt, expectSuccess) {
let pass = (expectSuccess ? tt.pass : tt.fail).bind(tt);
let fail = (expectSuccess ? tt.fail : tt.pass).bind(tt);

return RED.nodes.addFlow(flow).then((id) => {
pass('successfully created flow');
if (id) {
pass(`created a valid flow ID (${id})`);
} else {
fail(`created flow ID is not valid (${id})`);
}
testFlow = id;
}).catch((err) => {
// maintain the same number of assertions, but fail them all
fail('addFlow() rejected with eror ' + err);
fail('hence, no valid flow ID either');
});
}

RED.start().then((result) => {
t.comment('before waiting for \'nodes-started\':');
t.notOk(RED.nodes.getFlows(), 'getFlows() returns null');
t.throws(() => addFlow(t, false),
TypeError,
'addFlow() throws internal TypeError');
return embeddedStart(RED, 5000, result);
}).then(() => {
t.comment('after waiting for \'nodes-started\':');
t.ok(RED.nodes.getFlows(), 'getFlows() now returns a valid value');
t.doesNotThrow(() => addFlow(t, true),
undefined,
'addFlow() now returns without error');
}).catch(failAndEnd(t));
});
90 changes: 90 additions & 0 deletions test/02.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use strict";

var test = require('tape'),
delay = require('delay');
var path = require('path');
var embeddedStart = require('../');

const REDsettings = {
httpAdminRoot: false,
httpNodeRoot: false,
functionGlobalContext: {},
disableEditor: true,
userDir: path.join(__dirname, '.node-red'),
logging: {
console: {
level: process.env.NODE_RED_LOGLEVEL || "info"
}
}
};

var RED;
var REDstart;
var testFlow;

function failAndEnd(t) {
return (err) => {
t.fail(err);
t.end();
};
}

test.onFinish(function() {

function closeUp() {
let prom = testFlow ? RED.nodes.removeFlow(testFlow) : Promise.resolve();
if (RED) {
prom = prom.then(() => RED.stop());
}
prom.catch((e) => {
console.error("stopping Node-RED failed:");
console.error(e.stack ? e.stack : e);
});
}

if (REDstart && RED.start !== REDstart) RED.start = REDstart;
closeUp();
});

RED = require('node-red');
RED.init(undefined, REDsettings);

test('can inject wait into RED.start()', function(t) {
t.plan(6);

let flow = {
label: "Test Flow",
nodes: [{
id: RED.util.generateId(),
type: 'comment',
name: 'test',
wires: []
}]
};

function addFlow() {
return RED.nodes.addFlow(flow).then((id) => {
testFlow = id;
t.ok(id, `creates valid flow ID (${id})`);
});
}

REDstart = RED.start;
embeddedStart.inject(RED);
let ourStart = RED.start;
t.notEqual(ourStart, REDstart, 'replaces RED.start with ours');
embeddedStart.inject(RED);
t.equal(RED.start, ourStart, 'repeating injection does not change this');
RED.start = REDstart;
embeddedStart.inject(RED);
t.notEqual(RED.start, REDstart, 'injects again if RED.start isn\'t ours');

RED.stop().then(delay(1000)).then(() => RED.start()).then(() => {
t.ok(RED.nodes.getFlows(), 'getFlows() returns a valid value right away');
let prom;
t.doesNotThrow(() => { prom = addFlow() },
undefined,
'addFlow() returns without error');
return prom;
}).catch(failAndEnd(t));
});

0 comments on commit 8189942

Please sign in to comment.