@serenity-js/playwright-test
Version:
Serenity/JS test runner adapter for Playwright Test, combining Playwright's developer experience with the advanced reporting and automation capabilities of Serenity/JS
143 lines • 7.6 kB
JavaScript
import path from 'node:path';
import { Duration, LogicError, Timestamp } from '@serenity-js/core';
import { InteractionFinished, RetryableSceneDetected, SceneFinished, SceneTagged } from '@serenity-js/core/events';
import { Path } from '@serenity-js/core/io';
import { ArbitraryTag, ExecutionFailedWithAssertionError, ExecutionFailedWithError, ExecutionIgnored, ExecutionSkipped, ExecutionSuccessful } from '@serenity-js/core/model';
import { WorkerEventStreamReader } from '../api/WorkerEventStreamReader.js';
import { WorkerEventStreamWriter } from '../api/WorkerEventStreamWriter.js';
import { EventFactory, PlaywrightSceneId } from '../events/index.js';
import { PlaywrightErrorParser } from './PlaywrightErrorParser.js';
export class PlaywrightEventBuffer {
errorParser = new PlaywrightErrorParser();
eventStreamReader = new WorkerEventStreamReader();
eventFactory;
events = new Map();
deferredSceneFinishedEvents = new Map();
configure(config) {
this.eventFactory = new EventFactory(Path.from(config.rootDir));
}
appendTestStart(test, result) {
this.events.set(this.sceneId(test, result).value, this.eventFactory.createSceneStartEvents(test, result));
}
appendRetryableSceneEvents(test, result) {
const sceneId = this.sceneId(test, result);
const sceneEndTime = new Timestamp(result.startTime).plus(Duration.ofMilliseconds(result.duration));
this.events.get(sceneId.value).push(new RetryableSceneDetected(sceneId, sceneEndTime));
if (result.retry > 0 || result.status !== 'passed') {
this.events.get(sceneId.value).push(new SceneTagged(sceneId, new ArbitraryTag('retried'), // todo: replace with a dedicated tag
sceneEndTime));
}
}
deferAppendingSceneFinishedEvent(test, result) {
const sceneId = this.sceneId(test, result);
const scenarioOutcome = this.outcomeFrom(test, result);
this.deferredSceneFinishedEvents.set(sceneId.value, {
event: this.eventFactory.createSceneFinishedEvent(test, result, scenarioOutcome),
outputDirectory: test.parent.project().outputDir,
workerIndex: result.workerIndex,
});
}
determineScenarioOutcome(worstInteractionOutcome, scenarioOutcome) {
if (worstInteractionOutcome instanceof ExecutionFailedWithAssertionError) {
return worstInteractionOutcome;
}
if (worstInteractionOutcome instanceof ExecutionSkipped) {
return worstInteractionOutcome;
}
return worstInteractionOutcome.isWorseThan(scenarioOutcome)
? worstInteractionOutcome
: scenarioOutcome;
}
appendCrashedWorkerEvents(test, result) {
const workerStreamId = WorkerEventStreamWriter.workerStreamIdFor(result.workerIndex).value;
const sceneId = this.sceneId(test, result);
this.events.get(sceneId.value).push(...this.readEventStream(test.parent.project().outputDir, workerStreamId, sceneId.value));
}
appendSceneEvents(test, result) {
const sceneId = this.sceneId(test, result);
this.events.get(sceneId.value).push(...this.readEventStream(test.parent.project().outputDir, sceneId.value));
}
readEventStream(outputDirectory, streamId, expectedSceneId = streamId) {
const pathToEventStreamFile = path.join(outputDirectory, 'serenity', streamId, 'events.ndjson');
if (this.eventStreamReader.hasStream(pathToEventStreamFile)) {
return this.eventStreamReader.read(pathToEventStreamFile, (event) => {
// re-attach events from orphaned beforeAll to the test case
const hasSceneId = event.value['sceneId'] !== undefined;
const isAttachedToScene = event.value['sceneId'] === expectedSceneId;
if (hasSceneId && !isAttachedToScene) {
event.value['sceneId'] = expectedSceneId;
}
return event;
});
}
return [];
}
appendSceneFinishedEvent(test, result) {
const sceneId = this.sceneId(test, result);
const worstInteractionOutcome = this.determineWorstInteractionOutcome(this.events.get(sceneId.value));
const scenarioOutcome = this.determineScenarioOutcome(worstInteractionOutcome, this.outcomeFrom(test, result));
this.events.get(sceneId.value).push(this.eventFactory.createSceneFinishedEvent(test, result, scenarioOutcome));
}
flush(test, result) {
const sceneId = this.sceneId(test, result);
const events = this.events.get(sceneId.value);
if (!events) {
throw new LogicError(`No events found for test: ${sceneId.value}`);
}
this.events.delete(sceneId.value);
return events;
}
flushAllDeferred() {
const allEvents = [];
for (const [testId, events] of this.events.entries()) {
const scenarioEvents = [];
scenarioEvents.push(...events);
if (this.deferredSceneFinishedEvents.has(testId)) {
const lastRecordedEvent = scenarioEvents.at(-1);
const deferredSceneFinished = this.deferredSceneFinishedEvents.get(testId);
const eventStream = this.readEventStream(deferredSceneFinished.outputDirectory, deferredSceneFinished.event.sceneId.value);
const firstEventSinceLastIndex = eventStream.findIndex(event => lastRecordedEvent.equals(event));
const eventsSinceLast = firstEventSinceLastIndex === -1
? eventStream
: eventStream.slice(firstEventSinceLastIndex);
scenarioEvents.push(...eventsSinceLast);
const worstInteractionOutcome = this.determineWorstInteractionOutcome(scenarioEvents);
const sceneFinishedEvent = new SceneFinished(deferredSceneFinished.event.sceneId, deferredSceneFinished.event.details, this.determineScenarioOutcome(worstInteractionOutcome, deferredSceneFinished.event.outcome), deferredSceneFinished.event.timestamp);
scenarioEvents.push(sceneFinishedEvent);
}
allEvents.push(...scenarioEvents);
}
this.events.clear();
this.deferredSceneFinishedEvents.clear();
return allEvents;
}
determineWorstInteractionOutcome(events) {
let worstInteractionOutcome = new ExecutionSuccessful();
for (const event of events) {
if (event instanceof InteractionFinished && event.outcome.isWorseThan(worstInteractionOutcome)) {
worstInteractionOutcome = event.outcome;
}
}
return worstInteractionOutcome;
}
outcomeFrom(test, result) {
const outcome = test.outcome();
if (outcome === 'skipped') {
return new ExecutionSkipped();
}
if (outcome === 'unexpected' && result.status === 'passed') {
return new ExecutionFailedWithError(new LogicError(`Scenario expected to fail, but ${result.status}`));
}
if (['failed', 'interrupted', 'timedOut'].includes(result.status)) {
if (test.retries > result.retry) {
return new ExecutionIgnored(this.errorParser.errorFrom(result.error));
}
return new ExecutionFailedWithError(this.errorParser.errorFrom(result.error));
}
return new ExecutionSuccessful();
}
sceneId(test, result) {
return PlaywrightSceneId.from(test.parent.project()?.name, test, result);
}
}
//# sourceMappingURL=PlaywrightEventBuffer.js.map