halley
Version:
A bayeux client for modern browsers and node. Forked from Faye
560 lines (482 loc) • 14.1 kB
JavaScript
'use strict';
var StateMachineMixin = require('../lib/mixins/statemachine-mixin');
var assert = require('assert');
var extend = require('../lib/util/externals').extend;
var Promise = require('bluebird');
describe('statemachine-mixin', function() {
describe('normal flow', function() {
beforeEach(function() {
var TEST_FSM = {
name: "test",
initial: "A",
transitions: {
A: {
t1: "B"
},
B: {
t2: "C"
},
C: {
t3: "A"
}
}
};
function TestMachine() {
this.initStateMachine(TEST_FSM);
}
TestMachine.prototype = {
};
extend(TestMachine.prototype, StateMachineMixin);
this.testMachine = new TestMachine();
});
it('should transition', function() {
return this.testMachine.transitionState('t1')
.bind(this)
.then(function() {
assert(this.testMachine.stateIs('B'));
});
});
it('should serialize transitions', function() {
return Promise.all([
this.testMachine.transitionState('t1'),
this.testMachine.transitionState('t2')
])
.bind(this)
.then(function() {
assert(this.testMachine.stateIs('C'));
});
});
it('should handle optional transitions', function() {
return this.testMachine.transitionState('doesnotexist', { optional: true })
.bind(this)
.then(function() {
assert(this.testMachine.stateIs('A'));
});
});
it('should reject invalid transitions', function() {
return this.testMachine.transitionState('doesnotexist')
.bind(this)
.then(function() {
assert.ok(false);
}, function(err) {
assert.strictEqual(err.message, 'Unable to perform transition doesnotexist from state A');
});
});
it('should proceed with queued transitions after a transition has failed', function() {
return Promise.all([
this.testMachine.transitionState('doesnotexist'),
this.testMachine.transitionState('t1'),
].map(function(p) { return p.reflect(); }))
.bind(this)
.spread(function(p1, p2) {
assert(p1.isRejected());
assert(p2.isFulfilled());
assert(this.testMachine.stateIs('B'));
});
});
});
describe('automatic transitioning', function() {
beforeEach(function() {
var TEST_FSM = {
name: "test",
initial: "A",
transitions: {
A: {
t1: "B",
t2: "C"
},
B: {
t3: "C"
},
C: {
t4: "A"
}
}
};
function TestMachine() {
this.initStateMachine(TEST_FSM);
}
TestMachine.prototype = {
_onEnterA: function() {
return 't1';
},
_onEnterB: function() {
return 't3';
},
_onEnterC: function() {
}
};
extend(TestMachine.prototype, StateMachineMixin);
this.testMachine = new TestMachine();
});
it('should transition', function() {
return this.testMachine.transitionState('t1')
.bind(this)
.then(function() {
assert(this.testMachine.stateIs('C'));
});
});
it.skip('should reject on state transitions', function() {
return Promise.all([
this.testMachine.waitForState({
rejected: 'B',
fulfilled: 'C'
}),
this.testMachine.transitionState('t1')
])
.bind(this)
.then(function() {
assert.ok(false);
}, function(err) {
assert.strictEqual(err.message, 'State is B');
});
});
it.skip('should wait for state transitions when already in the state', function() {
return this.testMachine.waitForState({
fulfilled: 'A'
})
.bind(this)
.then(function() {
assert(this.testMachine.stateIs('A'));
});
});
it.skip('should reject state transitions when already in the state', function() {
return this.testMachine.waitForState({
fulfilled: 'C',
rejected: 'A'
})
.bind(this)
.then(function() {
assert.ok(false);
}, function() {
assert.ok(true);
});
});
it.skip('should timeout waiting for state transitions', function() {
return this.testMachine.waitForState({
fulfilled: 'C',
rejected: 'B',
timeout: 1
})
.bind(this)
.then(function() {
assert.ok(false);
}, function(err) {
assert.strictEqual(err.message, 'Timeout waiting for state C');
});
});
});
describe('error handling', function() {
beforeEach(function() {
var TEST_FSM = {
name: "test",
initial: "A",
transitions: {
A: {
t1: "B",
t4: "C",
t6: 'FAIL_ON_ENTER'
},
B: {
t2: "C",
error: 'D'
},
C: {
t5: 'E'
},
D: {
t3: "E"
},
E: {
},
FAIL_ON_ENTER: {
}
}
};
function TestMachine() {
this.initStateMachine(TEST_FSM);
}
TestMachine.prototype = {
_onLeaveC: function() {
},
_onEnterB: function() {
throw new Error('Failed to enter B');
},
_onEnterFAIL_ON_ENTER: function() {
throw new Error('Failed on enter');
}
};
extend(TestMachine.prototype, StateMachineMixin);
this.testMachine = new TestMachine();
});
it('should handle errors on transition', function() {
return this.testMachine.transitionState('t1')
.bind(this)
.then(function() {
assert(this.testMachine.stateIs('D'));
});
});
it('should transition to error state before other queued transitions', function() {
return Promise.all([
this.testMachine.transitionState('t1'),
this.testMachine.transitionState('t3'),
].map(function(p) { return p.reflect(); }))
.bind(this)
.spread(function(p1, p2) {
assert(p1.isFulfilled());
assert(p2.isFulfilled());
assert(this.testMachine.stateIs('E'));
});
});
it('should throw the original error if the state does not have an error transition', function() {
return this.testMachine.transitionState('t6')
.bind(this)
.then(function() {
assert.ok(false);
}, function(err) {
assert.strictEqual(err.message, 'Failed on enter');
});
});
});
describe('dedup', function() {
beforeEach(function() {
var TEST_FSM = {
name: "test",
initial: "A",
transitions: {
A: {
t1: "B"
},
B: {
t1: "C"
},
C: {
}
}
};
function TestMachine() {
this.initStateMachine(TEST_FSM);
}
TestMachine.prototype = {
_onEnterB: function() {
this.bCount = this.bCount ? this.bCount + 1 : 1;
return Promise.delay(1);
},
_onEnterC: function() {
return Promise.delay(1);
}
};
extend(TestMachine.prototype, StateMachineMixin);
this.testMachine = new TestMachine();
});
it('should transition with dedup', function() {
return Promise.all([
this.testMachine.transitionState('t1'),
this.testMachine.transitionState('t1', { dedup: true }),
])
.bind(this)
.then(function() {
assert.strictEqual(this.testMachine.bCount, 1);
assert(this.testMachine.stateIs('B'));
});
});
it('should clearup pending transitions', function() {
return this.testMachine.transitionState('t1')
.bind(this)
.then(function() {
assert.strictEqual(this.testMachine.bCount, 1);
assert(this.testMachine.stateIs('B'));
assert.deepEqual(this.testMachine._pendingTransitions, {});
});
});
it('should transition with dedup followed by non-dedup', function() {
return Promise.all([
this.testMachine.transitionState('t1'),
this.testMachine.transitionState('t1', { dedup: true }),
this.testMachine.transitionState('t1'),
])
.bind(this)
.then(function() {
assert(this.testMachine.stateIs('C'));
assert.deepEqual(this.testMachine._pendingTransitions, {});
});
});
it('should dedup against the first pending transition', function() {
var p1 = this.testMachine.transitionState('t1');
var p2 = this.testMachine.transitionState('t1');
var p3 = this.testMachine.transitionState('t1', { dedup: true })
.bind(this)
.then(function() {
assert(p1.isFulfilled());
assert(p2.isPending());
});
return Promise.all([p1, p2, p3])
.bind(this)
.then(function() {
assert(this.testMachine.stateIs('C'));
assert.deepEqual(this.testMachine._pendingTransitions, {});
});
});
});
describe('cancellation', function() {
beforeEach(function() {
var TEST_FSM = {
name: "test",
initial: "A",
transitions: {
A: {
t1: "B",
},
B: {
t2: "C",
},
C: {
t3: "D"
},
D: {
}
}
};
function TestMachine() {
this.count = 0;
this.initStateMachine(TEST_FSM);
}
TestMachine.prototype = {
_onEnterB: function() {
return Promise.delay(1);
},
_onEnterC: function() {
return Promise.delay(5)
.bind(this)
.then(function() {
this.count++;
});
}
};
extend(TestMachine.prototype, StateMachineMixin);
this.testMachine = new TestMachine();
});
it('should handle cancellations before they execute', function() {
var p1 = this.testMachine.transitionState('t1');
var p2 = this.testMachine.transitionState('t2');
p2.cancel();
return Promise.delay(5)
.bind(this)
.then(function() {
assert(!p1.isCancelled());
return p1;
})
.then(function() {
assert(this.testMachine.stateIs('B'));
});
});
it('should not cancel transitions after they start', function() {
var p;
return this.testMachine.transitionState('t1')
.bind(this)
.then(function() {
p = this.testMachine.transitionState('t2');
return Promise.delay(1);
})
.then(function() {
assert(p.isCancellable());
p.cancel();
return Promise.delay(5);
})
.then(function() {
assert.strictEqual(this.testMachine.count, 1);
assert(this.testMachine.stateIs('C'));
});
});
});
describe('resetTransition', function() {
beforeEach(function() {
var self = this;
var TEST_FSM = {
name: "test",
initial: "A",
globalTransitions: {
disable: "DISABLED"
},
transitions: {
A: {
t1: "B",
t2: "C",
t5: "D"
},
B: {
t3: "C"
},
C: {
t4: "B"
},
D: {
t6: "A"
},
DISABLED: {
}
}
};
function TestMachine() {
this.initStateMachine(TEST_FSM);
}
TestMachine.prototype = {
_onLeaveA: function() {
self.leaveACount++;
},
_onEnterB: function() {
self.enterBCount++;
return Promise.delay(1, 't3');
},
_onLeaveB: function() {
self.leaveBCount++;
},
_onEnterC: function() {
self.enterCCount++;
return Promise.delay(1, 't4');
}
};
extend(TestMachine.prototype, StateMachineMixin);
this.testMachine = new TestMachine();
this.leaveACount = 0;
this.enterBCount = 0;
this.leaveBCount = 0;
this.enterCCount = 0;
});
it('should transition', function() {
var promise = this.testMachine.transitionState('t1');
promise.catch(function() {}); // Prevent warnings here
var resetReason = new Error('We need to reset');
return this.testMachine.resetTransition('disable', resetReason)
.bind(this)
.then(function() {
assert.strictEqual(this.leaveACount, 1);
assert.strictEqual(this.enterBCount, 1);
assert.strictEqual(this.leaveBCount, 1);
assert.strictEqual(this.enterCCount, 0);
assert(this.testMachine.stateIs('DISABLED'));
return promise; // Ensure the original transition completed
})
.then(function() {
assert.ok(false);
}, function(err) {
assert.strictEqual(err, resetReason);
});
});
it('should not cancel the first transition', function() {
var promise1 = this.testMachine.transitionState('t5');
var promise2 = this.testMachine.transitionState('t5');
var promise3 = this.testMachine.transitionState('tXXX');
var resetReason = new Error('We need to reset');
return this.testMachine.resetTransition('disable', resetReason)
.bind(this)
.then(function() {
assert.strictEqual(this.leaveACount, 1);
assert(this.testMachine.stateIs('DISABLED'));
assert(promise1.isFulfilled());
assert(promise2.reason(), resetReason);
assert(promise3.reason(), resetReason);
});
});
});
});