action-graph
action-graph is a tool for automating complex tasks efficiently and predictably.
It helps you structure units of work into atomic actions that promote reuse and predictability. Born to improve integration testing it's suitable for other structured tasks, like build and deploy tooling.

Install
$ npm install --save action-graph
The idea
Actions have dependencies. The dependencies are also actions, which in turn can have their own dependencies. For example, an action to "send a message" might need to first "login", and before you can "login" you need to "open the website".
With action graph, you'd specify this with three actions: SendAMessage
, Login
and OpenTheWebsite
. SendAMessage
would depend on Login
, and Login
would depend on OpenTheWebsite
...
OpenTheWebsite
|
Login
|
SendAMessage
If you run the SendAMessage
action, action-graph will automatically run OpenTheWebsite
and Login
first.
If you then wanted to test "delete a message" it would also depend on "login" and "open the website". You'd create a new action, DeleteAMessage
, and have it depend on Login
. Your graph now looks like...
OpenTheWebsite
|
Login
/ \
SendAMessage DeleteAMessage
Running DeleteAMessage
would cause OpenAWebsite
and Login
to run, but not SendAMessage
. This is action-graph's special sauce — it will figure out the minimum amount of work required to run an action, even with very complicated (or repeated) dependencies.
A simple example
Here's an action with no dependencies.
import { createClass } from 'action-graph';
const SayHello = createClass({
run(state) {
console.log("Hello, world!");
return state;
}
});
Example use
Here's an example integration test suite that will get you familiar with action-graph's API and use.
import {
createClass,
run
} from 'action-graph';
const Type = createClass({
getDefaultProps: function () {
return {
selector: undefined,
text: undefined
};
},
getDescription: function () {
return 'type ' + this.props.text + ' into ' + this.props.selector;
},
run: function (state) {
return this.context.session
.findDisplayedByCssSelector(this.props.selector)
.then(elem => elem.type(this.props.text))
.then(() => state);
}
});
const Click = createClass({ ... });
const OpenUrl = createClass({
getDefaultProps() {
return {
url: 'http://localhost:9000',
expectedTitle: ''
};
},
getDescription() {
const { props } = this;
return `Open ${props.get('url')}`;
},
run(state) {
const { props, context: { session } } = this;
return session
.get(props.url)
.then(() => session.getPageTitle())
.then(title => {
if (title !== props.expectedTitle) {
throw new Error('Title was not as expected');
}
})
.then(() => state.set('currentUrl', props.url));
},
teardown() {}
});
const SendAMessage = createClass({
getDependencies() {
const { props } = this;
return [
new OpenUrl({
url: 'https://your.app',
expectedTitle: 'My App'
}),
new Click({
selector: '.new'
}),
new Type({
selector: '.subject',
text: 'The subject'
}),
new Type({
selector: 'textarea',
text: 'The message'
}),
new Click({
selector: '.send'
})
];
}
});
run(
SendAMessage,
{ session: getSession() },
{}
);
License
MIT