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.
The solution Testla offers is Ability Aliasing
. With that multiple instances of an ability can be assigned to a user at the same time.
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.
Once the abilities have been assigned they can be used independently but at the same time in the test flow.
The below example shows how to use Actions with default and aliased abilities. The same is for Tasks and Questions.
// actions
await Actor.attemptsTo(
// trigger action with default ability
Action.execute(),
// trigger action with aliased ability
Action.execute().withAbilityAlias('aliased'),
);
// tasks
await Actor.attemptsTo(
// trigger task with default ability
Task.execute(),
// trigger task with aliased ability
Task.execute().withAbilityAlias('aliased'),
);
// questions
await Actor.asks(
// ask question with default ability
Question.toBe.truthy(),
// ask question with aliased ability
Question.toBe.truthy().withAbilityAlias('aliased'),
);
To use the default ability without an alias the test codes do not need any special handling and are just written in the known format. To use Actions, Tasks or Questions with an aliased ability, the .withAbilityAlias('alias name')
method needs to be used via chaining.
This section gives you an overview over the specific integration details needed to offer ability aliasing in custom elements. If you need further information please refer to the Testla Screenplay Core Library.
Actors attemptsTo method triggers the Action internal performAs method with the given Actor and the optionally defined ability alias.
The Actions performAs method then internally calls the Ability's "as" method to get the instanciated instance from the Actors ability map.
Therefore it is important that during the custom integration this.abilityAlias
is passed to the Ability's as
method as seen in the example below.
Action.performAs(actor: Actor): Promise<T> {
// this.abilityAlias is provided to ability's as method
const ability = await Ability.as(actor, this.abilityAlias);
// ... now ability functionality can be used
return ability.doSomething();
};
By nature a Task is a collection of Actions or other Tasks. Therefore the Ability Alias likely needs to be forwarded to all internal Actions and Tasks. This can be achieved as shown below.
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,
);
};
In a second use case it might be desired to only pass down the Ability Alias to some Actions and Tasks. This can be achieved as follows.
Task.performAs(actor: Actor): Promise<T> {
return actor.attemptsTo(
// this.abilityAlias is passed in to the task
// by calling it with its own withAbilityAlias method
Action.execute().withAbilityAlias(this.abilityAlias),
Task.execute(),
);
};
The last scenario is that we do want to pass down Ability Aliases completely independent from a potentially passed in alias. In this case the aliases are just specified via their direct string value.
Task.performAs(actor: Actor): Promise<T> {
return actor.attemptsTo(
Action.execute().withAbilityAlias('aliased'),
Task.execute().withAbilityAlias('another alias'),
);
};
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.
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;
};