@serenity-js/core 3.2.1 → 3.3.1

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 +27 -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 +5 -5
  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
@@ -10,11 +10,7 @@ import { Timestamp } from './Timestamp';
10
10
  */
11
11
  export class Scheduler {
12
12
 
13
- private remainingCallbacks: Map<DelayedCallback<unknown>, CallbackInfo<unknown>> = new Map();
14
- private completedCallbacks: Map<DelayedCallback<unknown>, CallbackInfo<unknown>> = new Map();
15
- private failedCallbacks: Map<DelayedCallback<unknown>, CallbackInfo<unknown>> = new Map();
16
-
17
- private timer: NodeJS.Timer;
13
+ private scheduledOperations: Array<ScheduledOperation<unknown>> = [];
18
14
 
19
15
  /**
20
16
  * @param clock
@@ -39,7 +35,7 @@ export class Scheduler {
39
35
  {
40
36
  maxInvocations: 1,
41
37
  delayBetweenInvocations: () => delay,
42
- timeout: this.interactionTimeout,
38
+ timeout: this.interactionTimeout.plus(delay),
43
39
  },
44
40
  );
45
41
  }
@@ -81,154 +77,111 @@ export class Scheduler {
81
77
  errorHandler = rethrowErrors,
82
78
  } = limits;
83
79
 
84
- this.remainingCallbacks.set(callback, {
85
- exitCondition: exitCondition,
86
- currentInvocation: 0,
87
- invocationsLeft: maxInvocations,
88
- delayBetweenInvocations,
89
- startedAt: this.clock.now(),
90
- timeout,
91
- errorHandler,
92
- result: undefined,
93
- });
94
-
95
- return this.receiptFor<Result>(callback);
80
+ const operation = new ScheduledOperation(
81
+ this.clock,
82
+ callback,
83
+ {
84
+ exitCondition,
85
+ maxInvocations,
86
+ delayBetweenInvocations,
87
+ timeout,
88
+ errorHandler,
89
+ }
90
+ );
91
+
92
+ this.scheduledOperations.push(operation);
93
+ return operation.start()
96
94
  }
97
95
 
98
- start(): void {
99
- if (! this.timer) {
100
- this.timer = setInterval(
101
- () => this.invokeCallbacksScheduledUntil(this.clock.now()),
102
- 100
103
- )
96
+ stop(): void {
97
+ for (const operation of this.scheduledOperations) {
98
+ operation.cancel();
104
99
  }
105
100
  }
101
+ }
102
+
103
+ class ScheduledOperation<Result> {
104
+ private currentInvocation = 0;
105
+ private invocationsLeft = 0;
106
+ private startedAt: Timestamp;
107
+ private lastResult: Result;
106
108
 
107
- isRunning(): boolean {
108
- return Boolean(this.timer);
109
+ private isCancelled = false;
110
+
111
+ constructor(
112
+ private readonly clock: Clock,
113
+ private readonly callback: DelayedCallback<Result>,
114
+ private readonly limits: RepeatUntilLimits<Result> = {},
115
+ ) {
109
116
  }
110
117
 
111
- stop(): void {
112
- if (this.timer) {
113
- clearInterval(this.timer);
114
- this.timer = undefined;
115
-
116
- for (const [callback, info] of this.remainingCallbacks) {
117
- this.remainingCallbacks.delete(callback);
118
- this.failedCallbacks.set(callback, {
119
- ...info,
120
- error: new OperationInterruptedError(`Scheduler stopped before executing callback ${ callback }`)
121
- });
122
- }
123
- }
118
+ async start(): Promise<Result> {
119
+ this.currentInvocation = 0;
120
+ this.invocationsLeft = this.limits.maxInvocations;
121
+ this.startedAt = this.clock.now();
122
+
123
+ return await this.poll();
124
124
  }
125
125
 
126
- invokeCallbacksScheduledForNext(duration: Duration): void {
127
- this.invokeCallbacksScheduledUntil(
128
- this.clock.now().plus(duration)
129
- );
126
+ private async poll(): Promise<Result> {
127
+ await this.clock.waitFor(this.limits.delayBetweenInvocations(this.currentInvocation));
128
+
129
+ if (this.isCancelled) {
130
+ throw new OperationInterruptedError('Scheduler stopped before executing callback');
131
+ }
132
+
133
+ const receipt = await this.invoke();
134
+
135
+ if (receipt.hasCompleted) {
136
+ return receipt.result;
137
+ }
138
+
139
+ this.currentInvocation++;
140
+ this.invocationsLeft--;
141
+
142
+ return await this.poll();
130
143
  }
131
144
 
132
- private invokeCallbacksScheduledUntil(timestamp: Timestamp): void {
133
- for (const [ callback, info ] of this.remainingCallbacks) {
134
- const { startedAt, currentInvocation, delayBetweenInvocations } = info;
135
- const expectedInvocationTime = startedAt.plus(delayBetweenInvocations(currentInvocation));
145
+ private async invoke(): Promise<{ result?: Result, error?: Error, hasCompleted: boolean }> {
146
+
147
+ const timeoutExpired = this.startedAt.plus(this.limits.timeout).isBefore(this.clock.now());
148
+ const isLastInvocation = this.invocationsLeft === 1;
149
+
150
+ if (this.invocationsLeft === 0) {
151
+ return {
152
+ result: this.lastResult,
153
+ hasCompleted: true,
154
+ };
155
+ }
136
156
 
137
- if (expectedInvocationTime.isBeforeOrEqual(timestamp)) {
138
- this.invoke(callback);
157
+ try {
158
+ if (timeoutExpired) {
159
+ throw new TimeoutExpiredError(`Timeout of ${ this.limits.timeout } has expired`);
160
+ }
161
+
162
+ this.lastResult = await this.callback({ currentTime: this.clock.now(), i: this.currentInvocation });
163
+
164
+ return {
165
+ result: this.lastResult,
166
+ hasCompleted: this.limits.exitCondition(this.lastResult) || isLastInvocation,
139
167
  }
140
168
  }
141
- }
169
+ catch(error) {
142
170
 
143
- private invoke<Result>(callback: DelayedCallback<Result>): void {
144
- const info = this.remainingCallbacks.get(callback);
145
-
146
- this.remainingCallbacks.delete(callback);
147
-
148
- Promise.resolve()
149
- .then(async () => {
150
- const timeoutExpired = info.startedAt.plus(info.timeout).isBefore(this.clock.now());
151
- const isLastInvocation = info.invocationsLeft === 1;
152
-
153
- if (info.invocationsLeft === 0) {
154
- return {
155
- hasCompleted: true,
156
- };
157
- }
158
-
159
- try {
160
- if (timeoutExpired) {
161
- throw new TimeoutExpiredError(`Timeout of ${ info.timeout } has expired`);
162
- }
163
-
164
- const result = await callback({ currentTime: this.clock.now(), i: info.currentInvocation });
165
-
166
- return {
167
- result,
168
- hasCompleted: info.exitCondition(result) || isLastInvocation,
169
- }
170
- }
171
- catch(error) {
172
- info.errorHandler(error, info.result);
173
-
174
- // if the errorHandler didn't throw, it's a recoverable error
175
- return {
176
- error,
177
- hasCompleted: isLastInvocation,
178
- }
179
- }
180
- })
181
- .then(({ result, error, hasCompleted }) => {
182
- if (hasCompleted) {
183
- this['completedCallbacks'].set(callback, {
184
- ...info,
185
- result: result ?? info.result,
186
- error,
187
- });
188
- }
189
- else {
190
- this['remainingCallbacks'].set(callback, {
191
- ...info,
192
- currentInvocation: info.currentInvocation + 1,
193
- invocationsLeft: info.invocationsLeft - 1,
194
- result: result ?? info.result,
195
- error,
196
- });
197
- }
198
- })
199
- .catch(error => {
200
- this.failedCallbacks.set(callback, { ...info, error });
201
- });
202
- }
171
+ this.limits.errorHandler(error, this.lastResult);
203
172
 
204
- private receiptFor<Result>(callback: DelayedCallback<unknown>): Promise<Result> {
205
- return new Promise((resolve, reject) => {
206
-
207
- const timer = setInterval(() => {
208
- if (this.failedCallbacks.has(callback)) {
209
- clearInterval(timer);
210
- return reject(this.failedCallbacks.get(callback).error);
211
- }
212
-
213
- if (this.completedCallbacks.has(callback)) {
214
- clearInterval(timer);
215
- return resolve(this.completedCallbacks.get(callback).result as Result);
216
- }
217
- }, 25);
218
- })
173
+ // if the errorHandler didn't throw, it's a recoverable error
174
+ return {
175
+ result: this.lastResult,
176
+ error,
177
+ hasCompleted: isLastInvocation,
178
+ }
179
+ }
219
180
  }
220
- }
221
181
 
222
- interface CallbackInfo<Result> {
223
- exitCondition: (result: Result) => boolean,
224
- currentInvocation: number;
225
- invocationsLeft: number,
226
- delayBetweenInvocations: (i: number) => Duration,
227
- timeout: Duration
228
- startedAt: Timestamp,
229
- errorHandler: (error: Error, result: Result) => void,
230
- result?: Result,
231
- error?: Error,
182
+ cancel(): void {
183
+ this.isCancelled = true;
184
+ }
232
185
  }
233
186
 
234
187
  function noDelay() {
@@ -1,7 +1,16 @@
1
1
  import { ensure, isDefined } from 'tiny-types';
2
2
 
3
3
  import { ConfigurationError, ErrorFactory, ErrorOptions, LogicError, RaiseErrors, RuntimeError } from '../errors';
4
- import { AsyncOperationAttempted, AsyncOperationCompleted, AsyncOperationFailed, DomainEvent, SceneFinishes, SceneStarts, TestRunFinishes } from '../events';
4
+ import {
5
+ AsyncOperationAttempted,
6
+ AsyncOperationCompleted,
7
+ AsyncOperationFailed,
8
+ DomainEvent,
9
+ EmitsDomainEvents,
10
+ SceneFinishes,
11
+ SceneStarts,
12
+ TestRunFinishes
13
+ } from '../events';
5
14
  import { ActivityDetails, CorrelationId, Description, Name } from '../model';
6
15
  import { Actor, Clock, Duration, ScheduleWork, Timestamp } from '../screenplay';
7
16
  import { ListensToDomainEvents } from '../stage';
@@ -23,7 +32,7 @@ import { StageManager } from './StageManager';
23
32
  *
24
33
  * @group Stage
25
34
  */
26
- export class Stage {
35
+ export class Stage implements EmitsDomainEvents {
27
36
 
28
37
  private static readonly unknownSceneId = new CorrelationId('unknown')
29
38
 
@@ -91,11 +100,10 @@ export class Stage {
91
100
  if (! this.instantiatedActorCalled(name)) {
92
101
  let actor;
93
102
  try {
94
- const newActor = new Actor(name, this)
95
- .whoCan(
96
- new RaiseErrors(this),
97
- new ScheduleWork(this.clock, this.interactionTimeout),
98
- );
103
+ const newActor = new Actor(name, this, [
104
+ new RaiseErrors(this),
105
+ new ScheduleWork(this.clock, this.interactionTimeout)
106
+ ]);
99
107
 
100
108
  actor = this.cast.prepare(newActor);
101
109