@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.
- package/CHANGELOG.md +26 -0
- package/README.md +6 -5
- package/lib/api/PlaywrightTestConfig.d.ts +2 -2
- 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 +11 -20
- 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 +2 -2
- 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 +85 -248
- 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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
new
|
|
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.
|
|
101
|
-
|
|
102
|
-
);
|
|
73
|
+
this.eventBuffer.configure(config);
|
|
74
|
+
this.serenity.announce(new TestRunStarts(this.serenity.currentTime()));
|
|
103
75
|
|
|
104
|
-
this.
|
|
76
|
+
this.countTestsPerSuite(suite);
|
|
105
77
|
}
|
|
106
78
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
|
107
|
+
const pendingAfterAllHooks = this.countPendingAfterAllHooks(test);
|
|
149
108
|
|
|
150
|
-
|
|
109
|
+
if (test.retries > 0) {
|
|
110
|
+
this.eventBuffer.appendRetryableSceneEvents(test, result);
|
|
111
|
+
}
|
|
151
112
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
113
|
+
this.eventBuffer.appendCrashedWorkerEvents(test, result);
|
|
114
|
+
this.eventBuffer.appendSceneEvents(test, result);
|
|
156
115
|
|
|
157
|
-
|
|
116
|
+
if (pendingAfterAllHooks === 0) {
|
|
117
|
+
this.eventBuffer.appendSceneFinishedEvent(test, result)
|
|
158
118
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
128
|
+
private countPendingAfterAllHooks(test: TestCase): number {
|
|
129
|
+
let currentSuite: Suite | undefined = test.parent;
|
|
130
|
+
const pendingAfterAllHooks: Suite[] = [];
|
|
165
131
|
|
|
166
|
-
|
|
132
|
+
while (currentSuite) {
|
|
133
|
+
const remainingSuites = (this.suiteTestCounts.get(currentSuite) || 0) - 1;
|
|
134
|
+
this.suiteTestCounts.set(currentSuite, remainingSuites);
|
|
167
135
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
136
|
+
if (remainingSuites === 0 && currentSuite['_hooks'].some((hook: { type: HookType }) => hook.type === 'afterAll')) {
|
|
137
|
+
pendingAfterAllHooks.push(currentSuite);
|
|
171
138
|
}
|
|
172
|
-
}
|
|
173
139
|
|
|
174
|
-
|
|
140
|
+
currentSuite = currentSuite.parent;
|
|
141
|
+
}
|
|
175
142
|
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
156
|
+
this.serenity.announce(
|
|
157
|
+
...deferredEvents,
|
|
158
|
+
);
|
|
230
159
|
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
endTime,
|
|
270
176
|
),
|
|
271
177
|
);
|
|
272
|
-
}
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
273
180
|
this.serenity.announce(
|
|
274
181
|
new TestRunFinished(
|
|
275
182
|
new ExecutionFailedWithError(error),
|
|
276
|
-
|
|
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
|
-
}
|
package/src/reporter/index.ts
CHANGED