🎭 Screenplay Pattern - Part 1: Actors
If you're diving into the Screenplay pattern for test automation, the first concept to understand is the Actor.
In Screenplay, actors represent the users or roles interacting with your application.
They don’t know how to do things — they just know what they want to achieve.
💡 Think of them like personas:
A customer logs in and transfers money.
An admin reviews reports.
A guest browses the site.
Why use Actors?
✅ Readable scripts:
Instead of reading raw implementation, you read intent:
bankCustomer.makeAction('enterLoginCredentials')
✅ Modular logic:
Actors separate who is performing actions from how they're done. It keeps your actions reusable and test logic clean.
✅ Test from the user's POV:
You shift focus from UI details to user goals. That helps both devs and stakeholders understand the test’s purpose.
How an actor works:
- I use an base-actor, that have the methods to perform actions and questions:
interface IBaseActor {
actions: ActionMethods;
questions: QuestionMethods;
/**
*
* @param action receives the name of a valid action for the actor
* @param parameters this parameters will be passed to the action
*/
makeAction(action: string, parameters?: any): void;
/**
*
* @param question receives the name of a valid question for the actor
* @param parameters this parameters will be passed to the question
*/
makeQuestion(question: string, parameters?: any): void;
}
- Then, I create the test users extending the base one, and pass to super, the available actions that the user can do ( next post will be about actions, but actions are separated by feature):
export default class BankCustomer extends BaseActor {
constructor() {
const loginActions = new LoginActions();
const registrationActions = new RegistrationActions();
const accountActions = new AccountActions();
const transactionActions = new TransactionActions();
const actionsInstances = [loginActions, registrationActions, accountActions, transactionActions];
const loginQuestions = new LoginQuestions();
const registrationQuestions = new RegistrationQuestions();
const accountQuestions = new AccountQuestions();
const transactionQuestions = new TransactionQuestions();
const questionsInstances = [loginQuestions, registrationQuestions, accountQuestions, transactionQuestions];
super(actionsInstances, questionsInstances);
}
}
But, to this work, I had to make an proxy on the base-actor, because I'm passing to him actions and questions from different features, and I don't wanna do:
bankCustomer.login.makeAction('clickLoginButton');
I wanna that all actions be available on the same level, not separated by feature ( for the actor, an action is an action, the separation by feature is only for code organization ), so, I do an proxy that make all works:
constructor(actionsInstances: any[], questionsInstances: any[]) {
this.actions = new Proxy(
{},
{
get: (_, prop: string) => {
if (typeof prop !== 'string') {
return undefined;
}
for (const instance of actionsInstances) {
if (prop in instance) {
return instance[prop].bind(instance);
}
}
console.error(`Action ${prop} not found.`);
},
}
);
With this, actors can access all its actions and questions only passing the action name.
Example of an test using actors:
it('should successfully log in with valid credentials using BankCustomer actor', () => {
cy.setLocalStorageUser({ email: validUser.email, password: validUser.password });
const bankCustomer = new BankCustomer();
bankCustomer.makeAction('enterLoginCredentials', {
email: validUser.email,
password: validUser.password,
});
bankCustomer.makeAction('clickLoginButton');
bankCustomer.makeQuestion('isUserLoggedInSuccessfully');
});
In short:
Actors are the core of Screenplay because they bring meaning to your tests. They make scripts readable, reusable, and realistic.
Next post: 🎬 Actions – what can an actor do?