Internal Flows
In this section it is described how the different components (Ability, Action, Task and Question) technically play together during the execution flow.
We broke it down based on weather an Ability got aliased or not.
General flow without aliasing of abilities
Assigning of Ability to Actor
Actor.can(Ability.using(SETTINGS));
Puts an instance of an Ability into the Actors internal ability map.
Triggering of actions/tasks
await Actor.attemptsTo(
Action.execute(),
);
Actors attemptsTo method triggers the Action internal performAs method with the given Actor.
Action.performAs(actor: Actor): Promise<T> {
const ability = await Ability.as(actor);
// ... now ability functionality can be used
return ability.doSomething();
};
The Actions performAs method internally calls the Ability's "as" method to get the instanciated instance from the Actors ability map.
Ability.as(actor: IActor): IAbility {
return actor.withAbilityTo(this);
};
The Actors withAbilityTo method actually gets the instance from the map.
The flow for Tasks is identical.
Triggering of questions
Questions per se follow the same flow as actions/tasks. The only difference are the names of internal methods. So is attemptsTo substituted with asks and performAs with answeredBy.
await Actor.asks(
Question.toBe.truthy(),
);
Question.answeredBy(actor: Actor): Promise<T> {
const ability = await Ability.as(actor);
// ... now ability functionality can be used
expect(ability.doSomething()).toBe(true);
return true;
};
Special flow with aliased ability
It happens that there is the need to make use of the same ability but with different settings. A usecase for that could be to invoke 2 different APIs with different base URLs and authentication tokens.
Assigning of Abilities to Actor
Actor
.can(Ability.using(SETTINGS_1))
.can(Ability.using(SETTINGS_2).withAlias('aliased'));
Puts 2 instances of an Ability with different configuration into the Actors internal ability map. The first one is the default without alias, the second one gets an alias.
Triggering of actions
await Actor.attemptsTo(
// trigger action with default ability
Action.execute(),
// trigger action with aliased ability
Action.execute().withAbilityAlias('aliased'),
);
Actors attemptsTo method triggers the Action internal performAs method with the given Actor and the defined ability alias.
Action.performAs(actor: Actor): Promise<T> {
// this.abilityAlias is provided via Action which extends UsingAlias
const ability = await Ability.as(actor, this.abilityAlias);
// ... now ability functionality can be used
return ability.doSomething();
};
The Actions performAs method internally calls the Ability's "as" method to get the instanciated instance from the Actors ability map.
Ability.as(actor: IActor, alias?: any): IAbility {
return actor.withAbilityTo(this, alias);
};
The Actors withAbilityTo method actually gets the instance from the map. If an alias is provided the aliased version of the ability is used else the default one.
Triggering of tasks
await Actor.attemptsTo(
// trigger task with default ability
Task.execute(),
// trigger task with aliased ability
Task.execute().withAbilityAlias('aliased'),
);
Tasks are collections of Actions and/or sub-Tasks. The first usecase is to pass down the defined ability alias to all actions/tasks.
Task.performAs(actor: Actor): Promise<T> {
// define the actions / subtasks to execute
const activities: (Task | Action)[] = [
Action.execute(),
Task.execute(),
];
// provide the ability alias to all activities
this.activities.forEach(activity => {
activity.withAbilityAlias(this.abilityAlias);
});
// execute the activities and return the result
return actor.attemptsTo(
...this.activities,
);
};
The second usecase is to pass down the defined ability alias to only specific actions/tasks.
Task.performAs(actor: Actor): Promise<T> {
return actor.attemptsTo(
Action.execute().withAbilityAlias(this.abilityAlias),
Task.execute(),
);
};
The third usecase is to pass down the ability aliases independent from any passed in alias.
Task.performAs(actor: Actor): Promise<T> {
return actor.attemptsTo(
Action.execute().withAbilityAlias('aliased'),
Task.execute().withAbilityAlias('another alias'),
);
};
The lookup of tha desired Ability including the alias information is the same as described above in the Actions section.
Triggering of questions
Questions per se follow the same flow as actions/tasks. The only difference is the names of internal methods. So is attemptsTo substituted with asks and performAs with answeredBy.
await Actor.asks(
Question.toBe.truthy().withAbilityAlias('aliased'),
);
Question.answeredBy(actor: Actor): Promise<T> {
const ability = await Ability.as(actor, this.abilityAlias);
// ... now ability functionality can be used
expect(ability.doSomething()).toBe(true);
return true;
};