@serenity-js/playwright-test 3.31.17 → 3.32.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 (90) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +6 -5
  3. package/lib/api/PlaywrightTestConfig.d.ts +2 -2
  4. package/lib/api/PlaywrightTestConfig.d.ts.map +1 -1
  5. package/lib/api/WorkerEventStreamReader.d.ts +13 -0
  6. package/lib/api/WorkerEventStreamReader.d.ts.map +1 -0
  7. package/lib/api/WorkerEventStreamReader.js +58 -0
  8. package/lib/api/WorkerEventStreamReader.js.map +1 -0
  9. package/lib/api/WorkerEventStreamWriter.d.ts +24 -0
  10. package/lib/api/WorkerEventStreamWriter.d.ts.map +1 -0
  11. package/lib/api/WorkerEventStreamWriter.js +86 -0
  12. package/lib/api/WorkerEventStreamWriter.js.map +1 -0
  13. package/lib/api/index.d.ts +1 -2
  14. package/lib/api/index.d.ts.map +1 -1
  15. package/lib/api/index.js +1 -2
  16. package/lib/api/index.js.map +1 -1
  17. package/lib/api/serenity-fixtures.d.ts +377 -0
  18. package/lib/api/serenity-fixtures.d.ts.map +1 -0
  19. package/lib/api/{SerenityOptions.js → serenity-fixtures.js} +1 -1
  20. package/lib/api/serenity-fixtures.js.map +1 -0
  21. package/lib/api/test-api.d.ts +27 -15
  22. package/lib/api/test-api.d.ts.map +1 -1
  23. package/lib/api/test-api.js +126 -104
  24. package/lib/api/test-api.js.map +1 -1
  25. package/lib/events/EventFactory.d.ts +16 -0
  26. package/lib/events/EventFactory.d.ts.map +1 -0
  27. package/lib/events/EventFactory.js +94 -0
  28. package/lib/events/EventFactory.js.map +1 -0
  29. package/lib/events/PlaywrightSceneId.d.ts +7 -0
  30. package/lib/events/PlaywrightSceneId.d.ts.map +1 -0
  31. package/lib/events/PlaywrightSceneId.js +19 -0
  32. package/lib/events/PlaywrightSceneId.js.map +1 -0
  33. package/lib/events/index.d.ts +3 -0
  34. package/lib/events/index.d.ts.map +1 -0
  35. package/lib/events/index.js +19 -0
  36. package/lib/events/index.js.map +1 -0
  37. package/lib/reporter/PlaywrightErrorParser.d.ts +7 -0
  38. package/lib/reporter/PlaywrightErrorParser.d.ts.map +1 -0
  39. package/lib/reporter/PlaywrightErrorParser.js +28 -0
  40. package/lib/reporter/PlaywrightErrorParser.js.map +1 -0
  41. package/lib/reporter/PlaywrightEventBuffer.d.ts +25 -0
  42. package/lib/reporter/PlaywrightEventBuffer.d.ts.map +1 -0
  43. package/lib/reporter/PlaywrightEventBuffer.js +147 -0
  44. package/lib/reporter/PlaywrightEventBuffer.js.map +1 -0
  45. package/lib/reporter/PlaywrightTestSceneIdFactory.d.ts +8 -0
  46. package/lib/reporter/PlaywrightTestSceneIdFactory.d.ts.map +1 -0
  47. package/lib/reporter/PlaywrightTestSceneIdFactory.js +15 -0
  48. package/lib/reporter/PlaywrightTestSceneIdFactory.js.map +1 -0
  49. package/lib/reporter/SerenityReporterForPlaywrightTest.d.ts +11 -20
  50. package/lib/reporter/SerenityReporterForPlaywrightTest.d.ts.map +1 -1
  51. package/lib/reporter/SerenityReporterForPlaywrightTest.js +62 -163
  52. package/lib/reporter/SerenityReporterForPlaywrightTest.js.map +1 -1
  53. package/lib/reporter/index.d.ts +0 -2
  54. package/lib/reporter/index.d.ts.map +1 -1
  55. package/lib/reporter/index.js +0 -2
  56. package/lib/reporter/index.js.map +1 -1
  57. package/package.json +9 -9
  58. package/src/api/PlaywrightTestConfig.ts +2 -2
  59. package/src/api/WorkerEventStreamReader.ts +27 -0
  60. package/src/api/WorkerEventStreamWriter.ts +117 -0
  61. package/src/api/index.ts +1 -2
  62. package/src/api/serenity-fixtures.ts +392 -0
  63. package/src/api/test-api.ts +187 -99
  64. package/src/events/EventFactory.ts +204 -0
  65. package/src/events/PlaywrightSceneId.ts +20 -0
  66. package/src/events/index.ts +2 -0
  67. package/src/reporter/PlaywrightErrorParser.ts +35 -0
  68. package/src/reporter/PlaywrightEventBuffer.ts +251 -0
  69. package/src/reporter/PlaywrightTestSceneIdFactory.ts +14 -0
  70. package/src/reporter/SerenityReporterForPlaywrightTest.ts +85 -248
  71. package/src/reporter/index.ts +0 -2
  72. package/lib/api/SerenityFixtures.d.ts +0 -130
  73. package/lib/api/SerenityFixtures.d.ts.map +0 -1
  74. package/lib/api/SerenityFixtures.js +0 -3
  75. package/lib/api/SerenityFixtures.js.map +0 -1
  76. package/lib/api/SerenityOptions.d.ts +0 -271
  77. package/lib/api/SerenityOptions.d.ts.map +0 -1
  78. package/lib/api/SerenityOptions.js.map +0 -1
  79. package/lib/reporter/DomainEventBuffer.d.ts +0 -11
  80. package/lib/reporter/DomainEventBuffer.d.ts.map +0 -1
  81. package/lib/reporter/DomainEventBuffer.js +0 -24
  82. package/lib/reporter/DomainEventBuffer.js.map +0 -1
  83. package/lib/reporter/PlaywrightAttachments.d.ts +0 -2
  84. package/lib/reporter/PlaywrightAttachments.d.ts.map +0 -1
  85. package/lib/reporter/PlaywrightAttachments.js +0 -5
  86. package/lib/reporter/PlaywrightAttachments.js.map +0 -1
  87. package/src/api/SerenityFixtures.ts +0 -132
  88. package/src/api/SerenityOptions.ts +0 -277
  89. package/src/reporter/DomainEventBuffer.ts +0 -28
  90. package/src/reporter/PlaywrightAttachments.ts +0 -1
@@ -0,0 +1,251 @@
1
+ import path from 'node:path';
2
+
3
+ import { type FullConfig } from '@playwright/test';
4
+ import { type TestCase, type TestResult } from '@playwright/test/reporter';
5
+ import { Duration, LogicError, Timestamp } from '@serenity-js/core';
6
+ import {
7
+ type DomainEvent,
8
+ InteractionFinished,
9
+ RetryableSceneDetected,
10
+ SceneFinished,
11
+ SceneTagged
12
+ } from '@serenity-js/core/lib/events';
13
+ import { Path } from '@serenity-js/core/lib/io';
14
+ import type { CorrelationId,Outcome } from '@serenity-js/core/lib/model';
15
+ import {
16
+ ArbitraryTag,
17
+ ExecutionFailedWithAssertionError,
18
+ ExecutionFailedWithError,
19
+ ExecutionIgnored,
20
+ ExecutionSkipped,
21
+ ExecutionSuccessful
22
+ } from '@serenity-js/core/lib/model';
23
+ import { type JSONObject } from 'tiny-types';
24
+
25
+ import { WorkerEventStreamReader } from '../api/WorkerEventStreamReader';
26
+ import { WorkerEventStreamWriter } from '../api/WorkerEventStreamWriter';
27
+ import { EventFactory, PlaywrightSceneId } from '../events';
28
+ import { PlaywrightErrorParser } from './PlaywrightErrorParser';
29
+
30
+ export class PlaywrightEventBuffer {
31
+ private readonly errorParser = new PlaywrightErrorParser();
32
+ private readonly eventStreamReader = new WorkerEventStreamReader();
33
+
34
+ private eventFactory: EventFactory;
35
+ private readonly events = new Map<string, DomainEvent[]>();
36
+ private readonly deferredSceneFinishedEvents = new Map<string, {
37
+ event: SceneFinished;
38
+ outputDirectory: string;
39
+ workerIndex: number;
40
+ }>();
41
+
42
+ configure(config: Pick<FullConfig, 'rootDir'>): void {
43
+ this.eventFactory = new EventFactory(Path.from(config.rootDir));
44
+ }
45
+
46
+ appendTestStart(test: TestCase, result: TestResult): void {
47
+ this.events.set(
48
+ this.sceneId(test, result).value,
49
+ this.eventFactory.createSceneStartEvents(test, result),
50
+ );
51
+ }
52
+
53
+ appendRetryableSceneEvents(test: TestCase, result: TestResult): void {
54
+ const sceneId = this.sceneId(test, result);
55
+ const sceneEndTime = new Timestamp(result.startTime).plus(Duration.ofMilliseconds(result.duration));
56
+
57
+ this.events.get(sceneId.value).push(
58
+ new RetryableSceneDetected(sceneId, sceneEndTime),
59
+ );
60
+
61
+ if (result.retry > 0 || result.status !== 'passed') {
62
+ this.events.get(sceneId.value).push(
63
+ new SceneTagged(
64
+ sceneId,
65
+ new ArbitraryTag('retried'), // todo: replace with a dedicated tag
66
+ sceneEndTime,
67
+ ),
68
+ );
69
+ }
70
+ }
71
+
72
+ deferAppendingSceneFinishedEvent(test: TestCase, result: TestResult): void {
73
+ const sceneId = this.sceneId(test, result);
74
+ const scenarioOutcome = this.outcomeFrom(test, result);
75
+
76
+ this.deferredSceneFinishedEvents.set(sceneId.value, {
77
+ event: this.eventFactory.createSceneFinishedEvent(test, result, scenarioOutcome),
78
+ outputDirectory: test.parent.project().outputDir,
79
+ workerIndex: result.workerIndex,
80
+ });
81
+ }
82
+
83
+ private determineScenarioOutcome(
84
+ worstInteractionOutcome: Outcome,
85
+ scenarioOutcome: Outcome,
86
+ ): Outcome {
87
+ if (worstInteractionOutcome instanceof ExecutionFailedWithAssertionError) {
88
+ return worstInteractionOutcome;
89
+ }
90
+
91
+ return worstInteractionOutcome.isWorseThan(scenarioOutcome)
92
+ ? worstInteractionOutcome
93
+ : scenarioOutcome;
94
+ }
95
+
96
+ appendCrashedWorkerEvents(test: TestCase, result: TestResult): void {
97
+ const workerStreamId = WorkerEventStreamWriter.workerStreamIdFor(result.workerIndex).value;
98
+ const sceneId = this.sceneId(test, result);
99
+
100
+ this.events.get(sceneId.value).push(
101
+ ...this.readEventStream(
102
+ test.parent.project().outputDir,
103
+ workerStreamId,
104
+ sceneId.value,
105
+ ),
106
+ );
107
+ }
108
+
109
+ appendSceneEvents(test: TestCase, result: TestResult): void {
110
+ const sceneId = this.sceneId(test, result);
111
+ this.events.get(sceneId.value).push(
112
+ ...this.readEventStream(test.parent.project().outputDir, sceneId.value),
113
+ );
114
+ }
115
+
116
+ private readEventStream(
117
+ outputDirectory: string,
118
+ streamId: string,
119
+ expectedSceneId: string = streamId,
120
+ ): DomainEvent[] {
121
+ const pathToEventStreamFile = path.join(outputDirectory, 'serenity', streamId, 'events.ndjson');
122
+
123
+ if (this.eventStreamReader.hasStream(pathToEventStreamFile)) {
124
+ return this.eventStreamReader.read(
125
+ pathToEventStreamFile,
126
+ (event: { type: string, value: JSONObject }) => {
127
+ // re-attach events from orphaned beforeAll to the test case
128
+ const hasSceneId = event.value['sceneId'] !== undefined;
129
+ const isAttachedToScene = event.value['sceneId'] === expectedSceneId
130
+ if(hasSceneId && ! isAttachedToScene) {
131
+ event.value['sceneId'] = expectedSceneId;
132
+ }
133
+ return event;
134
+ }
135
+ );
136
+ }
137
+
138
+ return [];
139
+ }
140
+
141
+ appendSceneFinishedEvent(test: TestCase, result: TestResult): void {
142
+ const sceneId = this.sceneId(test, result);
143
+ const worstInteractionOutcome = this.determineWorstInteractionOutcome(this.events.get(sceneId.value));
144
+ const scenarioOutcome = this.determineScenarioOutcome(
145
+ worstInteractionOutcome,
146
+ this.outcomeFrom(test, result),
147
+ );
148
+
149
+ this.events.get(sceneId.value).push(
150
+ this.eventFactory.createSceneFinishedEvent(test, result, scenarioOutcome)
151
+ );
152
+ }
153
+
154
+ flush(test: TestCase, result: TestResult): DomainEvent[] {
155
+ const sceneId = this.sceneId(test, result);
156
+ const events = this.events.get(sceneId.value);
157
+
158
+ if (! events) {
159
+ throw new LogicError(`No events found for test: ${ sceneId.value }`);
160
+ }
161
+
162
+ this.events.delete(sceneId.value);
163
+
164
+ return events;
165
+ }
166
+
167
+ flushAllDeferred(): DomainEvent[] {
168
+ const allEvents = [];
169
+
170
+ for (const [ testId, events ] of this.events.entries()) {
171
+ const scenarioEvents = [];
172
+
173
+ scenarioEvents.push(...events);
174
+
175
+ if (this.deferredSceneFinishedEvents.has(testId)) {
176
+ const lastRecordedEvent = scenarioEvents.at(-1);
177
+ const deferredSceneFinished = this.deferredSceneFinishedEvents.get(testId);
178
+
179
+ const eventStream = this.readEventStream(
180
+ deferredSceneFinished.outputDirectory,
181
+ deferredSceneFinished.event.sceneId.value,
182
+ );
183
+
184
+ const firstEventSinceLastIndex = eventStream.findIndex(event => lastRecordedEvent.equals(event));
185
+ const eventsSinceLast = firstEventSinceLastIndex === -1
186
+ ? eventStream
187
+ : eventStream.slice(firstEventSinceLastIndex);
188
+
189
+ scenarioEvents.push(...eventsSinceLast);
190
+
191
+ const worstInteractionOutcome = this.determineWorstInteractionOutcome(scenarioEvents);
192
+
193
+ const sceneFinishedEvent = new SceneFinished(
194
+ deferredSceneFinished.event.sceneId,
195
+ deferredSceneFinished.event.details,
196
+ this.determineScenarioOutcome(worstInteractionOutcome, deferredSceneFinished.event.outcome),
197
+ deferredSceneFinished.event.timestamp,
198
+ )
199
+
200
+ scenarioEvents.push(sceneFinishedEvent);
201
+ }
202
+
203
+ allEvents.push(...scenarioEvents);
204
+ }
205
+
206
+ this.events.clear();
207
+ this.deferredSceneFinishedEvents.clear();
208
+
209
+ return allEvents;
210
+ }
211
+
212
+ private determineWorstInteractionOutcome(events: DomainEvent[]): Outcome {
213
+ let worstInteractionOutcome: Outcome = new ExecutionSuccessful();
214
+ for (const event of events) {
215
+ if (event instanceof InteractionFinished && event.outcome.isWorseThan(worstInteractionOutcome)) {
216
+ worstInteractionOutcome = event.outcome;
217
+ }
218
+ }
219
+ return worstInteractionOutcome;
220
+ }
221
+
222
+ private outcomeFrom(test: TestCase, result: TestResult): Outcome {
223
+ const outcome = test.outcome();
224
+
225
+ if (outcome === 'skipped') {
226
+ return new ExecutionSkipped();
227
+ }
228
+
229
+ if (outcome === 'unexpected' && result.status === 'passed') {
230
+ return new ExecutionFailedWithError(
231
+ new LogicError(`Scenario expected to fail, but ${ result.status }`),
232
+ );
233
+ }
234
+
235
+ if ([ 'failed', 'interrupted', 'timedOut' ].includes(result.status)) {
236
+ if (test.retries > result.retry) {
237
+ return new ExecutionIgnored(this.errorParser.errorFrom(result.error));
238
+ }
239
+
240
+ return new ExecutionFailedWithError(
241
+ this.errorParser.errorFrom(result.error),
242
+ );
243
+ }
244
+
245
+ return new ExecutionSuccessful();
246
+ }
247
+
248
+ private sceneId(test: TestCase, result: TestResult): CorrelationId {
249
+ return PlaywrightSceneId.from(test.parent.project()?.name, test, result);
250
+ }
251
+ }
@@ -0,0 +1,14 @@
1
+ import type { CorrelationIdFactory } from '@serenity-js/core/lib/model';
2
+ import { CorrelationId } from '@serenity-js/core/lib/model';
3
+
4
+ export class PlaywrightTestSceneIdFactory implements CorrelationIdFactory {
5
+ private testId: CorrelationId;
6
+
7
+ setTestId(testId: string): void {
8
+ this.testId = new CorrelationId(testId);
9
+ }
10
+
11
+ create(): CorrelationId {
12
+ return this.testId;
13
+ }
14
+ }
@@ -1,44 +1,16 @@
1
1
  import type { FullConfig } from '@playwright/test';
2
- import type { Reporter, Suite, TestCase, TestError, TestResult, } from '@playwright/test/reporter';
3
- import type {
4
- ClassDescription,
5
- Serenity,
6
- StageCrewMember,
7
- StageCrewMemberBuilder,
8
- Timestamp,
9
- } from '@serenity-js/core';
10
- import { LogicError, serenity as reporterSerenityInstance, } from '@serenity-js/core';
2
+ import type { FullResult, Reporter, Suite, TestCase, TestError, TestResult, } from '@playwright/test/reporter';
3
+ import type { ClassDescription, StageCrewMember, StageCrewMemberBuilder } from '@serenity-js/core';
4
+ import { Clock, Duration, Serenity, Timestamp } from '@serenity-js/core';
11
5
  import type { OutputStream } from '@serenity-js/core/lib/adapter';
12
- import type { DomainEvent } from '@serenity-js/core/lib/events';
13
- import * as events from '@serenity-js/core/lib/events';
14
- import {
15
- InteractionFinished,
16
- RetryableSceneDetected,
17
- SceneFinished,
18
- SceneStarts,
19
- SceneTagged,
20
- TestRunFinished,
21
- TestRunFinishes,
22
- TestRunnerDetected,
23
- TestRunStarts
24
- } from '@serenity-js/core/lib/events';
25
- import { FileSystem, FileSystemLocation, Path, RequirementsHierarchy, } from '@serenity-js/core/lib/io';
26
- import type { CorrelationId, Outcome, Tag } from '@serenity-js/core/lib/model';
27
- import {
28
- ArbitraryTag,
29
- Category,
30
- ExecutionFailedWithAssertionError,
31
- ExecutionFailedWithError,
32
- ExecutionIgnored,
33
- ExecutionRetriedTag,
34
- ExecutionSkipped,
35
- ExecutionSuccessful,
36
- Name,
37
- ScenarioDetails,
38
- Tags,
39
- } from '@serenity-js/core/lib/model';
40
-
41
- import { SERENITY_JS_DOMAIN_EVENTS_ATTACHMENT_CONTENT_TYPE } from './PlaywrightAttachments';
6
+ import { TestRunFinished, TestRunFinishes, TestRunStarts } from '@serenity-js/core/lib/events';
7
+ import { ExecutionFailedWithError, ExecutionSuccessful, } from '@serenity-js/core/lib/model';
8
+
9
+ import { PlaywrightErrorParser } from './PlaywrightErrorParser';
10
+ import { PlaywrightEventBuffer } from './PlaywrightEventBuffer';
11
+ import { PlaywrightTestSceneIdFactory } from './PlaywrightTestSceneIdFactory';
12
+
13
+ type HookType = 'beforeAll' | 'afterAll' | 'beforeEach' | 'afterEach';
42
14
 
43
15
  /**
44
16
  * Configuration object accepted by `@serenity-js/playwright-test` reporter.
@@ -74,113 +46,101 @@ export interface SerenityReporterForPlaywrightTestConfig {
74
46
  * Serenity/JS [stage crew members](https://serenity-js.org/api/core/interface/StageCrewMember/).
75
47
  */
76
48
  export class SerenityReporterForPlaywrightTest implements Reporter {
77
- private errorParser = new PlaywrightErrorParser();
78
- private sceneIds: Map<string, CorrelationId> = new Map();
49
+ private readonly errorParser = new PlaywrightErrorParser();
50
+
51
+ private readonly sceneIdFactory: PlaywrightTestSceneIdFactory;
52
+ private readonly serenity: Serenity;
79
53
  private unhandledError?: Error;
80
54
 
55
+ private readonly eventBuffer: PlaywrightEventBuffer = new PlaywrightEventBuffer();
56
+ private readonly suiteTestCounts = new Map<Suite, number>();
57
+
81
58
  /**
82
59
  * @param config
83
- * @param serenity
84
- * Instance of [`Serenity`](https://serenity-js.org/api/core/class/Serenity/), specific to the Node process running this Serenity reporter.
85
- * Note that Playwright runs test workers and reporters in separate processes.
86
- * @param requirementsHierarchy
87
- * Root directory of the requirements hierarchy, used to determine capabilities and themes.
88
60
  */
89
- constructor(
90
- config: SerenityReporterForPlaywrightTestConfig,
91
- private readonly serenity: Serenity = reporterSerenityInstance,
92
- private requirementsHierarchy: RequirementsHierarchy = new RequirementsHierarchy(
93
- new FileSystem(Path.from(process.cwd())),
94
- ),
95
- ) {
61
+ constructor(config: SerenityReporterForPlaywrightTestConfig) {
62
+ this.sceneIdFactory = new PlaywrightTestSceneIdFactory();
63
+
64
+ this.serenity = new Serenity(
65
+ new Clock(),
66
+ process.cwd(),
67
+ this.sceneIdFactory,
68
+ )
96
69
  this.serenity.configure(config);
97
70
  }
98
71
 
99
72
  onBegin(config: FullConfig, suite: Suite): void {
100
- this.requirementsHierarchy = new RequirementsHierarchy(
101
- new FileSystem(Path.from(config.rootDir)),
102
- );
73
+ this.eventBuffer.configure(config);
74
+ this.serenity.announce(new TestRunStarts(this.serenity.currentTime()));
103
75
 
104
- this.serenity.announce(new TestRunStarts(this.now()));
76
+ this.countTestsPerSuite(suite);
105
77
  }
106
78
 
107
- onTestBegin(test: TestCase): void {
108
- const currentSceneId = this.serenity.assignNewSceneId();
109
-
110
- this.sceneIds.set(test.id, currentSceneId);
111
-
112
- const { scenarioDetails, scenarioTags } = this.scenarioDetailsFrom(test);
113
-
114
- const tags: Tag[] = [
115
- ... scenarioTags,
116
- ... test.tags.flatMap(tag => Tags.from(tag)),
117
- ];
118
-
119
- this.emit(
120
- new SceneStarts(currentSceneId, scenarioDetails, this.serenity.currentTime()),
79
+ private countTestsPerSuite(suite: Suite): void {
80
+ suite.allTests().forEach(test => {
81
+ let currentSuite: Suite | undefined = test.parent;
82
+ while (currentSuite) {
83
+ const count = this.suiteTestCounts.get(currentSuite) ?? 0;
84
+ this.suiteTestCounts.set(currentSuite, count + 1);
121
85
 
122
- ...this.requirementsHierarchy
123
- .requirementTagsFor(scenarioDetails.location.path, scenarioDetails.category.value)
124
- .map(tag => new SceneTagged(currentSceneId, tag, this.serenity.currentTime())),
125
-
126
- new TestRunnerDetected(
127
- currentSceneId,
128
- new Name('Playwright'),
129
- this.serenity.currentTime(),
130
- ),
86
+ currentSuite = currentSuite.parent;
87
+ }
88
+ });
89
+ }
131
90
 
132
- ...tags.map(tag => new SceneTagged(currentSceneId, tag, this.serenity.currentTime())),
133
- );
91
+ onTestBegin(test: TestCase, result: TestResult): void {
92
+ this.eventBuffer.appendTestStart(test, result);
134
93
  }
135
94
 
136
95
  // TODO might be nice to support that by emitting TestStepStarted / Finished
137
96
  // onStepBegin(test: TestCase, _result: TestResult, step: TestStep): void {
138
97
  // // console.log('>> onStepBegin');
139
98
  // }
99
+ // todo: add stdout -> Log https://github.com/microsoft/playwright/blob/main/packages/playwright/src/reporters/list.ts#L67
140
100
 
141
101
  // onStepEnd(test: TestCase, _result: TestResult, step: TestStep): void {
142
102
  // // console.log('>> onStepEnd');
143
103
  // }
144
104
 
145
105
  onTestEnd(test: TestCase, result: TestResult): void {
146
- this.announceRetryIfNeeded(test, result);
147
106
 
148
- const currentSceneId = this.sceneIds.get(test.id);
107
+ const pendingAfterAllHooks = this.countPendingAfterAllHooks(test);
149
108
 
150
- let worstInteractionOutcome: Outcome = new ExecutionSuccessful();
109
+ if (test.retries > 0) {
110
+ this.eventBuffer.appendRetryableSceneEvents(test, result);
111
+ }
151
112
 
152
- for (const attachment of result.attachments) {
153
- if (! (attachment.contentType === SERENITY_JS_DOMAIN_EVENTS_ATTACHMENT_CONTENT_TYPE && attachment.body)) {
154
- continue;
155
- }
113
+ this.eventBuffer.appendCrashedWorkerEvents(test, result);
114
+ this.eventBuffer.appendSceneEvents(test, result);
156
115
 
157
- const messages = JSON.parse(attachment.body.toString());
116
+ if (pendingAfterAllHooks === 0) {
117
+ this.eventBuffer.appendSceneFinishedEvent(test, result)
158
118
 
159
- for (const message of messages) {
160
- if (message.value.sceneId === 'unknown') {
161
- message.value.sceneId = currentSceneId.value;
162
- }
119
+ const events = this.eventBuffer.flush(test, result);
120
+
121
+ this.serenity.announce(...events);
122
+ }
123
+ else {
124
+ this.eventBuffer.deferAppendingSceneFinishedEvent(test, result);
125
+ }
126
+ }
163
127
 
164
- const event = events[message.type].fromJSON(message.value);
128
+ private countPendingAfterAllHooks(test: TestCase): number {
129
+ let currentSuite: Suite | undefined = test.parent;
130
+ const pendingAfterAllHooks: Suite[] = [];
165
131
 
166
- this.serenity.announce(event);
132
+ while (currentSuite) {
133
+ const remainingSuites = (this.suiteTestCounts.get(currentSuite) || 0) - 1;
134
+ this.suiteTestCounts.set(currentSuite, remainingSuites);
167
135
 
168
- if (event instanceof InteractionFinished && event.outcome.isWorseThan(worstInteractionOutcome)) {
169
- worstInteractionOutcome = event.outcome;
170
- }
136
+ if (remainingSuites === 0 && currentSuite['_hooks'].some((hook: { type: HookType }) => hook.type === 'afterAll')) {
137
+ pendingAfterAllHooks.push(currentSuite);
171
138
  }
172
- }
173
139
 
174
- const scenarioOutcome = this.outcomeFrom(test, result);
140
+ currentSuite = currentSuite.parent;
141
+ }
175
142
 
176
- this.serenity.announce(
177
- new SceneFinished(
178
- currentSceneId,
179
- this.scenarioDetailsFrom(test).scenarioDetails,
180
- this.determineScenarioOutcome(worstInteractionOutcome, scenarioOutcome),
181
- this.now(),
182
- ),
183
- );
143
+ return pendingAfterAllHooks.length;
184
144
  }
185
145
 
186
146
  onError(error: TestError): void {
@@ -189,173 +149,50 @@ export class SerenityReporterForPlaywrightTest implements Reporter {
189
149
  }
190
150
  }
191
151
 
192
- private determineScenarioOutcome(
193
- worstInteractionOutcome: Outcome,
194
- scenarioOutcome: Outcome,
195
- ): Outcome {
196
- if (worstInteractionOutcome instanceof ExecutionFailedWithAssertionError) {
197
- return worstInteractionOutcome;
198
- }
199
-
200
- return worstInteractionOutcome.isWorseThan(scenarioOutcome)
201
- ? worstInteractionOutcome
202
- : scenarioOutcome;
203
- }
204
-
205
- private outcomeFrom(test: TestCase, result: TestResult): Outcome {
206
- const outcome = test.outcome();
207
-
208
- if (outcome === 'skipped') {
209
- return new ExecutionSkipped();
210
- }
152
+ async onEnd(fullResult: FullResult): Promise<void> {
211
153
 
212
- if (outcome === 'unexpected' && result.status === 'passed') {
213
- return new ExecutionFailedWithError(
214
- new LogicError(`Scenario expected to fail, but ${ result.status }`),
215
- );
216
- }
217
-
218
- if ([ 'failed', 'interrupted', 'timedOut' ].includes(result.status)) {
219
- if (test.retries > result.retry) {
220
- return new ExecutionIgnored(this.errorParser.errorFrom(result.error));
221
- }
222
-
223
- return new ExecutionFailedWithError(
224
- this.errorParser.errorFrom(result.error),
225
- );
226
- }
154
+ const deferredEvents = this.eventBuffer.flushAllDeferred();
227
155
 
228
- return new ExecutionSuccessful();
229
- }
156
+ this.serenity.announce(
157
+ ...deferredEvents,
158
+ );
230
159
 
231
- private scenarioDetailsFrom(test: TestCase): { scenarioDetails: ScenarioDetails, scenarioTags: Tag[] } {
232
- const [
233
- root_,
234
- browserName_,
235
- fileName,
236
- describeOrItBlockTitle,
237
- ...nestedTitles
238
- ] = test.titlePath();
239
-
240
- const path = new Path(test.location.file);
241
- const scenarioName = nestedTitles.join(' ').trim();
242
-
243
- const name = scenarioName || describeOrItBlockTitle;
244
- const featureName = scenarioName ? describeOrItBlockTitle : fileName;
245
-
246
- return {
247
- scenarioDetails: new ScenarioDetails(
248
- new Name(Tags.stripFrom(name)),
249
- new Category(Tags.stripFrom(featureName)),
250
- new FileSystemLocation(path, test.location.line, test.location.column),
251
- ),
252
- scenarioTags: Tags.from(`${ featureName } ${ name }`),
253
- };
254
- }
160
+ const fullDuration = Duration.ofMilliseconds(Math.round(fullResult.duration));
161
+ const endTime = new Timestamp(fullResult.startTime).plus(fullDuration);
255
162
 
256
- async onEnd(): Promise<void> {
257
- this.serenity.announce(new TestRunFinishes(this.serenity.currentTime()));
163
+ this.serenity.announce(new TestRunFinishes(endTime));
258
164
 
259
165
  try {
260
166
  await this.serenity.waitForNextCue();
261
167
 
262
- const outcome = this.unhandledError ?
263
- new ExecutionFailedWithError(this.unhandledError)
168
+ const outcome = this.unhandledError
169
+ ? new ExecutionFailedWithError(this.unhandledError)
264
170
  : new ExecutionSuccessful();
265
171
 
266
172
  this.serenity.announce(
267
173
  new TestRunFinished(
268
174
  outcome,
269
- this.serenity.currentTime(),
175
+ endTime,
270
176
  ),
271
177
  );
272
- } catch (error) {
178
+ }
179
+ catch (error) {
273
180
  this.serenity.announce(
274
181
  new TestRunFinished(
275
182
  new ExecutionFailedWithError(error),
276
- this.serenity.currentTime(),
183
+ endTime,
277
184
  ),
278
185
  );
186
+
279
187
  throw error;
280
188
  }
281
189
  }
282
190
 
283
- // TODO emit a text artifact with stdout?
191
+ // TODO emit a text artifact with stdout
284
192
  // reporter.onStdErr(chunk, test, result)
285
193
  // reporter.onStdOut(chunk, test, result)
286
194
 
287
- private emit(...events: DomainEvent[]): void {
288
- events.forEach((event) => {
289
- this.serenity.announce(event);
290
- });
291
- }
292
-
293
- private announceRetryIfNeeded(test: TestCase, result: TestResult): void {
294
- if (test.retries === 0) {
295
- return;
296
- }
297
-
298
- const currentSceneId = this.sceneIds.get(test.id);
299
-
300
- this.emit(
301
- new RetryableSceneDetected(currentSceneId, this.now()),
302
- new SceneTagged(
303
- currentSceneId,
304
- new ArbitraryTag('retried'), // todo: replace with a dedicated tag
305
- this.now(),
306
- ),
307
- );
308
-
309
- if (result.retry > 0) {
310
- this.emit(
311
- new SceneTagged(
312
- currentSceneId,
313
- new ExecutionRetriedTag(result.retry),
314
- this.serenity.currentTime(),
315
- ),
316
- );
317
- }
318
- }
319
-
320
- private now(): Timestamp {
321
- return this.serenity.currentTime();
322
- }
323
-
324
195
  printsToStdio(): boolean {
325
196
  return true;
326
197
  }
327
198
  }
328
-
329
- class PlaywrightErrorParser {
330
- private static ascii = new RegExp(
331
- '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', // eslint-disable-line no-control-regex
332
- 'g',
333
- );
334
-
335
- public errorFrom(testError: TestError): Error {
336
- const message = testError.message && PlaywrightErrorParser.stripAsciiFrom(testError.message);
337
-
338
- let stack = testError.stack && PlaywrightErrorParser.stripAsciiFrom(testError.stack);
339
-
340
- // TODO: Do I need to process it?
341
- // const value = testError.value;
342
-
343
- const prologue = `Error: ${ message }`;
344
- if (stack && message && stack.startsWith(prologue)) {
345
- stack = stack.slice(prologue.length);
346
- }
347
-
348
- if (testError.cause) {
349
- stack += `\nCaused by: ${ this.errorFrom(testError.cause).stack }`;
350
- }
351
-
352
- const error = new Error(message);
353
- error.stack = stack;
354
-
355
- return error;
356
- }
357
-
358
- private static stripAsciiFrom(text: string): string {
359
- return text.replace(this.ascii, '');
360
- }
361
- }
@@ -1,4 +1,2 @@
1
- export * from './DomainEventBuffer';
2
- export * from './PlaywrightAttachments';
3
1
  export * from './PlaywrightStepReporter';
4
2
  export * from './SerenityReporterForPlaywrightTest';