@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.
- package/CHANGELOG.md +37 -0
- package/README.md +6 -5
- package/lib/api/PlaywrightTestConfig.d.ts +7 -6
- package/lib/api/PlaywrightTestConfig.d.ts.map +1 -1
- package/lib/api/WorkerEventStreamReader.d.ts +13 -0
- package/lib/api/WorkerEventStreamReader.d.ts.map +1 -0
- package/lib/api/WorkerEventStreamReader.js +58 -0
- package/lib/api/WorkerEventStreamReader.js.map +1 -0
- package/lib/api/WorkerEventStreamWriter.d.ts +24 -0
- package/lib/api/WorkerEventStreamWriter.d.ts.map +1 -0
- package/lib/api/WorkerEventStreamWriter.js +86 -0
- package/lib/api/WorkerEventStreamWriter.js.map +1 -0
- package/lib/api/index.d.ts +1 -2
- package/lib/api/index.d.ts.map +1 -1
- package/lib/api/index.js +1 -2
- package/lib/api/index.js.map +1 -1
- package/lib/api/serenity-fixtures.d.ts +377 -0
- package/lib/api/serenity-fixtures.d.ts.map +1 -0
- package/lib/api/{SerenityOptions.js → serenity-fixtures.js} +1 -1
- package/lib/api/serenity-fixtures.js.map +1 -0
- package/lib/api/test-api.d.ts +27 -15
- package/lib/api/test-api.d.ts.map +1 -1
- package/lib/api/test-api.js +126 -104
- package/lib/api/test-api.js.map +1 -1
- package/lib/events/EventFactory.d.ts +16 -0
- package/lib/events/EventFactory.d.ts.map +1 -0
- package/lib/events/EventFactory.js +94 -0
- package/lib/events/EventFactory.js.map +1 -0
- package/lib/events/PlaywrightSceneId.d.ts +7 -0
- package/lib/events/PlaywrightSceneId.d.ts.map +1 -0
- package/lib/events/PlaywrightSceneId.js +19 -0
- package/lib/events/PlaywrightSceneId.js.map +1 -0
- package/lib/events/index.d.ts +3 -0
- package/lib/events/index.d.ts.map +1 -0
- package/lib/events/index.js +19 -0
- package/lib/events/index.js.map +1 -0
- package/lib/reporter/PlaywrightErrorParser.d.ts +7 -0
- package/lib/reporter/PlaywrightErrorParser.d.ts.map +1 -0
- package/lib/reporter/PlaywrightErrorParser.js +28 -0
- package/lib/reporter/PlaywrightErrorParser.js.map +1 -0
- package/lib/reporter/PlaywrightEventBuffer.d.ts +25 -0
- package/lib/reporter/PlaywrightEventBuffer.d.ts.map +1 -0
- package/lib/reporter/PlaywrightEventBuffer.js +147 -0
- package/lib/reporter/PlaywrightEventBuffer.js.map +1 -0
- package/lib/reporter/PlaywrightTestSceneIdFactory.d.ts +8 -0
- package/lib/reporter/PlaywrightTestSceneIdFactory.d.ts.map +1 -0
- package/lib/reporter/PlaywrightTestSceneIdFactory.js +15 -0
- package/lib/reporter/PlaywrightTestSceneIdFactory.js.map +1 -0
- package/lib/reporter/SerenityReporterForPlaywrightTest.d.ts +15 -22
- package/lib/reporter/SerenityReporterForPlaywrightTest.d.ts.map +1 -1
- package/lib/reporter/SerenityReporterForPlaywrightTest.js +62 -163
- package/lib/reporter/SerenityReporterForPlaywrightTest.js.map +1 -1
- package/lib/reporter/index.d.ts +0 -2
- package/lib/reporter/index.d.ts.map +1 -1
- package/lib/reporter/index.js +0 -2
- package/lib/reporter/index.js.map +1 -1
- package/package.json +9 -9
- package/src/api/PlaywrightTestConfig.ts +7 -6
- package/src/api/WorkerEventStreamReader.ts +27 -0
- package/src/api/WorkerEventStreamWriter.ts +117 -0
- package/src/api/index.ts +1 -2
- package/src/api/serenity-fixtures.ts +392 -0
- package/src/api/test-api.ts +187 -99
- package/src/events/EventFactory.ts +204 -0
- package/src/events/PlaywrightSceneId.ts +20 -0
- package/src/events/index.ts +2 -0
- package/src/reporter/PlaywrightErrorParser.ts +35 -0
- package/src/reporter/PlaywrightEventBuffer.ts +251 -0
- package/src/reporter/PlaywrightTestSceneIdFactory.ts +14 -0
- package/src/reporter/SerenityReporterForPlaywrightTest.ts +89 -250
- package/src/reporter/index.ts +0 -2
- package/lib/api/SerenityFixtures.d.ts +0 -130
- package/lib/api/SerenityFixtures.d.ts.map +0 -1
- package/lib/api/SerenityFixtures.js +0 -3
- package/lib/api/SerenityFixtures.js.map +0 -1
- package/lib/api/SerenityOptions.d.ts +0 -271
- package/lib/api/SerenityOptions.d.ts.map +0 -1
- package/lib/api/SerenityOptions.js.map +0 -1
- package/lib/reporter/DomainEventBuffer.d.ts +0 -11
- package/lib/reporter/DomainEventBuffer.d.ts.map +0 -1
- package/lib/reporter/DomainEventBuffer.js +0 -24
- package/lib/reporter/DomainEventBuffer.js.map +0 -1
- package/lib/reporter/PlaywrightAttachments.d.ts +0 -2
- package/lib/reporter/PlaywrightAttachments.d.ts.map +0 -1
- package/lib/reporter/PlaywrightAttachments.js +0 -5
- package/lib/reporter/PlaywrightAttachments.js.map +0 -1
- package/src/api/SerenityFixtures.ts +0 -132
- package/src/api/SerenityOptions.ts +0 -277
- package/src/reporter/DomainEventBuffer.ts +0 -28
- 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
|
+
}
|