@serenity-js/console-reporter 3.0.0-rc.9 → 3.0.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 (73) hide show
  1. package/CHANGELOG.md +75 -1171
  2. package/README.md +98 -27
  3. package/lib/index.d.ts +4 -0
  4. package/lib/index.d.ts.map +1 -0
  5. package/lib/index.js +11 -1
  6. package/lib/index.js.map +1 -1
  7. package/lib/stage/crew/console-reporter/ConsoleReporter.d.ts +84 -60
  8. package/lib/stage/crew/console-reporter/ConsoleReporter.d.ts.map +1 -0
  9. package/lib/stage/crew/console-reporter/ConsoleReporter.js +233 -171
  10. package/lib/stage/crew/console-reporter/ConsoleReporter.js.map +1 -1
  11. package/lib/stage/crew/console-reporter/ConsoleReporterConfig.d.ts +13 -0
  12. package/lib/stage/crew/console-reporter/ConsoleReporterConfig.d.ts.map +1 -0
  13. package/lib/stage/crew/console-reporter/ConsoleReporterConfig.js +3 -0
  14. package/lib/stage/crew/console-reporter/ConsoleReporterConfig.js.map +1 -0
  15. package/lib/stage/crew/console-reporter/Printer.d.ts +2 -4
  16. package/lib/stage/crew/console-reporter/Printer.d.ts.map +1 -0
  17. package/lib/stage/crew/console-reporter/Printer.js +0 -3
  18. package/lib/stage/crew/console-reporter/Printer.js.map +1 -1
  19. package/lib/stage/crew/console-reporter/Summary.d.ts +7 -1
  20. package/lib/stage/crew/console-reporter/Summary.d.ts.map +1 -0
  21. package/lib/stage/crew/console-reporter/Summary.js +14 -1
  22. package/lib/stage/crew/console-reporter/Summary.js.map +1 -1
  23. package/lib/stage/crew/console-reporter/SummaryFormatter.d.ts +1 -0
  24. package/lib/stage/crew/console-reporter/SummaryFormatter.d.ts.map +1 -0
  25. package/lib/stage/crew/console-reporter/SummaryFormatter.js +4 -1
  26. package/lib/stage/crew/console-reporter/SummaryFormatter.js.map +1 -1
  27. package/lib/stage/crew/console-reporter/index.d.ts +2 -0
  28. package/lib/stage/crew/console-reporter/index.d.ts.map +1 -0
  29. package/lib/stage/crew/console-reporter/index.js +6 -1
  30. package/lib/stage/crew/console-reporter/index.js.map +1 -1
  31. package/lib/stage/crew/console-reporter/themes/TerminalTheme.d.ts +1 -7
  32. package/lib/stage/crew/console-reporter/themes/TerminalTheme.d.ts.map +1 -0
  33. package/lib/stage/crew/console-reporter/themes/TerminalTheme.js +0 -6
  34. package/lib/stage/crew/console-reporter/themes/TerminalTheme.js.map +1 -1
  35. package/lib/stage/crew/console-reporter/themes/ThemeForColourTerminals.d.ts +7 -21
  36. package/lib/stage/crew/console-reporter/themes/ThemeForColourTerminals.d.ts.map +1 -0
  37. package/lib/stage/crew/console-reporter/themes/ThemeForColourTerminals.js +9 -30
  38. package/lib/stage/crew/console-reporter/themes/ThemeForColourTerminals.js.map +1 -1
  39. package/lib/stage/crew/console-reporter/themes/ThemeForDarkTerminals.d.ts +1 -0
  40. package/lib/stage/crew/console-reporter/themes/ThemeForDarkTerminals.d.ts.map +1 -0
  41. package/lib/stage/crew/console-reporter/themes/ThemeForLightTerminals.d.ts +1 -0
  42. package/lib/stage/crew/console-reporter/themes/ThemeForLightTerminals.d.ts.map +1 -0
  43. package/lib/stage/crew/console-reporter/themes/ThemeForMonochromaticTerminals.d.ts +7 -20
  44. package/lib/stage/crew/console-reporter/themes/ThemeForMonochromaticTerminals.d.ts.map +1 -0
  45. package/lib/stage/crew/console-reporter/themes/ThemeForMonochromaticTerminals.js +6 -29
  46. package/lib/stage/crew/console-reporter/themes/ThemeForMonochromaticTerminals.js.map +1 -1
  47. package/lib/stage/crew/console-reporter/themes/index.d.ts +1 -0
  48. package/lib/stage/crew/console-reporter/themes/index.d.ts.map +1 -0
  49. package/lib/stage/crew/console-reporter/themes/index.js +5 -1
  50. package/lib/stage/crew/console-reporter/themes/index.js.map +1 -1
  51. package/lib/stage/crew/index.d.ts +1 -0
  52. package/lib/stage/crew/index.d.ts.map +1 -0
  53. package/lib/stage/crew/index.js +5 -1
  54. package/lib/stage/crew/index.js.map +1 -1
  55. package/lib/stage/index.d.ts +1 -0
  56. package/lib/stage/index.d.ts.map +1 -0
  57. package/lib/stage/index.js +5 -1
  58. package/lib/stage/index.js.map +1 -1
  59. package/package.json +19 -42
  60. package/src/index.ts +9 -0
  61. package/src/stage/crew/console-reporter/ConsoleReporter.ts +237 -173
  62. package/src/stage/crew/console-reporter/ConsoleReporterConfig.ts +12 -0
  63. package/src/stage/crew/console-reporter/Printer.ts +1 -4
  64. package/src/stage/crew/console-reporter/Summary.ts +23 -4
  65. package/src/stage/crew/console-reporter/SummaryFormatter.ts +5 -10
  66. package/src/stage/crew/console-reporter/index.ts +1 -0
  67. package/src/stage/crew/console-reporter/themes/TerminalTheme.ts +0 -8
  68. package/src/stage/crew/console-reporter/themes/ThemeForColourTerminals.ts +11 -42
  69. package/src/stage/crew/console-reporter/themes/ThemeForDarkTerminals.ts +1 -1
  70. package/src/stage/crew/console-reporter/themes/ThemeForLightTerminals.ts +1 -1
  71. package/src/stage/crew/console-reporter/themes/ThemeForMonochromaticTerminals.ts +6 -30
  72. package/tsconfig.build.json +10 -0
  73. package/tsconfig.eslint.json +0 -10
@@ -1,5 +1,5 @@
1
- import { ListensToDomainEvents, Stage, StageCrewMemberBuilder } from '@serenity-js/core';
2
- import { AssertionError } from '@serenity-js/core/lib';
1
+ import { AssertionError, d, DomainEventQueues, Duration, ListensToDomainEvents, LogicError, Stage, StageCrewMemberBuilder, Timestamp } from '@serenity-js/core';
2
+ import { OutputStream } from '@serenity-js/core/lib/adapter';
3
3
  import {
4
4
  ActivityRelatedArtifactGenerated,
5
5
  DomainEvent,
@@ -10,12 +10,10 @@ import {
10
10
  TaskFinished,
11
11
  TaskStarts,
12
12
  TestRunFinished,
13
+ TestRunStarts,
13
14
  } from '@serenity-js/core/lib/events';
14
- import { OutputStream } from '@serenity-js/core/lib/io';
15
15
  import {
16
- AssertionReport,
17
16
  CorrelationId,
18
- Duration,
19
17
  ExecutionCompromised,
20
18
  ExecutionFailedWithAssertionError,
21
19
  ExecutionFailedWithError,
@@ -27,34 +25,40 @@ import {
27
25
  Name,
28
26
  Outcome,
29
27
  ProblemIndication,
30
- Timestamp,
31
28
  } from '@serenity-js/core/lib/model';
32
- import { Instance as ChalkInstance } from 'chalk'; // eslint-disable-line unicorn/import-style
29
+ import { Instance as ChalkInstance } from 'chalk'; // eslint-disable-line unicorn/import-style
33
30
  import { ensure, isDefined, match } from 'tiny-types';
34
31
 
32
+ import { ConsoleReporterConfig } from './ConsoleReporterConfig';
35
33
  import { Printer } from './Printer';
36
34
  import { Summary } from './Summary';
37
35
  import { SummaryFormatter } from './SummaryFormatter';
38
36
  import { TerminalTheme, ThemeForDarkTerminals, ThemeForLightTerminals, ThemeForMonochromaticTerminals } from './themes';
39
37
 
40
38
  /**
41
- * @desc
42
- * Uses [standard output](https://en.wikipedia.org/wiki/Standard_streams)
43
- * to report on progress of your Serenity/JS acceptance tests.
39
+ * A {@apilink StageCrewMember} that uses [standard output](https://en.wikipedia.org/wiki/Standard_streams)
40
+ * to report on progress of your Serenity/JS acceptance tests.
44
41
  *
45
- * `ConsoleReporter` ships with colour themes for both dark and light terminals,
46
- * as well as a monochromatic theme for those moments when you're in a noir mood
47
- * (or have a terminal that doesn't support colours, like the good old `cmd.exe` on Windows).
42
+ * `ConsoleReporter` ships with colour themes for both dark and light terminals,
43
+ * as well as a monochromatic theme for those moments when you're in a noir mood
44
+ * (or have a terminal that doesn't support colours, like the good old `cmd.exe` on Windows).
48
45
  *
49
- * @example <caption>Registering the reporter programmatically</caption>
46
+ * ## Registering the reporter programmatically
47
+ *
48
+ * ```ts
50
49
  * import { configure } from '@serenity-js/core';
51
50
  * import { ConsoleReporter } from '@serenity-js/console-reporter';
52
51
  *
53
52
  * configure({
54
- * crew: [ ConsoleReporter.withDefaultColourSupport() ],
53
+ * crew: [
54
+ * ConsoleReporter.withDefaultColourSupport()
55
+ * ],
55
56
  * });
57
+ * ```
58
+ *
59
+ * ## Redirecting output to a file
56
60
  *
57
- * @example <caption>Redirecting output to a file</caption>
61
+ * ```ts
58
62
  * import { configure } from '@serenity-js/core';
59
63
  * import { ConsoleReporter } from '@serenity-js/console-reporter';
60
64
  * import { createWriteStream } from 'fs';
@@ -63,93 +67,122 @@ import { TerminalTheme, ThemeForDarkTerminals, ThemeForLightTerminals, ThemeForM
63
67
  * outputStream: createWriteStream('./output.log'),
64
68
  * crew: [ ConsoleReporter.withDefaultColourSupport() ],
65
69
  * });
70
+ * ```
71
+ * ## Registering the reporter with WebdriverIO
72
+ *
73
+ * ```ts
74
+ * // wdio.conf.ts
75
+ * import { ConsoleReporter } from '@serenity-js/console-reporter';
76
+ * import { WebdriverIOConfig } from '@serenity-js/webdriverio';
77
+ *
78
+ * export const config: WebdriverIOConfig = {
79
+ *
80
+ * framework: '@serenity-js/webdriverio',
81
+ *
82
+ * serenity: {
83
+ * crew: [
84
+ * ConsoleReporter.forDarkTerminals(),
85
+ * ]
86
+ * // other Serenity/JS config
87
+ * },
88
+ *
89
+ * // other WebdriverIO config
90
+ * }
91
+ * ```
92
+ *
93
+ * ## Registering the reporter with Protractor
66
94
  *
67
- * @example <caption>Registering the reporter using Protractor configuration</caption>
68
- * // protractor.conf.js
69
- * const { ConsoleReporter } = require('@serenity-js/console-reporter');
95
+ * ```js
96
+ * // protractor.conf.js
97
+ * const { ConsoleReporter } = require('@serenity-js/console-reporter');
70
98
  *
71
- * exports.config = {
72
- * framework: 'custom',
73
- * frameworkPath: require.resolve('@serenity-js/protractor/adapter'),
99
+ * exports.config = {
100
+ * framework: 'custom',
101
+ * frameworkPath: require.resolve('@serenity-js/protractor/adapter'),
74
102
  *
75
- * serenity: {
76
- * crew: [
77
- * ConsoleReporter.withDefaultColourSupport(),
78
- * ],
79
- * // other Serenity/JS config
80
- * },
103
+ * serenity: {
104
+ * crew: [
105
+ * ConsoleReporter.withDefaultColourSupport(),
106
+ * ],
107
+ * // other Serenity/JS config
108
+ * },
81
109
  *
82
- * // other Protractor config
83
- * };
110
+ * // other Protractor config
111
+ * }
112
+ * ```
84
113
  *
85
114
  * @public
86
- * @implements {@serenity-js/core/lib/stage~ListensToDomainEvents}
115
+ *
116
+ * @group Stage
87
117
  */
88
118
  export class ConsoleReporter implements ListensToDomainEvents {
89
119
 
90
120
  private startTimes = new StartTimes();
91
121
  private artifacts = new ActivityRelatedArtifacts();
92
122
  private summary = new Summary();
93
- private firstError: FirstError = new FirstError();
123
+ private readonly firstErrors: Map<string, FirstError> = new Map();
94
124
  private readonly summaryFormatter: SummaryFormatter;
125
+ private readonly eventQueues = new DomainEventQueues();
126
+
127
+ static fromJSON(config: ConsoleReporterConfig): StageCrewMemberBuilder<ConsoleReporter> {
128
+ return new ConsoleReporterBuilder(ConsoleReporter.theme(config.theme));
129
+ }
95
130
 
96
131
  /**
97
- * @desc
98
- * Instantiates a `ConsoleReporter` that auto-detects
99
- * your terminal's support for colours and use a colour theme
100
- * for dark terminals if successful.
101
- *
102
- * Please note that spawning your test process from another process
103
- * (by using [npm-failsafe](https://www.npmjs.com/package/npm-failsafe), for example)
104
- * causes the `ConsoleReporter` to use the monochromatic colour scheme,
105
- * as colour support can't be detected in child processes.
106
- *
107
- * If the above describes your setup, use {@link ConsoleReporter#forDarkTerminals}
108
- * or {@link ConsoleReporter#forLightTerminals} to make the sub-process produce colour output.
132
+ * Instantiates a `ConsoleReporter` that auto-detects
133
+ * your terminal's support for colours and uses a colour theme
134
+ * for dark terminals if successful.
109
135
  *
110
- * @returns {@serenity-js/core/lib/stage~StageCrewMemberBuilder}
136
+ * Please note that spawning your test process from another process
137
+ * (by using [npm-failsafe](https://www.npmjs.com/package/npm-failsafe), for example)
138
+ * causes the `ConsoleReporter` to use the monochromatic colour scheme,
139
+ * as colour support can't be detected in child processes.
111
140
  */
112
141
  static withDefaultColourSupport(): StageCrewMemberBuilder<ConsoleReporter> {
113
- return new ConsoleReporterBuilder(new ThemeForDarkTerminals(new ChalkInstance(/* auto-detect */)));
142
+ return new ConsoleReporterBuilder(ConsoleReporter.theme('auto'));
114
143
  }
115
144
 
116
145
  /**
117
- * @desc
118
- * Instantiates a `ConsoleReporter` with a monochromatic colour theme.
119
- * Good for terminals with no colour support (like the `cmd.exe` on Windows),
120
- * or for when you need to pipe the output to a text file and want
121
- * to avoid printing control characters.
122
- *
123
- * @returns {@serenity-js/core/lib/stage~StageCrewMemberBuilder}
146
+ * Instantiates a `ConsoleReporter` with a monochromatic colour theme.
147
+ * Good for terminals with no colour support (like the `cmd.exe` on Windows),
148
+ * or for times when you need to pipe the output to a text file and want
149
+ * to avoid printing control characters.
124
150
  */
125
151
  static forMonochromaticTerminals(): StageCrewMemberBuilder<ConsoleReporter> {
126
- return new ConsoleReporterBuilder(new ThemeForMonochromaticTerminals());
152
+ return new ConsoleReporterBuilder(ConsoleReporter.theme('mono'));
127
153
  }
128
154
 
129
155
  /**
130
- * @desc
131
- * Instantiates a `ConsoleReporter` with a colour theme optimised for terminals with dark backgrounds.
132
- *
133
- * @returns {@serenity-js/core/lib/stage~StageCrewMemberBuilder}
156
+ * Instantiates a `ConsoleReporter` with a colour theme optimised for terminals with dark backgrounds.
134
157
  */
135
158
  static forDarkTerminals(): StageCrewMemberBuilder<ConsoleReporter> {
136
- return new ConsoleReporterBuilder(new ThemeForDarkTerminals(new ChalkInstance({ level: 2 })));
159
+ return new ConsoleReporterBuilder(ConsoleReporter.theme('dark'));
137
160
  }
138
161
 
139
162
  /**
140
- * @desc
141
- * Instantiates a `ConsoleReporter` with a colour theme optimised for terminals with light backgrounds.
142
- *
143
- * @returns {@serenity-js/core/lib/stage~StageCrewMemberBuilder}
163
+ * Instantiates a `ConsoleReporter` with a colour theme optimised for terminals with light backgrounds.
144
164
  */
145
165
  static forLightTerminals(): StageCrewMemberBuilder<ConsoleReporter> {
146
- return new ConsoleReporterBuilder(new ThemeForLightTerminals(new ChalkInstance({ level: 2 })));
166
+ return new ConsoleReporterBuilder(ConsoleReporter.theme('light'));
167
+ }
168
+
169
+ private static theme(theme: 'light' | 'dark' | 'mono' | 'auto') {
170
+ switch (theme) {
171
+ case 'dark':
172
+ return new ThemeForDarkTerminals(new ChalkInstance({ level: 2 }));
173
+ case 'light':
174
+ return new ThemeForLightTerminals(new ChalkInstance({ level: 2 }));
175
+ case 'mono':
176
+ return new ThemeForMonochromaticTerminals();
177
+ default:
178
+ return new ThemeForDarkTerminals(new ChalkInstance(/* auto-detect */));
179
+ }
147
180
  }
148
181
 
149
182
  /**
150
183
  * @param {Printer} printer
151
184
  * @param {TerminalTheme} theme
152
- * @param {@serenity-js/core/lib/stage~Stage} [stage=null]
185
+ * @param {Stage} [stage=undefined]
153
186
  */
154
187
  constructor(
155
188
  private readonly printer: Printer,
@@ -163,167 +196,190 @@ export class ConsoleReporter implements ListensToDomainEvents {
163
196
  }
164
197
 
165
198
  /**
166
- * @desc
167
- * Handles {@link @serenity-js/core/lib/events~DomainEvent} objects emitted by the {@link @serenity-js/core/lib/stage~StageCrewMember}.
199
+ * Handles {@apilink DomainEvent} objects emitted by the {@apilink Stage}.
168
200
  *
169
- * @see {@link @serenity-js/core/lib/stage~StageCrewMember}
201
+ * @see {@apilink StageCrewMember}
170
202
  *
171
- * @listens {@serenity-js/core/lib/events~DomainEvent}
203
+ * @listens {DomainEvent}
172
204
  *
173
- * @param {@serenity-js/core/lib/events~DomainEvent} event
174
- * @returns {void}
205
+ * @param {DomainEvent} event
175
206
  */
176
207
  notifyOf(event: DomainEvent): void {
177
- match(event)
178
- .when(SceneStarts, (e: SceneStarts) => {
179
208
 
180
- this.firstError = new FirstError();
181
- this.startTimes.recordStartOf(e);
209
+ if (event instanceof TestRunStarts) {
210
+ this.summary.recordTestRunStartedAt(event.timestamp);
211
+ }
212
+
213
+ if (this.isSceneSpecific(event)) {
214
+ this.eventQueues.enqueue(event);
215
+ }
216
+
217
+ if (event instanceof SceneStarts) {
218
+ this.firstErrors.set(event.sceneId.value, new FirstError());
219
+
220
+ this.startTimes.recordStartOf(event);
221
+ }
182
222
 
183
- // Print scenario header
184
- this.printer.println(this.theme.separator('-'));
185
- this.printer.println(e.details.location.path.value, e.details.location.line ? `:${ e.details.location.line }` : '');
186
- this.printer.println();
187
- this.printer.println(this.theme.heading(e.details.category.value, ': ', e.details.name.value));
188
- this.printer.println();
189
- })
223
+ if (event instanceof SceneFinished) {
224
+ this.printScene(event.sceneId);
225
+ }
190
226
 
191
- // todo: add SceneTagged ...
227
+ if (event instanceof TestRunFinished) {
228
+ this.summary.recordTestRunFinishedAt(event.timestamp);
192
229
 
193
- .when(TaskStarts, (e: TaskStarts) => {
230
+ this.printSummary(this.summary);
194
231
 
195
- this.printer.indent();
232
+ if (event.outcome instanceof ProblemIndication) {
233
+ this.printTestRunErrorOutcome(event.outcome);
234
+ }
235
+ }
236
+ }
196
237
 
197
- if (! this.firstError.alreadyRecorded()) {
198
- this.printer.println(e.details.name.value);
199
- }
238
+ private printTestRunErrorOutcome(outcome: ProblemIndication): void {
239
+ this.printer.println(this.theme.outcome(outcome, outcome.error.stack));
240
+ }
200
241
 
201
- })
202
- .when(InteractionStarts, (e: InteractionStarts) => {
242
+ private printScene(sceneId: CorrelationId): void {
243
+ const events = this.eventQueues.drainQueueFor(sceneId);
203
244
 
204
- this.startTimes.recordStartOf(e);
245
+ for (const event of events) {
246
+ match(event)
247
+ .when(SceneStarts, (e: SceneStarts) => {
205
248
 
206
- })
207
- .when(InteractionFinished, (e: InteractionFinished) => {
249
+ const category = e.details.name.value ? `${ e.details.category.value }: ` : '';
208
250
 
209
- this.printer.indent();
210
- this.printer.println(this.formattedOutcome(e));
251
+ // Print scenario header
252
+ this.printer.println(this.theme.separator('-'));
253
+ this.printer.println(e.details.location.path.value, e.details.location.line ? `:${ e.details.location.line }` : '');
254
+ this.printer.println();
255
+ this.printer.println(this.theme.heading(category, e.details.name.value));
256
+ this.printer.println();
257
+ })
211
258
 
212
- this.printer.indent();
259
+ .when(TaskStarts, (e: TaskStarts) => {
213
260
 
214
- if (e.outcome instanceof ProblemIndication) {
215
- this.firstError.recordIfNeeded(e.outcome.error);
261
+ this.printer.indent();
216
262
 
217
- if (! (e.outcome.error instanceof AssertionError)) {
218
- this.printer.println(this.theme.outcome(e.outcome, `${ e.outcome.error }`));
263
+ if (! this.firstErrors.get(e.sceneId.value)?.alreadyRecorded()) {
264
+ this.printer.println(e.details.name.value);
219
265
  }
220
- }
221
266
 
222
- const artifactGeneratedEvents = this.artifacts.recordedFor(e.activityId);
267
+ })
268
+ .when(InteractionStarts, (e: InteractionStarts) => {
223
269
 
224
- if (artifactGeneratedEvents.some(a => a instanceof AssertionReport || a instanceof LogEntry)) {
225
- this.printer.println();
226
- }
227
-
228
- artifactGeneratedEvents.forEach(artifactGenerated => {
229
- if (artifactGenerated.artifact instanceof AssertionReport) {
230
- const details = artifactGenerated.artifact.map(
231
- (artifactContents: { expected: string, actual: string }) =>
232
- this.theme.diff(
233
- artifactContents.expected,
234
- artifactContents.actual,
235
- ),
236
- );
270
+ this.startTimes.recordStartOf(e);
237
271
 
238
- this.printer.println();
272
+ })
273
+ .when(InteractionFinished, (e: InteractionFinished) => {
239
274
 
240
- this.printer.println(details);
275
+ this.printer.indent();
276
+ this.printer.println(this.formattedOutcome(e));
241
277
 
242
- this.printer.println();
243
- }
278
+ this.printer.indent();
244
279
 
245
- if (artifactGenerated.artifact instanceof LogEntry) {
246
- const details = artifactGenerated.artifact.map((artifactContents: { data: string }) => artifactContents.data);
280
+ if (e.outcome instanceof ProblemIndication) {
281
+ this.firstErrors.get(e.sceneId.value)?.recordIfNeeded(e.outcome.error);
247
282
 
248
- if (artifactGenerated.name.value !== details) {
249
- this.printer.println(this.theme.log(artifactGenerated.name.value, ':'));
283
+ if (! (e.outcome.error instanceof AssertionError)) {
284
+ this.printer.println(this.theme.outcome(e.outcome, `${ e.outcome.error }`));
250
285
  }
286
+ }
251
287
 
252
- this.printer.println(details);
288
+ const artifactGeneratedEvents = this.artifacts.recordedFor(e.activityId);
289
+
290
+ if (artifactGeneratedEvents.some(a => a instanceof LogEntry)) {
253
291
  this.printer.println();
254
292
  }
255
- });
256
293
 
257
- this.printer.outdent();
294
+ artifactGeneratedEvents.forEach(artifactGenerated => {
295
+ if (artifactGenerated.artifact instanceof LogEntry) {
296
+ const details = artifactGenerated.artifact.map((artifactContents: { data: string }) => artifactContents.data);
258
297
 
259
- this.printer.outdent();
298
+ if (artifactGenerated.name.value !== details) {
299
+ this.printer.println(this.theme.log(artifactGenerated.name.value, ':'));
300
+ }
260
301
 
261
- })
262
- .when(ActivityRelatedArtifactGenerated, (e: ActivityRelatedArtifactGenerated) => {
302
+ this.printer.println(details);
303
+ this.printer.println();
304
+ }
305
+ });
263
306
 
264
- this.artifacts.record(e);
307
+ this.printer.outdent();
265
308
 
266
- })
267
- .when(TaskFinished, (e: TaskFinished) => {
309
+ this.printer.outdent();
268
310
 
269
- this.printer.outdent();
311
+ })
312
+ .when(ActivityRelatedArtifactGenerated, (e: ActivityRelatedArtifactGenerated) => {
270
313
 
271
- if (e.outcome instanceof ProblemIndication) {
272
- this.printer.indent();
273
- this.printer.indent();
314
+ this.artifacts.record(e);
274
315
 
275
- if (! this.firstError.alreadyRecorded()) {
276
- this.printer.println(this.theme.outcome(e.outcome, this.iconFrom(e.outcome), `${ e.outcome.error }`));
277
- }
316
+ })
317
+ .when(TaskFinished, (e: TaskFinished) => {
278
318
 
279
- this.printer.outdent();
280
319
  this.printer.outdent();
281
320
 
282
- this.firstError.recordIfNeeded(e.outcome.error);
283
- }
321
+ if (e.outcome instanceof ProblemIndication) {
322
+ this.printer.indent();
323
+ this.printer.indent();
284
324
 
285
- else if (! (e.outcome instanceof ExecutionSuccessful)) {
286
- this.printer.indent();
287
- this.printer.println(this.iconFrom(e.outcome), e.details.name.value);
325
+ if (! this.firstErrors.get(e.sceneId.value).alreadyRecorded()) {
326
+ this.printer.println(this.theme.outcome(e.outcome, this.iconFrom(e.outcome), `${ e.outcome.error }`));
327
+ }
288
328
 
289
- this.printer.outdent();
290
- }
329
+ this.printer.outdent();
330
+ this.printer.outdent();
331
+
332
+ this.firstErrors.get(e.sceneId.value).recordIfNeeded(e.outcome.error);
333
+ }
291
334
 
292
- })
293
- .when(SceneFinished, (e: SceneFinished) => {
335
+ else if (! (e.outcome instanceof ExecutionSuccessful)) {
336
+ this.printer.indent();
337
+ this.printer.println(this.iconFrom(e.outcome), e.details.name.value);
294
338
 
295
- this.summary.record(e.details, e.outcome, this.startTimes.eventDurationOf(e));
339
+ this.printer.outdent();
340
+ }
296
341
 
297
- this.printer.println();
298
- this.printer.println(this.theme.outcome(e.outcome, this.formattedOutcome(e, this.deCamelCased(e.outcome.constructor.name))));
342
+ })
343
+ .when(SceneFinished, (e: SceneFinished) => {
299
344
 
300
- if (e.outcome instanceof ProblemIndication) {
345
+ this.summary.record(e.details, e.outcome, this.startTimes.eventDurationOf(e));
301
346
 
302
347
  this.printer.println();
348
+ this.printer.println(this.theme.outcome(e.outcome, this.formattedOutcome(e, this.deCamelCased(e.outcome.constructor.name))));
303
349
 
304
- this.printer.indent();
350
+ if (e.outcome instanceof ProblemIndication) {
351
+
352
+ this.printer.println();
305
353
 
306
- if (e.outcome instanceof ImplementationPending) {
307
- this.printer.println(`${ e.outcome.error.name }: ${ e.outcome.error.message }`);
308
- } else if (e.outcome.error?.stack) {
309
- this.printer.println(e.outcome.error.stack);
354
+ this.printer.indent();
355
+
356
+ if (e.outcome instanceof ImplementationPending) {
357
+ this.printer.println(`${ e.outcome.error.name }: ${ e.outcome.error.message }`);
358
+ } else if (e.outcome.error?.stack) {
359
+ this.printer.println(e.outcome.error.stack);
360
+ }
361
+
362
+ this.printer.outdent();
310
363
  }
311
364
 
312
- this.printer.outdent();
313
- }
365
+ this.artifacts.clear();
366
+ })
367
+ .else((_: DomainEvent) => {
368
+ return void 0;
369
+ });
370
+ }
371
+ }
314
372
 
315
- this.artifacts.clear();
316
- })
317
- .when(TestRunFinished, (_: TestRunFinished) => {
318
- this.printer.println(this.theme.separator('='));
373
+ private printSummary(summary: Summary) {
374
+ this.printer.println(this.theme.separator('='));
319
375
 
320
- this.printer.print(this.summaryFormatter.format(this.summary.aggregated()));
376
+ this.printer.print(this.summaryFormatter.format(summary.aggregated()));
377
+
378
+ this.printer.println(this.theme.separator('='));
379
+ }
321
380
 
322
- this.printer.println(this.theme.separator('='));
323
- })
324
- .else((_: DomainEvent) => {
325
- return void 0;
326
- });
381
+ private isSceneSpecific(event: DomainEvent): event is DomainEvent & { sceneId: CorrelationId } {
382
+ return Object.prototype.hasOwnProperty.call(event, 'sceneId');
327
383
  }
328
384
 
329
385
  private formattedOutcome(event: IdentifiableEvent & { outcome: Outcome }, description: string = event.details.name.value) {
@@ -377,14 +433,22 @@ interface IdentifiableEvent {
377
433
  }
378
434
 
379
435
  class StartTimes {
380
- private times: { [correlationId: string]: Timestamp } = {};
436
+ private times: { [key: string]: Timestamp } = {};
381
437
 
382
438
  recordStartOf(event: IdentifiableEvent) {
383
- this.times[event.details.toString()] = event.timestamp;
439
+ this.times[this.keyFor(event)] = event.timestamp;
384
440
  }
385
441
 
386
442
  eventDurationOf(event: IdentifiableEvent & { outcome: Outcome }): Duration {
387
- return event.timestamp.diff(this.times[event.details.toString()]);
443
+ if (! this.times[this.keyFor(event)]) {
444
+ throw new LogicError(d`StartTime missing for event ${ event }`)
445
+ }
446
+
447
+ return event.timestamp.diff(this.times[this.keyFor(event)]);
448
+ }
449
+
450
+ private keyFor(event: IdentifiableEvent): string {
451
+ return event.details.toString();
388
452
  }
389
453
  }
390
454
 
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Used to configure the {@apilink ConsoleReporter}.
3
+ *
4
+ * @group Stage
5
+ */
6
+ export interface ConsoleReporterConfig {
7
+ /**
8
+ * Choose a colour theme optimised for light, dark, or monochromatic terminals.
9
+ * Or, use 'auto' to automatically pick the most suitable one.
10
+ */
11
+ theme: 'light' | 'dark' | 'mono' | 'auto'
12
+ }
@@ -1,4 +1,4 @@
1
- import { OutputStream } from '@serenity-js/core/lib/io';
1
+ import { OutputStream } from '@serenity-js/core/lib/adapter';
2
2
 
3
3
  /**
4
4
  * @desc
@@ -10,9 +10,6 @@ import { OutputStream } from '@serenity-js/core/lib/io';
10
10
  export class Printer {
11
11
  private indentation = new Indentation(2);
12
12
 
13
- /**
14
- * @param {@serenity-js/core/lib/io~OutputStream} stdout
15
- */
16
13
  constructor(private readonly stdout: OutputStream = process.stdout) {
17
14
  }
18
15
 
@@ -1,9 +1,11 @@
1
- import { Duration } from '@serenity-js/core';
1
+ import { Duration, Timestamp } from '@serenity-js/core';
2
2
  import {
3
3
  ExecutionCompromised,
4
4
  ExecutionFailedWithAssertionError,
5
- ExecutionFailedWithError, ExecutionIgnored,
6
- ExecutionSkipped, ExecutionSuccessful,
5
+ ExecutionFailedWithError,
6
+ ExecutionIgnored,
7
+ ExecutionSkipped,
8
+ ExecutionSuccessful,
7
9
  ImplementationPending,
8
10
  Outcome,
9
11
  ScenarioDetails,
@@ -15,6 +17,9 @@ import {
15
17
  export class Summary {
16
18
  private readonly records: SummaryRecord[] = [];
17
19
 
20
+ private testRunStartedAt: Timestamp = new Timestamp();
21
+ private testRunFinishedAt: Timestamp = new Timestamp();
22
+
18
23
  record(details: ScenarioDetails, outcome: Outcome, duration: Duration): void {
19
24
  this.records.push({ details, outcome, duration });
20
25
  }
@@ -46,7 +51,20 @@ export class Summary {
46
51
  acc.categories[categoryName].totalTime = acc.categories[categoryName].totalTime.plus(record.duration);
47
52
 
48
53
  return acc;
49
- }, { categories: {}, totalTime: Duration.ofMilliseconds(0), numberOfScenarios: this.records.length });
54
+ }, {
55
+ categories: {},
56
+ totalTime: Duration.ofMilliseconds(0),
57
+ realTime: this.testRunFinishedAt.diff(this.testRunStartedAt),
58
+ numberOfScenarios: this.records.length
59
+ });
60
+ }
61
+
62
+ recordTestRunStartedAt(timestamp: Timestamp): void {
63
+ this.testRunStartedAt = timestamp;
64
+ }
65
+
66
+ recordTestRunFinishedAt(timestamp: Timestamp): void {
67
+ this.testRunFinishedAt = timestamp;
50
68
  }
51
69
  }
52
70
 
@@ -56,6 +74,7 @@ export class Summary {
56
74
  export interface AggregatedCategories {
57
75
  categories: {[categoryName: string]: AggregatedCategory};
58
76
  totalTime: Duration;
77
+ realTime: Duration;
59
78
  numberOfScenarios: number;
60
79
  }
61
80