@serenity-js/playwright-test 3.31.17 → 3.32.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 (90) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +6 -5
  3. package/lib/api/PlaywrightTestConfig.d.ts +7 -6
  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 +15 -22
  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 +7 -6
  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 +89 -250
  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
+ }