@serenity-js/core 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/lib/Serenity.d.ts +2 -2
  3. package/lib/Serenity.d.ts.map +1 -1
  4. package/lib/events/EmitsDomainEvents.d.ts +11 -0
  5. package/lib/events/EmitsDomainEvents.d.ts.map +1 -0
  6. package/lib/events/EmitsDomainEvents.js +3 -0
  7. package/lib/events/EmitsDomainEvents.js.map +1 -0
  8. package/lib/events/index.d.ts +1 -0
  9. package/lib/events/index.d.ts.map +1 -1
  10. package/lib/events/index.js +1 -0
  11. package/lib/events/index.js.map +1 -1
  12. package/lib/io/asyncMap.js +2 -2
  13. package/lib/io/asyncMap.js.map +1 -1
  14. package/lib/screenplay/Actor.d.ts +3 -3
  15. package/lib/screenplay/Actor.d.ts.map +1 -1
  16. package/lib/screenplay/Actor.js +14 -86
  17. package/lib/screenplay/Actor.js.map +1 -1
  18. package/lib/screenplay/abilities/AnswerQuestions.d.ts +21 -0
  19. package/lib/screenplay/abilities/AnswerQuestions.d.ts.map +1 -0
  20. package/lib/screenplay/abilities/AnswerQuestions.js +37 -0
  21. package/lib/screenplay/abilities/AnswerQuestions.js.map +1 -0
  22. package/lib/screenplay/abilities/PerformActivities.d.ts +28 -0
  23. package/lib/screenplay/abilities/PerformActivities.d.ts.map +1 -0
  24. package/lib/screenplay/abilities/PerformActivities.js +66 -0
  25. package/lib/screenplay/abilities/PerformActivities.js.map +1 -0
  26. package/lib/screenplay/abilities/index.d.ts +2 -0
  27. package/lib/screenplay/abilities/index.d.ts.map +1 -1
  28. package/lib/screenplay/abilities/index.js +2 -0
  29. package/lib/screenplay/abilities/index.js.map +1 -1
  30. package/lib/screenplay/time/abilities/ScheduleWork.d.ts +2 -4
  31. package/lib/screenplay/time/abilities/ScheduleWork.d.ts.map +1 -1
  32. package/lib/screenplay/time/abilities/ScheduleWork.js +0 -6
  33. package/lib/screenplay/time/abilities/ScheduleWork.js.map +1 -1
  34. package/lib/screenplay/time/models/Clock.d.ts +24 -0
  35. package/lib/screenplay/time/models/Clock.d.ts.map +1 -1
  36. package/lib/screenplay/time/models/Clock.js +41 -1
  37. package/lib/screenplay/time/models/Clock.js.map +1 -1
  38. package/lib/screenplay/time/models/Scheduler.d.ts +1 -10
  39. package/lib/screenplay/time/models/Scheduler.d.ts.map +1 -1
  40. package/lib/screenplay/time/models/Scheduler.js +65 -103
  41. package/lib/screenplay/time/models/Scheduler.js.map +1 -1
  42. package/lib/stage/Stage.d.ts +2 -2
  43. package/lib/stage/Stage.d.ts.map +1 -1
  44. package/lib/stage/Stage.js +4 -2
  45. package/lib/stage/Stage.js.map +1 -1
  46. package/package.json +6 -6
  47. package/src/Serenity.ts +2 -2
  48. package/src/events/EmitsDomainEvents.ts +11 -0
  49. package/src/events/index.ts +1 -0
  50. package/src/io/asyncMap.ts +2 -2
  51. package/src/screenplay/Actor.ts +32 -131
  52. package/src/screenplay/abilities/AnswerQuestions.ts +41 -0
  53. package/src/screenplay/abilities/PerformActivities.ts +88 -0
  54. package/src/screenplay/abilities/index.ts +2 -0
  55. package/src/screenplay/time/abilities/ScheduleWork.ts +2 -10
  56. package/src/screenplay/time/models/Clock.ts +47 -1
  57. package/src/screenplay/time/models/Scheduler.ts +89 -136
  58. package/src/stage/Stage.ts +15 -7
@@ -1,26 +1,22 @@
1
- import { match } from 'tiny-types';
2
-
3
- import { AssertionError, ConfigurationError, ImplementationPendingError, TestCompromisedError } from '../errors';
4
- import { ActivityRelatedArtifactGenerated, InteractionFinished, InteractionStarts, TaskFinished, TaskStarts } from '../events';
1
+ import { ConfigurationError, TestCompromisedError } from '../errors';
2
+ import { ActivityRelatedArtifactGenerated } from '../events';
5
3
  import { typeOf } from '../io';
6
- import {
7
- ActivityDetails,
8
- Artifact,
9
- ExecutionCompromised,
10
- ExecutionFailedWithAssertionError,
11
- ExecutionFailedWithError,
12
- ExecutionSuccessful,
13
- ImplementationPending,
14
- Name,
15
- ProblemIndication,
16
- } from '../model';
17
- import { Ability, AbilityType, Answerable, Discardable, Initialisable, Interaction } from '../screenplay';
4
+ import { Artifact, Name, } from '../model';
18
5
  import { Stage } from '../stage';
19
- import { CanHaveAbilities, UsesAbilities } from './abilities';
6
+ import {
7
+ Ability,
8
+ AbilityType,
9
+ AnswerQuestions,
10
+ CanHaveAbilities,
11
+ Discardable,
12
+ Initialisable,
13
+ PerformActivities,
14
+ UsesAbilities
15
+ } from './abilities';
20
16
  import { PerformsActivities } from './activities';
21
17
  import { Activity } from './Activity';
18
+ import { Answerable } from './Answerable';
22
19
  import { CollectsArtifacts } from './artifacts';
23
- import { Question } from './Question';
24
20
  import { AnswersQuestions } from './questions';
25
21
 
26
22
  /**
@@ -82,18 +78,23 @@ import { AnswersQuestions } from './questions';
82
78
  *
83
79
  * @group Screenplay Pattern
84
80
  */
85
- export class Actor implements
86
- PerformsActivities,
81
+ export class Actor implements PerformsActivities,
87
82
  UsesAbilities,
88
83
  CanHaveAbilities<Actor>,
89
84
  AnswersQuestions,
90
- CollectsArtifacts
91
- {
85
+ CollectsArtifacts {
86
+ private readonly abilities: Map<AbilityType<Ability>, Ability> = new Map<AbilityType<Ability>, Ability>();
87
+
92
88
  constructor(
93
89
  public readonly name: string,
94
90
  private readonly stage: Stage,
95
- private readonly abilities: Map<AbilityType<Ability>, Ability> = new Map<AbilityType<Ability>, Ability>(),
91
+ abilities: Ability[] = [],
96
92
  ) {
93
+ [
94
+ new PerformActivities(this, stage),
95
+ new AnswerQuestions(this),
96
+ ...abilities
97
+ ].forEach(ability => this.acquireAbility(ability));
97
98
  }
98
99
 
99
100
  /**
@@ -111,16 +112,9 @@ export class Actor implements
111
112
  const found = this.findAbilityTo(abilityType);
112
113
 
113
114
  if (! found) {
114
- if (this.abilities.size > 0) {
115
- throw new ConfigurationError(
116
- `${ this.name } can ${ Array.from(this.abilities.keys()).map(type => type.name).join(', ') }. ` +
117
- `They can't, however, ${ abilityType.name } yet. ` +
118
- `Did you give them the ability to do so?`
119
- );
120
- }
121
-
122
115
  throw new ConfigurationError(
123
- `${ this.name } can't ${ abilityType.name } yet. ` +
116
+ `${ this.name } can ${ Array.from(this.abilities.keys()).map(type => type.name).join(', ') }. ` +
117
+ `They can't, however, ${ abilityType.name } yet. ` +
124
118
  `Did you give them the ability to do so?`
125
119
  );
126
120
  }
@@ -137,15 +131,9 @@ export class Actor implements
137
131
  */
138
132
  attemptsTo(...activities: Activity[]): Promise<void> {
139
133
  return activities
140
- .map(activity => new TrackedActivity(activity, this.stage))
141
134
  .reduce((previous: Promise<void>, current: Activity) => {
142
135
  return previous
143
- // synchronise async operations like taking screenshots
144
- .then(() => this.stage.waitForNextCue())
145
- .then(() =>{
146
- /* todo: add an execution strategy */
147
- return current.performAs(this);
148
- });
136
+ .then(() => PerformActivities.as(this).perform(current));
149
137
  }, this.initialiseAbilities());
150
138
  }
151
139
 
@@ -176,23 +164,7 @@ export class Actor implements
176
164
  * The answer to the Answerable
177
165
  */
178
166
  answer<T>(answerable: Answerable<T>): Promise<T> {
179
- function isAPromise<V>(v: Answerable<V>): v is Promise<V> {
180
- return Object.prototype.hasOwnProperty.call(v, 'then');
181
- }
182
-
183
- function isDefined<V>(v: Answerable<V>) {
184
- return ! (v === undefined || v === null);
185
- }
186
-
187
- if (isDefined(answerable) && isAPromise(answerable)) {
188
- return answerable;
189
- }
190
-
191
- if (isDefined(answerable) && Question.isAQuestion(answerable)) {
192
- return this.answer(answerable.answeredBy(this));
193
- }
194
-
195
- return Promise.resolve(answerable as T);
167
+ return AnswerQuestions.as(this).answer(answerable);
196
168
  }
197
169
 
198
170
  /**
@@ -237,7 +209,7 @@ export class Actor implements
237
209
 
238
210
  private initialiseAbilities(): Promise<void> {
239
211
  return this.findAbilitiesOfType<Initialisable>('initialise', 'isInitialised')
240
- .filter(ability => ! ability.isInitialised())
212
+ .filter(ability => !ability.isInitialised())
241
213
  .reduce(
242
214
  (previous: Promise<void>, ability: (Initialisable & Ability)) =>
243
215
  previous
@@ -254,7 +226,7 @@ export class Actor implements
254
226
  Array.from(map.values());
255
227
 
256
228
  const abilitiesWithDesiredMethods = (ability: Ability & T): boolean =>
257
- methodNames.every(methodName => typeof(ability[methodName]) === 'function');
229
+ methodNames.every(methodName => typeof (ability[methodName]) === 'function');
258
230
 
259
231
  return abilitiesFrom(this.abilities)
260
232
  .filter(abilitiesWithDesiredMethods) as Array<Ability & T>;
@@ -267,7 +239,7 @@ export class Actor implements
267
239
  }
268
240
 
269
241
  private acquireAbility(ability: Ability): void {
270
- if (! (ability instanceof Ability)) {
242
+ if (!(ability instanceof Ability)) {
271
243
  throw new ConfigurationError(`Custom abilities must extend Ability from '@serenity-js/core'. Received ${ typeOf(ability) }`);
272
244
  }
273
245
 
@@ -280,7 +252,7 @@ export class Actor implements
280
252
  abilityType: AbilityType<Specific_Ability>
281
253
  ): AbilityType<Generic_Ability> {
282
254
  const parentType = Object.getPrototypeOf(abilityType);
283
- return ! parentType || parentType === Ability
255
+ return !parentType || parentType === Ability
284
256
  ? abilityType
285
257
  : this.mostGenericTypeOf(parentType)
286
258
  }
@@ -297,74 +269,3 @@ export class Actor implements
297
269
  : maybeName;
298
270
  }
299
271
  }
300
-
301
- class ActivityDescriber {
302
-
303
- describe(activity: Activity, actor: { name: string }): Name {
304
- const template = activity.toString() === ({}).toString()
305
- ? `#actor performs ${ activity.constructor.name }`
306
- : activity.toString();
307
-
308
- return new Name(
309
- this.includeActorName(template, actor),
310
- );
311
- }
312
-
313
- private includeActorName(template: string, actor: { name: string }) {
314
- return template.replace('#actor', actor.name);
315
- }
316
- }
317
-
318
- class OutcomeMatcher {
319
- outcomeFor(error: Error | any): ProblemIndication {
320
- return match<Error, ProblemIndication>(error)
321
- .when(ImplementationPendingError, _ => new ImplementationPending(error))
322
- .when(TestCompromisedError, _ => new ExecutionCompromised(error))
323
- .when(AssertionError, _ => new ExecutionFailedWithAssertionError(error))
324
- .when(Error, _ =>
325
- /AssertionError/.test(error.constructor.name) // mocha
326
- ? new ExecutionFailedWithAssertionError(error)
327
- : new ExecutionFailedWithError(error))
328
- .else(_ => new ExecutionFailedWithError(error));
329
- }
330
- }
331
-
332
- class TrackedActivity extends Activity {
333
-
334
- protected static readonly describer = new ActivityDescriber();
335
- protected static readonly outcomes = new OutcomeMatcher();
336
-
337
- constructor(
338
- protected readonly activity: Activity,
339
- protected readonly stage: Stage,
340
- ) {
341
- super(activity.toString(), activity.instantiationLocation());
342
- }
343
-
344
- performAs(actor: (PerformsActivities | UsesAbilities | AnswersQuestions) & { name: string }): Promise<void> {
345
- const sceneId = this.stage.currentSceneId();
346
- const details = new ActivityDetails(
347
- TrackedActivity.describer.describe(this.activity, actor),
348
- this.activity.instantiationLocation(),
349
- );
350
- const activityId = this.stage.assignNewActivityId(details);
351
-
352
- const [ activityStarts, activityFinished] = this.activity instanceof Interaction
353
- ? [ InteractionStarts, InteractionFinished ]
354
- : [ TaskStarts, TaskFinished ];
355
-
356
- return Promise.resolve()
357
- .then(() => this.stage.announce(new activityStarts(sceneId, activityId, details, this.stage.currentTime())))
358
- .then(() => this.activity.performAs(actor))
359
- .then(() => {
360
- const outcome = new ExecutionSuccessful();
361
- this.stage.announce(new activityFinished(sceneId, activityId, details, outcome, this.stage.currentTime()));
362
- })
363
- .catch(error => {
364
- const outcome = TrackedActivity.outcomes.outcomeFor(error);
365
- this.stage.announce(new activityFinished(sceneId, activityId, details, outcome, this.stage.currentTime()));
366
-
367
- throw error;
368
- });
369
- }
370
- }
@@ -0,0 +1,41 @@
1
+ import { Answerable } from '../Answerable';
2
+ import { Question } from '../Question';
3
+ import { AnswersQuestions } from '../questions';
4
+ import { Ability } from './Ability';
5
+ import { UsesAbilities } from './UsesAbilities';
6
+
7
+ /**
8
+ * This {@apilink Ability} enables an {@apilink Actor} to resolve the value of a given {@apilink Answerable}.
9
+ *
10
+ * {@apilink AnswerQuestions} is used internally by {@apilink Actor.answer}, and it is unlikely you'll ever need to use it directly in your code.
11
+ * That is, unless you're building a custom Serenity/JS extension and want to override the default behaviour of the framework,
12
+ * in which case you should check out the [Contributor's Guide](/contributing).
13
+ *
14
+ * @group Abilities
15
+ */
16
+ export class AnswerQuestions extends Ability {
17
+ constructor(protected readonly actor: AnswersQuestions & UsesAbilities) {
18
+ super();
19
+ }
20
+
21
+ answer<T>(answerable: Answerable<T>): Promise<T> {
22
+
23
+ if (AnswerQuestions.isDefined(answerable) && AnswerQuestions.isAPromise(answerable)) {
24
+ return answerable;
25
+ }
26
+
27
+ if (AnswerQuestions.isDefined(answerable) && Question.isAQuestion(answerable)) {
28
+ return this.answer(answerable.answeredBy(this.actor));
29
+ }
30
+
31
+ return Promise.resolve(answerable as T);
32
+ }
33
+
34
+ private static isAPromise<V>(v: Answerable<V>): v is Promise<V> {
35
+ return Object.prototype.hasOwnProperty.call(v, 'then');
36
+ }
37
+
38
+ private static isDefined<V>(v: Answerable<V>) {
39
+ return !(v === undefined || v === null);
40
+ }
41
+ }
@@ -0,0 +1,88 @@
1
+ import { match } from 'tiny-types';
2
+
3
+ import { AssertionError, ImplementationPendingError, TestCompromisedError } from '../../errors';
4
+ import { EmitsDomainEvents, InteractionFinished, InteractionStarts, TaskFinished, TaskStarts } from '../../events';
5
+ import {
6
+ ActivityDetails,
7
+ ExecutionCompromised,
8
+ ExecutionFailedWithAssertionError,
9
+ ExecutionFailedWithError,
10
+ ExecutionSuccessful,
11
+ ImplementationPending,
12
+ Name,
13
+ Outcome,
14
+ ProblemIndication
15
+ } from '../../model';
16
+ import { PerformsActivities } from '../activities/PerformsActivities';
17
+ import { Activity } from '../Activity';
18
+ import { Interaction } from '../Interaction';
19
+ import { Ability } from './index';
20
+
21
+ /**
22
+ * An {@apilink Ability} that enables an {@apilink Actor} to perform a given {@apilink Activity}.
23
+ *
24
+ * {@apilink PerformActivities} is used internally by {@apilink Actor.perform}, and it is unlikely you'll ever need to use it directly in your code.
25
+ * That is, unless you're building a custom Serenity/JS extension and want to override the default behaviour of the framework,
26
+ * in which case you should check out the [Contributor's Guide](/contributing).
27
+ *
28
+ * @group Abilities
29
+ */
30
+ export class PerformActivities extends Ability {
31
+ constructor(
32
+ protected readonly actor: PerformsActivities & { name: string },
33
+ protected readonly stage: EmitsDomainEvents,
34
+ ) {
35
+ super();
36
+ }
37
+
38
+ async perform(activity: Activity): Promise<void> {
39
+ const sceneId = this.stage.currentSceneId();
40
+ const details = this.detailsOf(activity);
41
+ const activityId = this.stage.assignNewActivityId(details);
42
+
43
+ const [ activityStarts, activityFinished ] = activity instanceof Interaction
44
+ ? [ InteractionStarts, InteractionFinished ]
45
+ : [ TaskStarts, TaskFinished ];
46
+
47
+ try {
48
+ this.stage.announce(new activityStarts(sceneId, activityId, details, this.stage.currentTime()))
49
+
50
+ await activity.performAs(this.actor);
51
+
52
+ this.stage.announce(new activityFinished(sceneId, activityId, details, new ExecutionSuccessful(), this.stage.currentTime()));
53
+ }
54
+ catch (error) {
55
+ this.stage.announce(new activityFinished(sceneId, activityId, details, this.outcomeFor(error), this.stage.currentTime()));
56
+ throw error;
57
+ }
58
+ finally {
59
+ await this.stage.waitForNextCue();
60
+ }
61
+ }
62
+ protected outcomeFor(error: Error | any): Outcome {
63
+ return match<Error, ProblemIndication>(error)
64
+ .when(ImplementationPendingError, _ => new ImplementationPending(error))
65
+ .when(TestCompromisedError, _ => new ExecutionCompromised(error))
66
+ .when(AssertionError, _ => new ExecutionFailedWithAssertionError(error))
67
+ .when(Error, _ =>
68
+ /AssertionError/.test(error.constructor.name) // mocha
69
+ ? new ExecutionFailedWithAssertionError(error)
70
+ : new ExecutionFailedWithError(error))
71
+ .else(_ => new ExecutionFailedWithError(error));
72
+ }
73
+
74
+ private detailsOf(activity: Activity): ActivityDetails {
75
+ return new ActivityDetails(
76
+ new Name(this.nameOf(activity)),
77
+ activity.instantiationLocation(),
78
+ )
79
+ }
80
+
81
+ protected nameOf(activity: Activity): string {
82
+ const template = activity.toString() === ({}).toString()
83
+ ? `#actor performs ${ activity.constructor.name }`
84
+ : activity.toString();
85
+
86
+ return template.replace('#actor', this.actor.name);
87
+ }
88
+ }
@@ -1,6 +1,8 @@
1
1
  export * from './Ability';
2
2
  export * from './AbilityType';
3
+ export * from './AnswerQuestions';
3
4
  export * from './CanHaveAbilities';
4
5
  export * from './Discardable';
5
6
  export * from './Initialisable';
7
+ export * from './PerformActivities';
6
8
  export * from './UsesAbilities';
@@ -1,4 +1,4 @@
1
- import { Ability, Discardable, Initialisable } from '../../abilities';
1
+ import { Ability, Discardable } from '../../abilities';
2
2
  import { Clock, DelayedCallback, Duration, RepeatUntilLimits, Scheduler } from '../models';
3
3
 
4
4
  /**
@@ -11,7 +11,7 @@ import { Clock, DelayedCallback, Duration, RepeatUntilLimits, Scheduler } from '
11
11
  *
12
12
  * @group Time
13
13
  */
14
- export class ScheduleWork extends Ability implements Initialisable, Discardable {
14
+ export class ScheduleWork extends Ability implements Discardable {
15
15
 
16
16
  private readonly scheduler: Scheduler;
17
17
 
@@ -20,14 +20,6 @@ export class ScheduleWork extends Ability implements Initialisable, Discardable
20
20
  this.scheduler = new Scheduler(clock, interactionTimeout);
21
21
  }
22
22
 
23
- initialise(): void {
24
- this.scheduler.start();
25
- }
26
-
27
- isInitialised(): boolean {
28
- return this.scheduler.isRunning();
29
- }
30
-
31
23
  /**
32
24
  * @param callback
33
25
  * @param limits
@@ -1,3 +1,6 @@
1
+ import { ensure, isDefined } from 'tiny-types';
2
+
3
+ import { Duration } from './Duration';
1
4
  import { Timestamp } from './Timestamp';
2
5
 
3
6
  /**
@@ -15,14 +18,57 @@ import { Timestamp } from './Timestamp';
15
18
  * @group Time
16
19
  */
17
20
  export class Clock {
21
+ private static resolution: Duration = Duration.ofMilliseconds(10);
22
+ private timeAdjustment: Duration = Duration.ofMilliseconds(0);
18
23
 
19
24
  constructor(private readonly checkTime: () => Date = () => new Date()) {
20
25
  }
21
26
 
27
+ /**
28
+ * Sets the clock ahead to force early resolution of promises
29
+ * returned by {@apilink Clock.waitFor};
30
+ *
31
+ * Useful for test purposes to avoid unnecessary delays.
32
+ *
33
+ * @param duration
34
+ */
35
+ setAhead(duration: Duration): void {
36
+ this.timeAdjustment = ensure('duration', duration, isDefined());
37
+ }
38
+
39
+ /**
40
+ * Returns a Promise that resolves after one tick of the clock.
41
+ *
42
+ * Useful for test purposes to avoid unnecessary delays.
43
+ */
44
+ async tick(): Promise<void> {
45
+ return new Promise(resolve => setTimeout(resolve, Clock.resolution.inMilliseconds()));
46
+ }
47
+
22
48
  /**
23
49
  * Returns current time
24
50
  */
25
51
  now(): Timestamp {
26
- return new Timestamp(this.checkTime());
52
+ return new Timestamp(this.checkTime()).plus(this.timeAdjustment);
53
+ }
54
+
55
+ /**
56
+ * Returns a Promise that will be resolved after the given duration
57
+ *
58
+ * @param duration
59
+ */
60
+ async waitFor(duration: Duration): Promise<void> {
61
+ const stopAt = this.now().plus(duration);
62
+
63
+ let timer: NodeJS.Timer;
64
+
65
+ return new Promise<void>(resolve => {
66
+ timer = setInterval(() => {
67
+ if (this.now().isAfterOrEqual(stopAt)) {
68
+ clearInterval(timer);
69
+ return resolve();
70
+ }
71
+ }, Clock.resolution.inMilliseconds());
72
+ });
27
73
  }
28
74
  }