UNPKG

@serenity-js/core

Version:

The core Serenity/JS framework, providing the Screenplay Pattern interfaces, as well as the test reporting and integration infrastructure

326 lines (296 loc) 11.7 kB
import { ensure, isInstanceOf } from 'tiny-types'; import type { OutputStream } from './adapter'; import type { SerenityConfig } from './config'; import type { ErrorOptions, RuntimeError } from './errors'; import { ConfigurationError, ErrorFactory, NoOpDiffFormatter } from './errors'; import type { DomainEvent, EmitsDomainEvents } from './events'; import { ClassDescriptionParser, ClassLoader, d, FileSystem, has, ModuleLoader, Path } from './io'; import { type ActivityDetails, CorrelationId, type CorrelationIdFactory } from './model'; import type { Actor, Timestamp } from './screenplay'; import { Clock, Duration } from './screenplay'; import type { StageCrewMember, StageCrewMemberBuilder } from './stage'; import type { Cast } from './stage/Cast'; import { Extras } from './stage/Extras'; import { Stage } from './stage/Stage'; import { StageManager } from './stage/StageManager'; /** * @group Serenity */ export class Serenity implements EmitsDomainEvents { private static defaultCueTimeout = Duration.ofSeconds(5); private static defaultInteractionTimeout = Duration.ofSeconds(5); private static defaultActors = new Extras(); private readonly stage: Stage; private readonly fileSystem: FileSystem; private outputStream: OutputStream = process.stdout; private readonly classLoader: ClassLoader; private readonly workingDirectory: Path; /** * @param clock * @param cwd * @param sceneIdFactory */ constructor( clock: Clock = new Clock(), cwd: string = process.cwd(), sceneIdFactory: CorrelationIdFactory = CorrelationId, ) { this.stage = new Stage( Serenity.defaultActors, new StageManager(Serenity.defaultCueTimeout, clock), new ErrorFactory(), clock, Serenity.defaultInteractionTimeout, sceneIdFactory, ); this.classLoader = new ClassLoader( new ModuleLoader(cwd), new ClassDescriptionParser(), ); this.workingDirectory = new Path(cwd); this.fileSystem = new FileSystem(this.workingDirectory); } /** * Configures Serenity/JS. Every call to this function * replaces the previous configuration provided, * so this function should be called exactly once * in your test suite. * * @param config */ configure(config: SerenityConfig): void { const looksLikeBuilder = has<StageCrewMemberBuilder>({ build: 'function' }); const looksLikeStageCrewMember = has<StageCrewMember>({ assignedTo: 'function', notifyOf: 'function' }); const cueTimeout = config.cueTimeout ? ensure('cueTimeout', config.cueTimeout, isInstanceOf(Duration)) : Serenity.defaultCueTimeout; const interactionTimeout = config.interactionTimeout ? ensure('interactionTimeout', config.interactionTimeout, isInstanceOf(Duration)) : Serenity.defaultInteractionTimeout; if (config.outputStream) { this.outputStream = config.outputStream; } this.stage.configure({ actors: config.actors ?? Serenity.defaultActors, cueTimeout, interactionTimeout, diffFormatter: config.diffFormatter ?? new NoOpDiffFormatter(), }); if (Array.isArray(config.crew)) { this.stage.assign( ...config.crew.map((stageCrewMemberDescription, i) => { const stageCrewMember = this.classLoader.looksLoadable(stageCrewMemberDescription) ? this.classLoader.instantiate<StageCrewMember | StageCrewMemberBuilder>(stageCrewMemberDescription) : stageCrewMemberDescription; if (looksLikeBuilder(stageCrewMember)) { return stageCrewMember.build({ stage: this.stage, fileSystem: this.fileSystem, outputStream: this.outputStream, }); } if (looksLikeStageCrewMember(stageCrewMember)) { return stageCrewMember.assignedTo(this.stage); } throw new ConfigurationError( d`Entries under \`crew\` should implement either StageCrewMember or StageCrewMemberBuilder interfaces, \`${ stageCrewMemberDescription }\` found at index ${ i }` ); }), ); } } /** * Re-configures Serenity/JS with a new [cast](https://serenity-js.org/api/core/class/Cast/) of [actors](https://serenity-js.org/api/core/class/Actor/) * you want to use in any subsequent calls to [`actorCalled`](https://serenity-js.org/api/core/function/actorCalled/). * * For your convenience, use [`engage`](https://serenity-js.org/api/core/function/engage/) function instead, * which provides an alternative to calling [`Actor.whoCan`](https://serenity-js.org/api/core/class/Actor/#whoCan) directly in your tests * and is typically invoked in a "before all" or "before each" hook of your test runner of choice. * * If your implementation of the [cast](https://serenity-js.org/api/core/class/Cast/) interface is stateless, * you can invoke this function just once before your entire test suite is executed, see * - [`beforeAll`](https://jasmine.github.io/api/3.6/global.html#beforeAll) in Jasmine, * - [`before`](https://mochajs.org/#hooks) in Mocha, * - [`BeforeAll`](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/hooks.md#beforeall--afterall) in Cucumber.js * * However, if your [cast](https://serenity-js.org/api/core/class/Cast/) holds state that you want to reset before each scenario, * it's better to invoke `engage` before each test using: * - [`beforeEach`](https://jasmine.github.io/api/3.6/global.html#beforeEach) in Jasmine * - [`beforeEach`](https://mochajs.org/#hooks) in Mocha, * - [`Before`](https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/hooks.md#hooks) in Cucumber.js * * ## Engaging a cast of actors * * ```ts * import { Actor, Cast } from '@serenity-js/core'; * * class Actors implements Cast { * prepare(actor: Actor) { * return actor.whoCan( * // ... abilities you'd like the Actor to have * ); * } * } * * engage(new Actors()); * ``` * * ### Using with Mocha test runner * * ```ts * import { beforeEach } from 'mocha' * * beforeEach(() => engage(new Actors())) * ``` * * ### Using with Jasmine test runner * * ```ts * import 'jasmine' * * beforeEach(() => engage(new Actors())) * ``` * * ### Using with Cucumber.js test runner * * ```ts * import { Before } from '@cucumber/cucumber' * * Before(() => engage(new Actors())) * ``` * * ## Learn more * - [`Actor`](https://serenity-js.org/api/core/class/Actor/) * - [`Cast`](https://serenity-js.org/api/core/class/Cast/) * - [`engage`](https://serenity-js.org/api/core/function/engage/) * * @param actors */ engage(actors: Cast): void { this.stage.engage(actors); } /** * Instantiates or retrieves an [`Actor`](https://serenity-js.org/api/core/class/Actor/) * called `name` if one has already been instantiated. * * For your convenience, use [`actorCalled`](https://serenity-js.org/api/core/function/actorCalled/) function instead. * * ## Usage with Mocha * * ```typescript * import { describe, it } from 'mocha'; * import { actorCalled } from '@serenity-js/core'; * * describe('Feature', () => { * * it('should have some behaviour', () => * actorCalled('James').attemptsTo( * // ... activities * )) * }) * ``` * * ## Usage with Jasmine * * ```typescript * import 'jasmine'; * import { actorCalled } from '@serenity-js/core'; * * describe('Feature', () => { * * it('should have some behaviour', () => * actorCalled('James').attemptsTo( * // ... activities * )) * }) * ``` * * ## Usage with Cucumber * * ```typescript * import { actorCalled } from '@serenity-js/core'; * import { Given } from '@cucumber/cucumber'; * * Given(/(.*?) is a registered user/, (name: string) => * actorCalled(name).attemptsTo( * // ... activities * )) * ``` * * ## Learn more * * - [`engage`](https://serenity-js.org/api/core/function/engage/) * - [`Actor`](https://serenity-js.org/api/core/class/Actor/) * - [`Cast`](https://serenity-js.org/api/core/class/Cast/) * - [`actorCalled`](https://serenity-js.org/api/core/function/actorCalled/) * * @param name * The name of the actor to instantiate or retrieve */ theActorCalled(name: string): Actor { return this.stage.actor(name); } /** * Retrieves an actor who was last instantiated or retrieved * using [`Serenity.theActorCalled`](https://serenity-js.org/api/core/class/Serenity/#theActorCalled). * * This function is particularly useful when automating Cucumber scenarios. * * For your convenience, use [`actorInTheSpotlight`](https://serenity-js.org/api/core/function/actorInTheSpotlight/) function instead. * * ## Usage with Cucumber * * ```ts * import { actorCalled } from '@serenity-js/core'; * import { Given, When } from '@cucumber/cucumber'; * * Given(/(.*?) is a registered user/, (name: string) => * actorCalled(name).attemptsTo( * // ... activities * )) * * When(/(?:he|she|they) browse their recent orders/, () => * actorInTheSpotlight().attemptsTo( * // ... activities * )) * ``` * * ## Learn more * * - [`engage`](https://serenity-js.org/api/core/function/engage/) * - [`actorCalled`](https://serenity-js.org/api/core/function/actorCalled/) * - [`actorInTheSpotlight`](https://serenity-js.org/api/core/function/actorInTheSpotlight/) * - [`Actor`](https://serenity-js.org/api/core/class/Actor/) * - [`Cast`](https://serenity-js.org/api/core/class/Cast/) */ theActorInTheSpotlight(): Actor { return this.stage.theActorInTheSpotlight(); } announce(...events: Array<DomainEvent>): void { this.stage.announce(...events); } currentTime(): Timestamp { return this.stage.currentTime(); } assignNewSceneId(): CorrelationId { return this.stage.assignNewSceneId(); } currentSceneId(): CorrelationId { return this.stage.currentSceneId(); } assignNewActivityId(activityDetails: ActivityDetails): CorrelationId { return this.stage.assignNewActivityId(activityDetails); } createError<RE extends RuntimeError>(errorType: new (...args: any[]) => RE, options: ErrorOptions): RE { return this.stage.createError(errorType, options); } /** * @package */ waitForNextCue(): Promise<void> { return this.stage.waitForNextCue(); } cwd(): Path { return this.workingDirectory; } }