diff --git a/integration/playwright-test/examples/failing/global-error-thrown.spec.ts b/integration/playwright-test/examples/failing/global-error-thrown.spec.ts new file mode 100644 index 00000000000..3e4dd39f946 --- /dev/null +++ b/integration/playwright-test/examples/failing/global-error-thrown.spec.ts @@ -0,0 +1,13 @@ +import { describe, it } from '@serenity-js/playwright-test'; + +describe('Playwright Test reporting', () => { + + describe('A scenario', () => { + + throw new Error('Something happened'); + + it('where this shouldnt be invoked', () => { + throw new Error(`This shouldn't happen`); + }); + }); +}); \ No newline at end of file diff --git a/integration/playwright-test/spec/failing_scenarios.spec.ts b/integration/playwright-test/spec/failing_scenarios.spec.ts index 1bdf4318a89..58429ac1029 100644 --- a/integration/playwright-test/spec/failing_scenarios.spec.ts +++ b/integration/playwright-test/spec/failing_scenarios.spec.ts @@ -1,6 +1,6 @@ import { expect, ifExitCodeIsOtherThan, logOutput, PickEvent } from '@integration/testing-tools'; import { AssertionError, LogicError, TestCompromisedError } from '@serenity-js/core'; -import { SceneFinished, SceneStarts, SceneTagged, TestRunnerDetected } from '@serenity-js/core/lib/events'; +import { SceneFinished, SceneStarts, SceneTagged, TestRunFinished, TestRunnerDetected } from '@serenity-js/core/lib/events'; import { trimmed } from '@serenity-js/core/lib/io'; import { CapabilityTag, ExecutionCompromised, ExecutionFailedWithAssertionError, ExecutionFailedWithError, FeatureTag, Name, ProblemIndication } from '@serenity-js/core/lib/model'; import { describe, it } from 'mocha'; @@ -163,5 +163,20 @@ describe('@serenity-js/playwright-test', function () { }) ; })); + + it('fails with unhandled global exception', () => playwrightTest('--project=default', 'failing/global-error-thrown.spec.ts') + .then(ifExitCodeIsOtherThan(1, logOutput)) + .then(result => { + expect(result.exitCode).to.equal(1); + + PickEvent.from(result.events) + .next(TestRunFinished, event => { + const outcome: ProblemIndication = event.outcome as ProblemIndication; + expect(outcome).to.be.instanceOf(ExecutionFailedWithError); + expect(outcome.error.name).to.equal('Error'); + expect(outcome.error.message).to.equal('Error: Something happened'); + }) + ; + })); }); }); diff --git a/packages/console-reporter/src/stage/crew/console-reporter/ConsoleReporter.ts b/packages/console-reporter/src/stage/crew/console-reporter/ConsoleReporter.ts index 18e35559dda..fd8263e8190 100644 --- a/packages/console-reporter/src/stage/crew/console-reporter/ConsoleReporter.ts +++ b/packages/console-reporter/src/stage/crew/console-reporter/ConsoleReporter.ts @@ -5,7 +5,6 @@ import type { DomainEvent} from '@serenity-js/core/lib/events'; import { ActivityRelatedArtifactGenerated, - GlobalExceptionEncountered, InteractionFinished, InteractionStarts, SceneFinished, @@ -268,11 +267,6 @@ export class ConsoleReporter implements ListensToDomainEvents { this.printScene(event.sceneId); } - if (event instanceof GlobalExceptionEncountered && event.outcome instanceof ProblemIndication) { - // this.globalError.recordIfNeeded(event.outcome.error); - this.printTestRunErrorOutcome(event.outcome); - } - if (event instanceof TestRunFinished) { this.summary.recordTestRunFinishedAt(event.timestamp); diff --git a/packages/core/src/events/GlobalExceptionEncountered.ts b/packages/core/src/events/GlobalExceptionEncountered.ts deleted file mode 100644 index 3a5cdc2fb8b..00000000000 --- a/packages/core/src/events/GlobalExceptionEncountered.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { JSONObject } from 'tiny-types'; - -import type { SerialisedOutcome } from '../model'; -import { Outcome } from '../model'; -import { Timestamp } from '../screenplay'; -import { DomainEvent } from './DomainEvent'; - -/** - * Emitted when the last test in the test suite has finished running - * and it's time for any last-minute reporting activities to take place. - * - * @group Events - */ -export class GlobalExceptionEncountered extends DomainEvent { - static fromJSON(o: JSONObject): GlobalExceptionEncountered { - return new GlobalExceptionEncountered( - Outcome.fromJSON(o.error as SerialisedOutcome), - Timestamp.fromJSON(o.timestamp as string), - ); - } - - constructor( - public readonly error: Outcome, - timestamp?: Timestamp - ) { - super(timestamp); - } -} \ No newline at end of file diff --git a/packages/core/src/events/index.ts b/packages/core/src/events/index.ts index 107f409c2e1..222befd69c2 100644 --- a/packages/core/src/events/index.ts +++ b/packages/core/src/events/index.ts @@ -12,7 +12,6 @@ export * from './BusinessRuleDetected'; export * from './DomainEvent'; export * from './EmitsDomainEvents'; export * from './FeatureNarrativeDetected'; -export * from './GlobalExceptionEncountered'; export * from './InteractionFinished'; export * from './InteractionStarts'; export * from './RetryableSceneDetected'; diff --git a/packages/playwright-test/src/reporter/SerenityReporterForPlaywrightTest.ts b/packages/playwright-test/src/reporter/SerenityReporterForPlaywrightTest.ts index a7904a917b4..88c988ecaa1 100644 --- a/packages/playwright-test/src/reporter/SerenityReporterForPlaywrightTest.ts +++ b/packages/playwright-test/src/reporter/SerenityReporterForPlaywrightTest.ts @@ -21,7 +21,6 @@ import type { OutputStream } from '@serenity-js/core/lib/adapter'; import type { DomainEvent } from '@serenity-js/core/lib/events'; import * as events from '@serenity-js/core/lib/events'; import { - GlobalExceptionEncountered, InteractionFinished, RetryableSceneDetected, SceneFinished, @@ -91,6 +90,7 @@ export interface SerenityReporterForPlaywrightTestConfig { export class SerenityReporterForPlaywrightTest implements Reporter { private errorParser = new PlaywrightErrorParser(); private sceneIds: Map = new Map(); + private unhandledError?: Error; /** * @param config @@ -209,14 +209,7 @@ export class SerenityReporterForPlaywrightTest implements Reporter { } onError(error: TestError): void { - const parsedError = this.errorParser.errorFrom(error); - - this.serenity.announce( - new GlobalExceptionEncountered( - new ExecutionFailedWithError(parsedError), - this.serenity.currentTime() - ), - ); + this.unhandledError = this.errorParser.errorFrom(error); } private determineScenarioOutcome( @@ -284,9 +277,14 @@ export class SerenityReporterForPlaywrightTest implements Reporter { try { await this.serenity.waitForNextCue(); + + const outcome = this.unhandledError? + new ExecutionFailedWithError(this.unhandledError) + : new ExecutionSuccessful() + this.serenity.announce( new TestRunFinished( - new ExecutionSuccessful(), + outcome, this.serenity.currentTime(), ), ); @@ -354,9 +352,16 @@ class PlaywrightErrorParser { ); public errorFrom(testError: TestError): Error { - const message = + let message = testError.message && PlaywrightErrorParser.stripAsciiFrom(testError.message); + + const snippet = + testError.snippet && + PlaywrightErrorParser.stripAsciiFrom(testError.snippet); + + message = snippet ? [message, snippet].join('\n') : message; + let stack = testError.stack && PlaywrightErrorParser.stripAsciiFrom(testError.stack);