UNPKG

@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
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