Skip to main content

Customising actors

Actors begin with the Abilities needed to browse the web, take notes, and call APIs. You customise them when a scenario needs participants to share information, work in separate browser sessions, use a particular service, or represent roles created in a specific way. This guide explains how to make that change for one scenario and how to reuse it across a suite.

An Actor can have only one instance of an Ability type. Supplying another instance replaces the default, which is useful when the scenario needs a different browser, Notepad, or API client.

Overriding abilities inline​

You can override the default abilities inline or add custom abilities inline within a test scenario using the actor.whoCan method:

./spec/todo_app.spec.ts
import { describe, it, beforeEach } from '@serenity-js/playwright-test'
import { TakeNotes, Notepad } from '@serenity-js/core'

describe('Todo App', () => {

describe('actor', () => {

it('can share notes with another actor', async ({ actor, actorCalled, browser }) => {

const sharedNotepad = Notepad.empty()

// Override the default actor's ability to take notes
await actor
.whoCan(TakeNotes.using(sharedNotepad))
.attemptsTo(
// ...
)

// Create an extra actor with access to the same notepad
// to allow for sharing data between actors
await actorCalled('Bob')
.whoCan(TakeNotes.using(sharedNotepad))
.attemptsTo(
// ...
)
})
})
})

Overriding abilities via test.use​

To avoid adding or overriding abilities inline, you can define them via test.use and using the extraAbilities fixture. This approach enables you to reuse the same configuration for all the test scenarios within the given describe block.

./spec/todo_app.spec.ts
import { describe, it, test } from '@serenity-js/playwright-test'
import { MyAbility } from './MyAbility'

describe('Todo App', () => {

test.use({
extraAbilities: [ new MyAbility() ]
});

// ...
})

Naming the default actor​

By default, the single actor fixture is named Actor. Change it by setting defaultActorName in playwright.config.ts:

playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
import type { SerenityFixtures, SerenityWorkerFixtures } from '@serenity-js/playwright-test'

export default defineConfig<SerenityFixtures, SerenityWorkerFixtures>({
// Global configuration for all test scenarios
use: {
defaultActorName: 'Alice',
},

projects: [
// Per-project configuration overrides the global defaults.
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
defaultActorName: 'Alice',
},
},
],
});

You can also set it directly in a test file with test.use:

./spec/todo_app.spec.ts
import { describe, it, beforeEach, test } from '@serenity-js/playwright-test'
import { Navigate } from '@serenity-js/web'
import { Ensure, equals } from '@serenity-js/assertions'

import { TodoApp } from './screenplay/TodoApp'

describe('Todo App', () => {

describe('Guest user', () => {

test.use({
defaultActorName: 'Guest',
});

beforeEach(async ({ actor }) => {
await actor.attemptsTo(
Navigate.to('https://todo-app.serenity-js.org/#/'),
)
})

it('should start with an empty todo list', async ({ actor }) => {

await actor.attemptsTo(
Ensure.that(TodoApp.recordedItems().count(), equals(0)),
)
})
})
})

Overriding abilities via useFixtures​

When you have multiple test files that require the same customisations to your actor configuration or the same custom fixtures, you'll want to centralise it in a file like serenity-api.ts and leverage the useFixtures function.

This function returns a test API, including functions such as describe, it, beforeEach, or test, which you can use instead of the default ones provided by the framework.

./spec/test-api.ts
import { useFixtures } from '@serenity-js/playwright-test'
import { TakeNotes, Notepad } from '@serenity-js/core'

export interface MyNotes {
testListId: string;
}

export interface TestScopeFixtures {
notepad: Notepad<MyNotes>
}

export const { describe, it, beforeEach, test } = useFixtures<TestScopeFixtures>({
notepad: Notepad.with<MyNotes>({
testListId: process.env.TEST_LIST_ID,
}),

extraAbilities: async ({ notepad }, use) => {
const extraAbilities = [
TakeNotes.using(notepad),
];

await use(extraAbilities);
},
})
./spec/todo_app.spec.ts
import { Navigate } from '@serenity-js/web'
import { notes, q } from '@serenity-js/core'

import { describe, it, beforeEach, test, type MyNotes } from './test-api.ts'

describe('Todo App', () => {

it('uses shared notes', async ({ actor }) => {
await actor.attemptsTo(
Navigate.to(q`/lists/${ notes<MyNotes>().testListId }`),
// ...
)
});

// ...
})

Sharing browser sessions and notes​

Named Actors share the default browser session unless you give them different browsing Abilities. They use separate Notepads unless you deliberately share one.

Using multiple browsers​

By default, scenarios with multiple actors reuse the same underlying page instance to align with Playwright Test defaults. While this is useful for simulating multiple users interacting within the same browser session, it may not be appropriate for scenarios where different actors represent distinct users or roles. For instance, a doctor and a patient using a healthcare system would not share the same browser session or user interface.

To address this, use the actor.whoCan method to override the default ability to BrowseTheWebWithPlaywright. This allows actors who require a separate browser to have their own instance.

In this modified implementation below, Alice uses the default browser instance, associated with the page fixture, while Bob uses a separate browser instance launched using the browser fixture.

./spec/todo_app.spec.ts
import { describe, it, beforeEach } from '@serenity-js/playwright-test'
import { BrowseTheWebWithPlaywright } from '@serenity-js/playwright'
import { Navigate } from '@serenity-js/web'
import { Ensure, equals } from '@serenity-js/assertions'

import { TodoApp } from './screenplay/TodoApp'

describe('Todo App', () => {

describe('Guest user', () => {


it('does not share lists across browsers', async ({ actorCalled, browser }) => {

await actorCalled('Alice').attemptsTo(
Navigate.to('https://todo-app.serenity-js.org/#/'),
TodoApp.recordItem('Read a book'),
Ensure.that(TodoApp.recordedItems(), equals([
'Read a book'
])),
)

await actorCalled('Bob')
.whoCan(BrowseTheWebWithPlaywright.using(browser))
.attemptsTo(
Navigate.to('https://todo-app.serenity-js.org/#/'),
Ensure.that(TodoApp.recordedItems(), equals([ ])),
)
})
})
})

If your scenario requires more actors using separate browser instances, you can override the default ability to BrowseTheWeb for each actor in the same way.

Sharing notes between actors​

By default, each actor receives an ability to TakeNotes using their own notepad. To share notes between actors, you can introduce a notepad fixture and override the default actorCalled function to make the actors reuse the same notepad.

Overriding the actorCalled function allows you to customise the abilities of the actors created via the actorCalled fixture, as well as the default actor injected via the actor fixture.

./spec/test-api.ts
import { useFixtures } from '@serenity-js/playwright-test';
import { Notepad, notes, TakeNotes } from '@serenity-js/core';

export interface SharedNotes {
items: string[];
}

export interface TestScopeFixtures {
notepad: Notepad<SharedNotes>
}

export const { describe, it, beforeEach, afterEach, test } = useFixtures<TestScopeFixtures>({

// Shared notepad instantiated before each test
notepad: async ({ }, use) => {

// Create an empty notepad to share between actors
const notepad = Notepad.empty<SharedNotes>()

// Alternatively, pre-populate the notepad with some data
// const notepad = Notepad.with<SharedNotes>({
// items: ['strawberries', 'bananas'],
// })

await use(notepad)
},

// Override the actorCalled fixture to use the shared notepad
extraAbilities: async ({ notepad }, use) => {

await use(actorName => {
// Return the same set of extra abilities for all the actors,
// or make it conditional based on the actorName
return [
TakeNotes.using(notepad),
]
})
},
})
./spec/todo_app.spec.ts
import { notes } from '@serenity-js/core'
import { Ensure, equals } from '@serenity-js/assertions'
import { describe, it, SharedNotes } from './test-api'

describe('Todo App', () => {

describe('actor', () => {

it('can access a shared notepad', async ({ actor, actorCalled, browser }) => {

// default actor
await actor.attemptsTo(
notes<SharedNotes>().set('items', [
'apples',
'oranges',
]),
)

// additional actor, sharing the same notes
await actorCalled('Bobby').attemptsTo(
Ensure.that(
notes<SharedNotes>().get('items'),
equals([
'apples',
'oranges',
]),
),
)
})
})
})

Replacing the default actors​

For advanced Serenity/JS users, rather than modifying individual abilities, you can replace the default Cast of actors with a custom implementation.

For example, a custom cast can ensure each actor receives their own browser instance while sharing notes.

./spec/test-api.ts
import { Cast, Notepad, TakeNotes } from '@serenity-js/core'
import { BrowseTheWebWithPlaywright } from '@serenity-js/playwright'
import { useFixtures } from '@serenity-js/playwright-test'

export interface MyNotes {
testListId: string;
}

export const { describe, it, beforeEach, test } = useFixtures<{ notepad: Notepad<MyNotes> }>({
notepad: Notepad.with<MyNotes>({
testListId: process.env.TEST_LIST_ID,
}),

actors: async ({ browser, contextOptions, notepad }, use) => {
const cast = Cast.where(actor => actor.whoCan(
BrowseTheWebWithPlaywright.using(browser, {
...contextOptions,
userAgent: `${ actor.name }`
}),
TakeNotes.using<MyNotes>(notepad),
))

await use(cast)
},
})

Overriding actors replaces the cast used to configure the actors instantiated via the actorCalled fixture, as well as the default actor injected via the actor fixture.

What you learnt​

  • Actors come with default abilities (BrowseTheWebWithPlaywright, TakeNotes, CallAnApi) that you can override.
  • Use actor.whoCan(...) for one-off changes, test.use for file-level config, and useFixtures for cross-file reuse.
  • Give actors separate browser instances with BrowseTheWebWithPlaywright.using(browser).
  • Share data between actors by providing them with the same Notepad instance.
  • For full control, replace the entire Cast via the actors fixture.

Next step​

Learn about the reporting capabilities available with Serenity/JS and Playwright Test.