@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
118 lines (83 loc) • 3.9 kB
text/typescript
import fs from 'node:fs';
import path from 'node:path';
import { type WorkerInfo } from '@playwright/test';
import type { TestCase } from '@playwright/test/reporter';
import type { Stage, StageCrewMember } from '@serenity-js/core';
import { LogicError } from '@serenity-js/core';
import type { DomainEvent } from '@serenity-js/core/lib/events';
import { CorrelationId } from '@serenity-js/core/lib/model';
import type { JSONObject } from 'tiny-types';
export class WorkerEventStreamWriter implements StageCrewMember {
private readonly beforeAllId: CorrelationId; // = new CorrelationId('unknown');
private activeSceneId: CorrelationId; // = WorkerEventStreamWriter.beforeTest;
private events: Map<TestCase['id'], DomainEvent[]> = new Map();
static workerStreamIdFor(workerIndex: number): CorrelationId {
return new CorrelationId(`worker-${ workerIndex }`);
}
constructor(
private readonly outputDirectory: string,
private readonly workerInfo: WorkerInfo,
private stage?: Stage,
) {
this.beforeAllId = WorkerEventStreamWriter.workerStreamIdFor(this.workerInfo.workerIndex);
this.activeSceneId = this.beforeAllId;
this.events.set(this.beforeAllId.value, []);
}
assignedTo(stage: Stage): StageCrewMember {
this.stage = stage;
return this;
}
notifyOf(event: DomainEvent): void {
if (this.isSceneEvent(event) && ! this.activeSceneExistsFor(event)) {
this.activateScene(event);
}
this.events.get(this.activeSceneId.value).push(event);
}
private isSceneEvent(event: DomainEvent & { sceneId?: CorrelationId }): event is DomainEvent & { sceneId: CorrelationId } {
return event['sceneId'] instanceof CorrelationId;
}
private activeSceneExistsFor(event: DomainEvent & { sceneId: CorrelationId }): boolean {
return this.activeSceneId.equals(event.sceneId);
}
private activateScene(event: DomainEvent & { sceneId: CorrelationId }): void {
this.activeSceneId = event.sceneId;
const testId = event.sceneId.value;
if (! this.events.has(testId)) {
this.events.set(testId, []);
}
this.events.get(testId).push(
...this.events.get(this.beforeAllId.value),
);
this.events.set(this.beforeAllId.value, []);
}
async persistAll(workerBeforeAllSceneId: CorrelationId): Promise<void> {
const testIds = [...this.events.keys()];
await Promise.all(testIds.map(testId => this.persist(testId, workerBeforeAllSceneId)));
}
async persist(testId: TestCase['id'], workerBeforeAllSceneId?: CorrelationId): Promise<void> {
const testOutputDirectory = path.join(this.outputDirectory, testId);
const filePath = path.join(testOutputDirectory, 'events.ndjson');
const events = this.flush(testId);
if (events.length === 0) {
return;
}
await fs.promises.mkdir(testOutputDirectory, { recursive: true })
for (const event of events) {
const shouldReattachToScene = event['sceneId'] && event['sceneId'].equals(workerBeforeAllSceneId);
const type = event.constructor.name;
const value = shouldReattachToScene
? ({ ...(event.toJSON() as JSONObject), sceneId: testId })
: event.toJSON();
const serialisedEvent = JSON.stringify({ type, value }, undefined, 0);
await fs.promises.appendFile(filePath, serialisedEvent + '\n');
}
}
private flush(testId: TestCase['id']): DomainEvent[] {
if (! this.events.has(testId)) {
throw new LogicError(`No events recorded for test with id ${ testId }`);
}
const events = this.events.get(testId);
this.events.set(testId, []);
return events;
}
}