From ae5bd7efe780894c07451edec030d156c16aa8fa Mon Sep 17 00:00:00 2001 From: Jan Molak Date: Thu, 18 Jun 2020 02:08:13 +0100 Subject: [PATCH] feat(protractor): support for using Mocha with Protractor --- integration/protractor-mocha/.gitignore | 7 + .../protractor-mocha/examples/Actors.js | 9 + .../protractor-mocha/examples/failing.spec.js | 11 + .../multiple_passing_scenarios.spec.js | 24 +++ .../protractor-mocha/examples/passing.spec.js | 9 + .../examples/protractor.conf.js | 50 +++++ .../retries_passing_the_third_time.spec.js | 17 ++ .../examples/screenplay.spec.js | 22 ++ integration/protractor-mocha/package.json | 43 ++++ .../spec/failing_scenario.spec.ts | 41 ++++ .../spec/passing_scenario.spec.ts | 29 +++ .../restarting_browser_between_tests.spec.ts | 107 ++++++++++ .../spec/retrying_failed_scenarios.spec.ts | 32 +++ .../protractor-mocha/spec/screenplay.spec.ts | 33 +++ .../protractor-mocha/spec/using_grep.spec.ts | 29 +++ .../protractor-mocha/src/protractor.ts | 20 ++ .../protractor-mocha/tsconfig-lint.json | 10 + integration/protractor-mocha/tsconfig.json | 13 ++ packages/mocha/spec/exampleTest.ts | 3 + .../spec/mappers/MochaOutcomeMapper.spec.ts | 48 +++++ .../mocha/src/SerenityReporterForMocha.ts | 11 +- packages/mocha/src/adapter/MochaAdapter.ts | 40 ++++ packages/mocha/src/adapter/MochaConfig.ts | 188 ++++++++++++++++++ packages/mocha/src/adapter/index.ts | 4 + packages/mocha/src/index.ts | 1 + .../mocha/src/mappers/MochaOutcomeMapper.ts | 17 ++ packages/protractor/README.md | 12 +- packages/protractor/package.json | 8 +- .../ProtractorFrameworkAdapter.spec.ts | 2 + .../spec/adapter/TestRunnerDetector.spec.ts | 42 +++- .../src/adapter/TestRunnerDetector.ts | 14 +- packages/protractor/src/adapter/run.ts | 2 + .../src/adapter/runners/MochaTestRunner.ts | 18 ++ 33 files changed, 904 insertions(+), 12 deletions(-) create mode 100644 integration/protractor-mocha/.gitignore create mode 100644 integration/protractor-mocha/examples/Actors.js create mode 100644 integration/protractor-mocha/examples/failing.spec.js create mode 100644 integration/protractor-mocha/examples/multiple_passing_scenarios.spec.js create mode 100644 integration/protractor-mocha/examples/passing.spec.js create mode 100644 integration/protractor-mocha/examples/protractor.conf.js create mode 100644 integration/protractor-mocha/examples/retries_passing_the_third_time.spec.js create mode 100644 integration/protractor-mocha/examples/screenplay.spec.js create mode 100644 integration/protractor-mocha/package.json create mode 100644 integration/protractor-mocha/spec/failing_scenario.spec.ts create mode 100644 integration/protractor-mocha/spec/passing_scenario.spec.ts create mode 100644 integration/protractor-mocha/spec/restarting_browser_between_tests.spec.ts create mode 100644 integration/protractor-mocha/spec/retrying_failed_scenarios.spec.ts create mode 100644 integration/protractor-mocha/spec/screenplay.spec.ts create mode 100644 integration/protractor-mocha/spec/using_grep.spec.ts create mode 100644 integration/protractor-mocha/src/protractor.ts create mode 100644 integration/protractor-mocha/tsconfig-lint.json create mode 100644 integration/protractor-mocha/tsconfig.json create mode 100644 packages/mocha/src/adapter/MochaAdapter.ts create mode 100644 packages/mocha/src/adapter/MochaConfig.ts create mode 100644 packages/mocha/src/adapter/index.ts create mode 100644 packages/protractor/src/adapter/runners/MochaTestRunner.ts diff --git a/integration/protractor-mocha/.gitignore b/integration/protractor-mocha/.gitignore new file mode 100644 index 00000000000..5f9b4aea905 --- /dev/null +++ b/integration/protractor-mocha/.gitignore @@ -0,0 +1,7 @@ +# Node +node_modules +*.log + +# Build artifacts +.nyc_output +lib diff --git a/integration/protractor-mocha/examples/Actors.js b/integration/protractor-mocha/examples/Actors.js new file mode 100644 index 00000000000..f78ab0cbfef --- /dev/null +++ b/integration/protractor-mocha/examples/Actors.js @@ -0,0 +1,9 @@ +const { BrowseTheWeb } = require('@serenity-js/protractor'); + +module.exports.Actors = class Actors { + prepare(actor) { + return actor.whoCan( + BrowseTheWeb.using(require('protractor').browser), + ); + } +}; diff --git a/integration/protractor-mocha/examples/failing.spec.js b/integration/protractor-mocha/examples/failing.spec.js new file mode 100644 index 00000000000..516aecc45ff --- /dev/null +++ b/integration/protractor-mocha/examples/failing.spec.js @@ -0,0 +1,11 @@ +const assert = require('assert'); + +describe('Mocha', () => { + + describe('A scenario', () => { + + it('fails', () => { + assert.equal(false, true, 'Expected false to be true.'); + }); + }); +}); diff --git a/integration/protractor-mocha/examples/multiple_passing_scenarios.spec.js b/integration/protractor-mocha/examples/multiple_passing_scenarios.spec.js new file mode 100644 index 00000000000..7bec601eb78 --- /dev/null +++ b/integration/protractor-mocha/examples/multiple_passing_scenarios.spec.js @@ -0,0 +1,24 @@ +const + { actorCalled } = require('@serenity-js/core'), + { UseAngular, Navigate } = require('@serenity-js/protractor'); + +describe('Mocha', () => { + + describe('A scenario', () => { + + it('passes the first time', () => actorCalled('Mocha').attemptsTo( + UseAngular.disableSynchronisation(), + Navigate.to('chrome://version/'), + )); + + it('passes the second time', () => actorCalled('Mocha').attemptsTo( + UseAngular.disableSynchronisation(), + Navigate.to('chrome://accessibility/'), + )); + + it('passes the third time', () => actorCalled('Mocha').attemptsTo( + UseAngular.disableSynchronisation(), + Navigate.to('chrome://chrome-urls/'), + )); + }); +}); diff --git a/integration/protractor-mocha/examples/passing.spec.js b/integration/protractor-mocha/examples/passing.spec.js new file mode 100644 index 00000000000..e2b86b9cb43 --- /dev/null +++ b/integration/protractor-mocha/examples/passing.spec.js @@ -0,0 +1,9 @@ +describe('Mocha', () => { + + describe('A scenario', () => { + + it('passes', () => { + // no-op + }); + }); +}); diff --git a/integration/protractor-mocha/examples/protractor.conf.js b/integration/protractor-mocha/examples/protractor.conf.js new file mode 100644 index 00000000000..bcf8279d6af --- /dev/null +++ b/integration/protractor-mocha/examples/protractor.conf.js @@ -0,0 +1,50 @@ +const + path = require('path'), + { StreamReporter } = require('@serenity-js/core'), + { ChildProcessReporter } = require('@integration/testing-tools'), + { Actors } = require('./Actors'); + +exports.config = { + chromeDriver: require('chromedriver/lib/chromedriver').path, + SELENIUM_PROMISE_MANAGER: false, + + directConnect: true, + + allScriptsTimeout: 11000, + + specs: [ '*.spec.js', ], + + framework: 'custom', + frameworkPath: require.resolve('@serenity-js/protractor/adapter'), + + serenity: { + runner: 'mocha', + actors: new Actors(), + crew: [ + new ChildProcessReporter(), + new StreamReporter(), + ] + }, + + mochaOpts: { + + }, + + onPrepare: function() { + return browser.waitForAngularEnabled(false); + }, + + capabilities: { + browserName: 'chrome', + + chromeOptions: { + args: [ + '--disable-infobars', + '--no-sandbox', + '--disable-gpu', + '--window-size=1024x768', + '--headless', + ], + }, + }, +}; diff --git a/integration/protractor-mocha/examples/retries_passing_the_third_time.spec.js b/integration/protractor-mocha/examples/retries_passing_the_third_time.spec.js new file mode 100644 index 00000000000..8ecda1aaef9 --- /dev/null +++ b/integration/protractor-mocha/examples/retries_passing_the_third_time.spec.js @@ -0,0 +1,17 @@ +describe('Mocha', () => { + + describe('A scenario', () => { + + let counter = 0; + + it('passes the third time', () => { + + if (counter++ < 3) { + throw new Error(`Something's happened`); + } + + // third time lucky, isn't it? + + }); + }); +}); diff --git a/integration/protractor-mocha/examples/screenplay.spec.js b/integration/protractor-mocha/examples/screenplay.spec.js new file mode 100644 index 00000000000..c9a3d020709 --- /dev/null +++ b/integration/protractor-mocha/examples/screenplay.spec.js @@ -0,0 +1,22 @@ +const + { actorCalled } = require('@serenity-js/core'), + { Navigate, UseAngular } = require('@serenity-js/protractor'); + +describe('Mocha', () => { + + describe('A screenplay scenario', () => { + + beforeEach(() => actorCalled('Mocha').attemptsTo( + UseAngular.disableSynchronisation(), + Navigate.to('chrome://version/'), + )); + + it('passes', () => actorCalled('Mocha').attemptsTo( + Navigate.to('chrome://accessibility/'), + )); + + afterEach(() => actorCalled('Mocha').attemptsTo( + Navigate.to('chrome://chrome-urls/'), + )); + }); +}); diff --git a/integration/protractor-mocha/package.json b/integration/protractor-mocha/package.json new file mode 100644 index 00000000000..5a25c54ed24 --- /dev/null +++ b/integration/protractor-mocha/package.json @@ -0,0 +1,43 @@ +{ + "name": "@integration/protractor-mocha", + "version": "2.10.2", + "description": "Protractor/Mocha integration tests", + "author": { + "name": "Jan Molak", + "email": "jan.molak@smartcodeltd.co.uk", + "url": "https://janmolak.com" + }, + "homepage": "https://serenity-js.org", + "license": "Apache-2.0", + "private": true, + "config": { + "access": "private" + }, + "scripts": { + "clean": "rimraf lib", + "lint": "tslint --project tsconfig-lint.json --config ../../tslint.json --format stylish", + "test": "mocha --config ../../.mocharc.yml 'spec/**/*.spec.*'" + }, + "repository": { + "type": "git", + "url": "https://github.com/serenity-js/serenity-js.git" + }, + "bugs": { + "url": "https://github.com/serenity-js/serenity-js/issues" + }, + "engines": { + "node": ">= 10", + "npm": ">= 6" + }, + "dependencies": { + "@integration/testing-tools": "2.10.2", + "@serenity-js/core": "2.10.2", + "@serenity-js/mocha": "2.10.2", + "@serenity-js/protractor": "2.10.2", + "chromedriver": "^83.0.0", + "protractor": "^7.0.0" + }, + "devDependencies": { + "@types/mocha": "^7.0.2" + } +} diff --git a/integration/protractor-mocha/spec/failing_scenario.spec.ts b/integration/protractor-mocha/spec/failing_scenario.spec.ts new file mode 100644 index 00000000000..f7c1b841dc4 --- /dev/null +++ b/integration/protractor-mocha/spec/failing_scenario.spec.ts @@ -0,0 +1,41 @@ +import 'mocha'; + +import { expect, ifExitCodeIsOtherThan, logOutput, PickEvent } from '@integration/testing-tools'; +import { AssertionError } from '@serenity-js/core'; +import { SceneFinished, SceneStarts, SceneTagged, TestRunnerDetected } from '@serenity-js/core/lib/events'; +import { ExecutionFailedWithAssertionError, FeatureTag, Name, ProblemIndication } from '@serenity-js/core/lib/model'; +import { protractor } from '../src/protractor'; + +describe('@serenity-js/mocha', function () { + + this.timeout(30000); + + it('recognises a failing scenario', () => + protractor( + './examples/protractor.conf.js', + '--specs=examples/failing.spec.js', + ) + .then(ifExitCodeIsOtherThan(1, logOutput)) + .then(res => { + + expect(res.exitCode).to.equal(1); + + PickEvent.from(res.events) + .next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario fails'))) + .next(SceneTagged, event => expect(event.tag).to.equal(new FeatureTag('Mocha'))) + .next(TestRunnerDetected, event => expect(event.value).to.equal(new Name('Mocha'))) + .next(SceneFinished, event => { + const outcome = event.outcome as ProblemIndication; + + expect(outcome).to.be.instanceOf(ExecutionFailedWithAssertionError); + + const error = outcome.error as AssertionError; + + expect(error.constructor.name).to.match(/^AssertionError/); + expect(error.message).to.equal('Expected false to be true.'); + expect(error.expected).to.equal('true'); + expect(error.actual).to.equal('false'); + }) + ; + })); +}); diff --git a/integration/protractor-mocha/spec/passing_scenario.spec.ts b/integration/protractor-mocha/spec/passing_scenario.spec.ts new file mode 100644 index 00000000000..0bff1214d66 --- /dev/null +++ b/integration/protractor-mocha/spec/passing_scenario.spec.ts @@ -0,0 +1,29 @@ +import 'mocha'; + +import { expect, ifExitCodeIsOtherThan, logOutput, PickEvent } from '@integration/testing-tools'; +import { SceneFinished, SceneStarts, SceneTagged, TestRunnerDetected } from '@serenity-js/core/lib/events'; +import { ExecutionSuccessful, FeatureTag, Name } from '@serenity-js/core/lib/model'; +import { protractor } from '../src/protractor'; + +describe('@serenity-js/mocha', function () { + + this.timeout(30000); + + it('recognises a passing scenario', () => + protractor( + './examples/protractor.conf.js', + '--specs=examples/passing.spec.js', + ) + .then(ifExitCodeIsOtherThan(0, logOutput)) + .then(res => { + + expect(res.exitCode).to.equal(0); + + PickEvent.from(res.events) + .next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario passes'))) + .next(SceneTagged, event => expect(event.tag).to.equal(new FeatureTag('Mocha'))) + .next(TestRunnerDetected, event => expect(event.value).to.equal(new Name('Mocha'))) + .next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful())) + ; + })); +}); diff --git a/integration/protractor-mocha/spec/restarting_browser_between_tests.spec.ts b/integration/protractor-mocha/spec/restarting_browser_between_tests.spec.ts new file mode 100644 index 00000000000..170fc59eccc --- /dev/null +++ b/integration/protractor-mocha/spec/restarting_browser_between_tests.spec.ts @@ -0,0 +1,107 @@ +import { expect, ifExitCodeIsOtherThan, logOutput, PickEvent } from '@integration/testing-tools'; +import { AsyncOperationAttempted, AsyncOperationCompleted, InteractionStarts, SceneFinished, SceneFinishes, SceneStarts, TestRunFinished } from '@serenity-js/core/lib/events'; +import { Description, ExecutionSuccessful, Name } from '@serenity-js/core/lib/model'; +import 'mocha'; +import { protractor } from '../src/protractor'; + +describe('@serenity-js/Mocha', function () { + + /* + * See: + * - https://github.com/angular/protractor/issues/3234 + * - https://github.com/jan-molak/serenity-js/issues/56 + */ + + this.timeout(30000); + + it('supports restarting the browser between test scenarios', () => + protractor( + './examples/protractor.conf.js', + '--specs=examples/multiple_passing_scenarios.spec.js', + '--restartBrowserBetweenTests', + ) + .then(ifExitCodeIsOtherThan(0, logOutput)) + .then(res => { + + expect(res.exitCode).to.equal(0); + + PickEvent.from(res.events) + .next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario passes the first time'))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha disables synchronisation with Angular`))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha navigates to 'chrome://version/'`))) + .next(SceneFinishes, event => expect(event.value.name).to.equal(new Name('A scenario passes the first time'))) + .next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] Invoking ProtractorRunner.afterEach...'))) + .next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Mocha discards abilities...'))) + .next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Mocha discarded abilities successfully'))) + .next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] ProtractorRunner.afterEach succeeded'))) + .next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful())) + + .next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario passes the second time'))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha disables synchronisation with Angular`))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha navigates to 'chrome://accessibility/'`))) + .next(SceneFinishes, event => expect(event.value.name).to.equal(new Name('A scenario passes the second time'))) + .next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] Invoking ProtractorRunner.afterEach...'))) + .next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Mocha discards abilities...'))) + .next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Mocha discarded abilities successfully'))) + .next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] ProtractorRunner.afterEach succeeded'))) + .next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful())) + + .next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario passes the third time'))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha disables synchronisation with Angular`))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha navigates to 'chrome://chrome-urls/'`))) + .next(SceneFinishes, event => expect(event.value.name).to.equal(new Name('A scenario passes the third time'))) + .next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] Invoking ProtractorRunner.afterEach...'))) + .next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Mocha discards abilities...'))) + .next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Mocha discarded abilities successfully'))) + .next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] ProtractorRunner.afterEach succeeded'))) + .next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful())) + + .next(TestRunFinished, event => expect(event.timestamp).to.not.be.undefined) + ; + })); + + it('produces the same result when the browser is not restarted between the tests', () => + protractor( + './examples/protractor.conf.js', + '--specs=examples/multiple_passing_scenarios.spec.js', + ) + .then(ifExitCodeIsOtherThan(0, logOutput)) + .then(res => { + + expect(res.exitCode).to.equal(0); + + PickEvent.from(res.events) + .next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario passes the first time'))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha disables synchronisation with Angular`))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha navigates to 'chrome://version/'`))) + .next(SceneFinishes, event => expect(event.value.name).to.equal(new Name('A scenario passes the first time'))) + .next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] Invoking ProtractorRunner.afterEach...'))) + .next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Mocha discards abilities...'))) + .next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Mocha discarded abilities successfully'))) + .next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] ProtractorRunner.afterEach succeeded'))) + .next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful())) + + .next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario passes the second time'))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha disables synchronisation with Angular`))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha navigates to 'chrome://accessibility/'`))) + .next(SceneFinishes, event => expect(event.value.name).to.equal(new Name('A scenario passes the second time'))) + .next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] Invoking ProtractorRunner.afterEach...'))) + .next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Mocha discards abilities...'))) + .next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Mocha discarded abilities successfully'))) + .next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] ProtractorRunner.afterEach succeeded'))) + .next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful())) + + .next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario passes the third time'))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha disables synchronisation with Angular`))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha navigates to 'chrome://chrome-urls/'`))) + .next(SceneFinishes, event => expect(event.value.name).to.equal(new Name('A scenario passes the third time'))) + .next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] Invoking ProtractorRunner.afterEach...'))) + .next(AsyncOperationAttempted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Mocha discards abilities...'))) + .next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[Actor] Mocha discarded abilities successfully'))) + .next(AsyncOperationCompleted, event => expect(event.taskDescription).to.equal(new Description('[ProtractorReporter] ProtractorRunner.afterEach succeeded'))) + .next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful())) + + .next(TestRunFinished, event => expect(event.timestamp).to.not.be.undefined) + ; + })); +}); diff --git a/integration/protractor-mocha/spec/retrying_failed_scenarios.spec.ts b/integration/protractor-mocha/spec/retrying_failed_scenarios.spec.ts new file mode 100644 index 00000000000..30e26d215cd --- /dev/null +++ b/integration/protractor-mocha/spec/retrying_failed_scenarios.spec.ts @@ -0,0 +1,32 @@ +import 'mocha'; + +import { expect, ifExitCodeIsOtherThan, logOutput, PickEvent } from '@integration/testing-tools'; +import { AssertionError } from '@serenity-js/core'; +import { SceneFinished, SceneStarts, SceneTagged, TestRunnerDetected } from '@serenity-js/core/lib/events'; +import { ExecutionFailedWithAssertionError, ExecutionFailedWithError, ExecutionSuccessful, FeatureTag, Name, ProblemIndication } from '@serenity-js/core/lib/model'; +import { protractor } from '../src/protractor'; + +describe('@serenity-js/mocha', function () { + + this.timeout(30000); + + it('recognises a retryable scenario', () => + protractor( + './examples/protractor.conf.js', + '--specs=examples/retries_passing_the_third_time.spec.js', + '--mochaOpts.retries=3', + ) + .then(ifExitCodeIsOtherThan(1, logOutput)) + .then(res => { + + expect(res.exitCode).to.equal(1); // Protractor will still report 3 failed attempts as a scenario failure + + PickEvent.from(res.events) + .next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A scenario passes the third time'))) + .next(SceneFinished, event => expect(event.outcome).to.be.instanceOf(ExecutionFailedWithError)) + .next(SceneFinished, event => expect(event.outcome).to.be.instanceOf(ExecutionFailedWithError)) + .next(SceneFinished, event => expect(event.outcome).to.be.instanceOf(ExecutionFailedWithError)) + .next(SceneFinished, event => expect(event.outcome).to.be.instanceOf(ExecutionSuccessful)) + ; + })); +}); diff --git a/integration/protractor-mocha/spec/screenplay.spec.ts b/integration/protractor-mocha/spec/screenplay.spec.ts new file mode 100644 index 00000000000..d151dd9de99 --- /dev/null +++ b/integration/protractor-mocha/spec/screenplay.spec.ts @@ -0,0 +1,33 @@ +import 'mocha'; + +import { expect, ifExitCodeIsOtherThan, logOutput, PickEvent } from '@integration/testing-tools'; +import { InteractionStarts, SceneFinished, SceneStarts, SceneTagged, TestRunnerDetected } from '@serenity-js/core/lib/events'; +import { ExecutionSuccessful, FeatureTag, Name } from '@serenity-js/core/lib/model'; +import { protractor } from '../src/protractor'; + +describe('@serenity-js/mocha', function () { + + this.timeout(30000); + + it('correctly reports on Screenplay scenarios', () => + protractor( + './examples/protractor.conf.js', + '--specs=examples/screenplay.spec.js', + ) + .then(ifExitCodeIsOtherThan(0, logOutput)) + .then(res => { + + expect(res.exitCode).to.equal(0); + + PickEvent.from(res.events) + .next(SceneStarts, event => expect(event.value.name).to.equal(new Name('A screenplay scenario passes'))) + .next(SceneTagged, event => expect(event.tag).to.equal(new FeatureTag('Mocha'))) + .next(TestRunnerDetected, event => expect(event.value).to.equal(new Name('Mocha'))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha disables synchronisation with Angular`))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha navigates to 'chrome://version/'`))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha navigates to 'chrome://accessibility/'`))) + .next(InteractionStarts, event => expect(event.value.name).to.equal(new Name(`Mocha navigates to 'chrome://chrome-urls/'`))) + .next(SceneFinished, event => expect(event.outcome).to.equal(new ExecutionSuccessful())) + ; + })); +}); diff --git a/integration/protractor-mocha/spec/using_grep.spec.ts b/integration/protractor-mocha/spec/using_grep.spec.ts new file mode 100644 index 00000000000..f647cba46cd --- /dev/null +++ b/integration/protractor-mocha/spec/using_grep.spec.ts @@ -0,0 +1,29 @@ +import 'mocha'; + +import { expect, ifExitCodeIsOtherThan, logOutput } from '@integration/testing-tools'; +import { TestRunFinished, TestRunFinishes } from '@serenity-js/core/lib/events'; +import { protractor } from '../src/protractor'; + +describe('@serenity-js/mocha', function () { + + this.timeout(30000); + + it('allows for selective execution of scenarios via grep', () => + protractor( + './examples/protractor.conf.js', + '--specs=examples/failing.spec.js', + '--mochaOpts.grep=".*passes.*"', + ) + .then(ifExitCodeIsOtherThan(0, logOutput)) + .then(res => { + + expect(res.exitCode).to.equal(0); + + // Unlike Jasmine, Mocha won't even touch the scenarios that have been excluded using grep + // so they won't emit any events + expect(res.events).to.have.lengthOf(2); + + expect(res.events[0]).to.be.instanceOf(TestRunFinishes); + expect(res.events[1]).to.be.instanceOf(TestRunFinished); + })); +}); diff --git a/integration/protractor-mocha/src/protractor.ts b/integration/protractor-mocha/src/protractor.ts new file mode 100644 index 00000000000..02d96fd7823 --- /dev/null +++ b/integration/protractor-mocha/src/protractor.ts @@ -0,0 +1,20 @@ +import { spawner, SpawnResult } from '@integration/testing-tools'; +import * as path from 'path'; + +const protractorExecutable = path.resolve( + require.resolve('protractor/package.json'), + '..', + 'bin', + 'protractor', +); + +const protractorSpawner = spawner( + protractorExecutable, + { cwd: path.resolve(__dirname, '..') }, +); + +export function protractor(...params: string[]): Promise { + return protractorSpawner( + ...params, + ); +} diff --git a/integration/protractor-mocha/tsconfig-lint.json b/integration/protractor-mocha/tsconfig-lint.json new file mode 100644 index 00000000000..84a2f6ded92 --- /dev/null +++ b/integration/protractor-mocha/tsconfig-lint.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig", + "include": [ + "src/**/*.ts", + "spec/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/integration/protractor-mocha/tsconfig.json b/integration/protractor-mocha/tsconfig.json new file mode 100644 index 00000000000..15283aeff8a --- /dev/null +++ b/integration/protractor-mocha/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2018", + "lib": ["es2018"], + "module": "commonjs", + "sourceMap": true, + "declaration": true + }, + + "exclude": [ + "node_modules" + ] +} diff --git a/packages/mocha/spec/exampleTest.ts b/packages/mocha/spec/exampleTest.ts index acf09cfdc60..abb961a996c 100644 --- a/packages/mocha/spec/exampleTest.ts +++ b/packages/mocha/spec/exampleTest.ts @@ -1,6 +1,7 @@ import { Test } from 'mocha'; export const exampleTest = { + 'state': 'passed', 'type': 'test', 'title': 'passes', 'body': '() => {\n\n }', @@ -13,6 +14,8 @@ export const exampleTest = { 'timedOut': false, '_currentRetry': 0, 'pending': false, + isPassed: () => ! exampleTest.pending && exampleTest.state === 'passed', + isFailed: () => ! exampleTest.pending && exampleTest.state === 'failed', isPending: () => exampleTest.pending, 'file': '/Users/jan/Projects/serenity-js/integration/mocha/examples/passing.spec.js', 'parent': { diff --git a/packages/mocha/spec/mappers/MochaOutcomeMapper.spec.ts b/packages/mocha/spec/mappers/MochaOutcomeMapper.spec.ts index 8f028749d5b..95c69e77538 100644 --- a/packages/mocha/spec/mappers/MochaOutcomeMapper.spec.ts +++ b/packages/mocha/spec/mappers/MochaOutcomeMapper.spec.ts @@ -109,4 +109,52 @@ describe('MochaTestMapper', () => { expect(outcome).to.be.instanceof(ExecutionFailedWithError); expect((outcome as ProblemIndication).error).to.equal(error); }); + + describe('when working with retryable tests', () => { + + it('recognises the first failure (singular)', () => { + + const test = new Test('example', someScenario); + test.isPending = () => false; + test.isPassed = () => false; + test.isFailed = () => false; + test.retries = () => 1; + (test as any).currentRetry = () => 0; + + const outcome = mapper.outcomeOf(test); + + expect(outcome).to.be.instanceof(ExecutionFailedWithError); + expect((outcome as ProblemIndication).error.message).to.equal('Execution failed, 1 retry left.'); + }); + + it('recognises the first failure (plural)', () => { + + const test = new Test('example', someScenario); + test.isPending = () => false; + test.isPassed = () => false; + test.isFailed = () => false; + test.retries = () => 2; + (test as any).currentRetry = () => 0; + + const outcome = mapper.outcomeOf(test); + + expect(outcome).to.be.instanceof(ExecutionFailedWithError); + expect((outcome as ProblemIndication).error.message).to.equal('Execution failed, 2 retries left.'); + }); + + it('recognises a failed retry attempt', () => { + + const test = new Test('example', someScenario); + test.isPending = () => false; + test.isPassed = () => false; + test.isFailed = () => false; + test.retries = () => 2; + (test as any).currentRetry = () => 1; + + const outcome = mapper.outcomeOf(test); + + expect(outcome).to.be.instanceof(ExecutionFailedWithError); + expect((outcome as ProblemIndication).error.message).to.equal('Retry 1 of 2 failed.'); + }); + }); }); diff --git a/packages/mocha/src/SerenityReporterForMocha.ts b/packages/mocha/src/SerenityReporterForMocha.ts index 00826fea267..08270e35ae3 100644 --- a/packages/mocha/src/SerenityReporterForMocha.ts +++ b/packages/mocha/src/SerenityReporterForMocha.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { Serenity } from '@serenity-js/core'; import { DomainEvent, SceneFinished, SceneFinishes, SceneStarts, SceneTagged, TestRunFinished, TestRunFinishes, TestRunnerDetected } from '@serenity-js/core/lib/events'; import { FeatureTag, Name } from '@serenity-js/core/lib/model'; @@ -33,13 +35,13 @@ export class SerenityReporterForMocha extends reporters.Base { runner.on(Runner.constants.EVENT_TEST_PASS, (test: Test) => { - this.recorder.finished(test.ctx.currentTest || test, this.outcomeMapper.outcomeOf(test)) + this.recorder.finished(!! test.ctx ? test.ctx.currentTest : test, this.outcomeMapper.outcomeOf(test)) }, ); runner.on(Runner.constants.EVENT_TEST_FAIL, (test: Test, err: Error) => { - this.recorder.finished(test.ctx.currentTest || test, this.outcomeMapper.outcomeOf(test)) + this.recorder.finished(!! test.ctx ? test.ctx.currentTest : test, this.outcomeMapper.outcomeOf(test)) }, ); @@ -49,6 +51,11 @@ export class SerenityReporterForMocha extends reporters.Base { return announceSceneFinishedFor(this.currentTest); }); + // https://github.com/cypress-io/cypress/issues/7562 + runner.on('test:after:run', (test: Test) => { + return announceSceneFinishedFor(test); + }); + // Tests without body don't trigger the above custom afterEach hook runner.on(Runner.constants.EVENT_TEST_PENDING, (test: Test) => { diff --git a/packages/mocha/src/adapter/MochaAdapter.ts b/packages/mocha/src/adapter/MochaAdapter.ts new file mode 100644 index 00000000000..aad15f84eb9 --- /dev/null +++ b/packages/mocha/src/adapter/MochaAdapter.ts @@ -0,0 +1,40 @@ +/* istanbul ignore file */ + +import { ModuleLoader } from '@serenity-js/core/lib/io'; +import { MochaConfig } from './MochaConfig'; + +/** + * @desc + * Allows for programmatic execution of Mocha test scenarios, + * using {@link SerenityReporterForMocha} to report progress. + */ +export class MochaAdapter { + + constructor( + private readonly config: MochaConfig, + private readonly loader: ModuleLoader, + ) { + } + + /** + * @param {string[]} pathsToScenarios + * @returns {Promise} + */ + run(pathsToScenarios: string[]): Promise { + return new Promise((resolve, reject) => { + const + Mocha = this.loader.require('mocha'), + mocha = new Mocha({ + ...this.config, + reporter: require.resolve('../index'), + }); + + mocha.files = pathsToScenarios; + + mocha.loadFilesAsync() + .then(() => + mocha.run(numberOfFailures => resolve()) + ); + }); + } +} diff --git a/packages/mocha/src/adapter/MochaConfig.ts b/packages/mocha/src/adapter/MochaConfig.ts new file mode 100644 index 00000000000..0da20377274 --- /dev/null +++ b/packages/mocha/src/adapter/MochaConfig.ts @@ -0,0 +1,188 @@ +/** + * @desc + * Configuration object for the Mocha test runner. + * + * @see https://github.com/mochajs/mocha/blob/v8.0.1/example/config/.mocharc.yml + */ +export interface MochaConfig { + + /** + * @desc + * Path to config file. + * + * @see https://github.com/mochajs/mocha/tree/v8.0.1/example/config + * + * @type {string | undefined} + * @public + */ + config?: string; + + /** + * @desc + * Allow uncaught errors to propagate. + * + * @type {boolean | undefined} + * @public + */ + 'allow-uncaught'?: boolean; + + /** + * @desc + * Require all tests to use a callback (async) or return a Promise. + * + * @type {boolean | undefined} + * @public + */ + 'async-only'?: boolean; + + /** + * @desc + * Abort ("bail") after first test failure. + * + * @type {boolean | undefined} + * @public + */ + bail?: boolean; + + /** + * @desc + * Check for global variable leaks. + * + * @type {boolean | undefined} + * @public + */ + 'check-leaks'?: boolean; + + /** + * @desc + * Delay initial execution of root suite. + * + * @type {boolean | undefined} + * @public + */ + delay?: boolean; + + /** + * @desc + * Only run tests containing this string. + * Please note: {@link MochaConfig.fgrep} and {@link MochaConfig.grep} are mutually exclusive. + * + * @type {string | undefined} + * @public + */ + fgrep?: string; + + /** + * @desc + * File(s) to be loaded prior to root suite execution. + * + * @type {string[] | undefined} + * @public + */ + file?: string[]; + + /** + * @desc + * Fail if exclusive test(s) encountered. + * + * @type {boolean | undefined} + * @public + */ + 'forbid-only'?: boolean; + + /** + * @desc + * Fail if pending test(s) encountered. + * + * @type {boolean | undefined} + * @public + */ + 'forbid-pending': boolean; + + /** + * @desc + * List of allowed global variables. + * + * @type {string[] | undefined} + * @public + */ + global?: string[]; + + /** + * @desc + * Only run tests matching this string or regexp. + * Please note: {@link MochaConfig.fgrep} and {@link MochaConfig.grep} are mutually exclusive. + * + * @type {string | RegExp | undefined} + * @public + */ + grep?: string | RegExp; + + /** + * @desc + * Enable Growl notifications. + * + * @type {boolean | undefined} + * @public + */ + growl?: boolean; + + /** + * @desc + * Inverts {@link MochaConfig.grep} and {@link MochaConfig.fgrep} matches. + * + * @type {boolean | undefined} + * @public + */ + invert?: boolean; + + /** + * @desc + * Require module. + * + * @type {string[] | undefined} + * @public + */ + require?: string[]; + + /** + * @desc + * Retry failed tests this many times. + * + * @todo: will this work with Protractor? + * + * @type {number | undefined} + * @public + */ + retries?: number; + + /** + * @desc + * Specify "slow" test threshold (in milliseconds). + * + * @type {number} [slow=75] + * @public + */ + slow?: number; + + /** + * @desc + * Specify test timeout threshold (in milliseconds). + * Please note: setting this property to 0 means "no timeout". + * + * @type {number} [timeout=2000] + * @public + */ + timeout?: number; + + /** + * @desc + * Specify user interface. + * + * @see https://mochajs.org/#interfaces + * + * @type {string} [ui="bdd"] + * @public + */ + ui?: string; +} diff --git a/packages/mocha/src/adapter/index.ts b/packages/mocha/src/adapter/index.ts new file mode 100644 index 00000000000..be958cfb623 --- /dev/null +++ b/packages/mocha/src/adapter/index.ts @@ -0,0 +1,4 @@ +/* istanbul ignore file */ + +export * from './MochaAdapter'; +export * from './MochaConfig'; diff --git a/packages/mocha/src/index.ts b/packages/mocha/src/index.ts index 4eafac89151..6cdef7adc0d 100644 --- a/packages/mocha/src/index.ts +++ b/packages/mocha/src/index.ts @@ -3,5 +3,6 @@ import { MochaOptions, Runner } from 'mocha'; import { SerenityReporterForMocha } from './SerenityReporterForMocha'; export = function (runner: Runner, options?: MochaOptions) { + // todo: add requires - a list that includes a serenity config file return new SerenityReporterForMocha(serenity, runner, options); } diff --git a/packages/mocha/src/mappers/MochaOutcomeMapper.ts b/packages/mocha/src/mappers/MochaOutcomeMapper.ts index 995b3056829..4c1bbbadbf3 100644 --- a/packages/mocha/src/mappers/MochaOutcomeMapper.ts +++ b/packages/mocha/src/mappers/MochaOutcomeMapper.ts @@ -31,11 +31,28 @@ export class MochaOutcomeMapper { case test.isPending() && ! test.fn: return new ImplementationPending(new ImplementationPendingError(`Scenario not implemented`)); + case this.isRetryable(test) && (test as any).currentRetry() === 0: + return new ExecutionFailedWithError( + new Error(`Execution failed, ${ test.retries() } ${ test.retries() === 1 ? 'retry' : 'retries' } left.`) + ); + + case this.isRetryable(test) && (test as any).currentRetry() > 0: + return new ExecutionFailedWithError( + new Error(`Retry ${(test as any).currentRetry()} of ${ test.retries() } failed.`) + ); + default: return new ExecutionSuccessful(); } } + private isRetryable(test: Test) { + return ! test.isPassed() + && ! test.isFailed() + && ! test.isPending() + && test.retries() > 0; + } + private isAssertionError(error: Error): error is AssertionError { return error instanceof AssertionError || this.looksLikeAnAssertionError(error); diff --git a/packages/protractor/README.md b/packages/protractor/README.md index 5f71d4b3e5b..9b1495ee5d7 100644 --- a/packages/protractor/README.md +++ b/packages/protractor/README.md @@ -27,7 +27,9 @@ exports.config = { // Configure Serenity/JS to use an appropriate test runner // and the Stage Crew Members we've imported at the top of this file serenity: { - runner: 'jasmine', // or 'cucumber' + runner: 'jasmine', + // runner: 'cucumber', + // runner: 'mocha', crew: [ ArtifactArchiver.storingArtifactsAt('./target/site/serenity'), ConsoleReporter.forDarkTerminals(), @@ -41,11 +43,16 @@ exports.config = { // see the Cucumber configuration options below }, - // configure Jasmine runner + // or configure Jasmine runner jasmineNodeOpts: { // see the Jasmine configuration options below }, + // or configure Mocha runner + mochaOpts: { + // see the Mocha configuration options below + }, + // ... other Protractor-specific configuration }; ``` @@ -53,6 +60,7 @@ exports.config = { Learn more about: - [Cucumber configuration options](https://serenity-js.org/modules/cucumber/class/src/cli/CucumberConfig.ts~CucumberConfig.html) - [Jasmine configuration options](https://serenity-js.org/modules/jasmine/class/src/adapter/JasmineConfig.ts~JasmineConfig.html) +- [Mocha configuration options](https://serenity-js.org/modules/mocha/class/src/adapter/MochaConfig.ts~MochaConfig.html) - [Protractor configuration file](https://github.com/angular/protractor/blob/master/lib/config.ts). ### Interacting with websites and web apps diff --git a/packages/protractor/package.json b/packages/protractor/package.json index 5511213abbd..473209a55a3 100644 --- a/packages/protractor/package.json +++ b/packages/protractor/package.json @@ -53,10 +53,13 @@ "is-plain-object": "^3.0.0", "tiny-types": "^1.14.1" }, - "peerDependencies": { - "@serenity-js/core": "2.x", + "optionalDependencies": { "@serenity-js/cucumber": "2.x", "@serenity-js/jasmine": "2.x", + "@serenity-js/mocha": "2.x" + }, + "peerDependencies": { + "@serenity-js/core": "2.x", "protractor": "^5.0.0 || ^7.0.0", "selenium-webdriver": "^3.6.0" }, @@ -67,6 +70,7 @@ "@serenity-js/core": "2.10.2", "@serenity-js/cucumber": "2.10.2", "@serenity-js/jasmine": "2.10.2", + "@serenity-js/mocha": "2.10.2", "@serenity-js/local-server": "2.10.2", "@types/express": "^4.17.6", "@types/html-minifier": "^3.5.3", diff --git a/packages/protractor/spec/adapter/ProtractorFrameworkAdapter.spec.ts b/packages/protractor/spec/adapter/ProtractorFrameworkAdapter.spec.ts index f6a52d5f657..3c0cb8e8954 100644 --- a/packages/protractor/spec/adapter/ProtractorFrameworkAdapter.spec.ts +++ b/packages/protractor/spec/adapter/ProtractorFrameworkAdapter.spec.ts @@ -1,3 +1,5 @@ +import 'mocha'; + import { expect } from '@integration/testing-tools'; import { Serenity, Stage } from '@serenity-js/core'; import { DomainEvent, SceneFinished, SceneFinishes, SceneStarts } from '@serenity-js/core/lib/events'; diff --git a/packages/protractor/spec/adapter/TestRunnerDetector.spec.ts b/packages/protractor/spec/adapter/TestRunnerDetector.spec.ts index 908a15e3ffd..475aefcbab9 100644 --- a/packages/protractor/spec/adapter/TestRunnerDetector.spec.ts +++ b/packages/protractor/spec/adapter/TestRunnerDetector.spec.ts @@ -1,8 +1,11 @@ +import 'mocha'; + import { expect } from '@integration/testing-tools'; import { ModuleLoader } from '@serenity-js/core/lib/io'; import { TestRunnerDetector } from '../../src/adapter'; import { CucumberTestRunner } from '../../src/adapter/runners/CucumberTestRunner'; import { JasmineTestRunner } from '../../src/adapter/runners/JasmineTestRunner'; +import { MochaTestRunner } from '../../src/adapter/runners/MochaTestRunner'; describe('TestRunnerDetector', () => { @@ -24,13 +27,14 @@ describe('TestRunnerDetector', () => { expect(runner).to.be.instanceOf(CucumberTestRunner); }); - it('uses the CucumberTestRunner even when the Jasmine config is present as well', () => { + it('uses the CucumberTestRunner even when config for other runners is present as well', () => { const runner = detector.runnerFor({ serenity: { runner: 'cucumber', }, cucumberOpts: {}, jasmineNodeOpts: {}, + mochaOpts: {}, }); expect(runner).to.be.instanceOf(CucumberTestRunner); @@ -46,19 +50,41 @@ describe('TestRunnerDetector', () => { expect(runner).to.be.instanceOf(JasmineTestRunner); }); - it('uses the JasmineTestRunner even when the Cucumber config is present as well', () => { + it('uses the JasmineTestRunner even when config for other runners is present as well', () => { const runner = detector.runnerFor({ serenity: { runner: 'jasmine', }, cucumberOpts: {}, jasmineNodeOpts: {}, + mochaOpts: {}, }); expect(runner).to.be.instanceOf(JasmineTestRunner); }); - it('uses the MochaTestRunner'); + it('uses the MochaTestRunner', () => { + const runner = detector.runnerFor({ + serenity: { + runner: 'mocha', + }, + }); + + expect(runner).to.be.instanceOf(MochaTestRunner); + }); + + it('uses the MochaTestRunner even when config for other runners is present as well', () => { + const runner = detector.runnerFor({ + serenity: { + runner: 'mocha', + }, + cucumberOpts: {}, + jasmineNodeOpts: {}, + mochaOpts: {}, + }); + + expect(runner).to.be.instanceOf(MochaTestRunner); + }); }); describe('when no specific test runner is set', () => { @@ -92,7 +118,15 @@ describe('TestRunnerDetector', () => { expect(runner).to.be.instanceOf(JasmineTestRunner); }); - it('uses the MochaTestRunner when mochaOpts are specified'); + it('uses the MochaTestRunner when mochaOpts are specified', () => { + const runner = detector.runnerFor({ + mochaOpts: { + require: 'ts:ts-node/register', + }, + }); + + expect(runner).to.be.instanceOf(MochaTestRunner); + }); }); describe('to support test runner options specified in the capabilities section', () => { diff --git a/packages/protractor/src/adapter/TestRunnerDetector.ts b/packages/protractor/src/adapter/TestRunnerDetector.ts index 49c64326912..6d311055c05 100644 --- a/packages/protractor/src/adapter/TestRunnerDetector.ts +++ b/packages/protractor/src/adapter/TestRunnerDetector.ts @@ -10,8 +10,9 @@ export class TestRunnerDetector { static protractorCliOptions() { return [ - 'cucumberOpts', // todo: alias: cucumber ? - 'jasmineNodeOpts', // todo: alias: jasmine ? + 'cucumberOpts', + 'jasmineNodeOpts', + 'mochaOpts', ]; } @@ -40,8 +41,12 @@ export class TestRunnerDetector { return this.useCucumber(config); case specifiesRunnerFor('jasmine'): return this.useJasmine(config); + case specifiesRunnerFor('mocha'): + return this.useMocha(config); case !! config.cucumberOpts: return this.useCucumber(config); + case !! config.mochaOpts: + return this.useMocha(config); case !! config.jasmineNodeOpts: default: return this.useJasmine(config); @@ -57,4 +62,9 @@ export class TestRunnerDetector { const { JasmineTestRunner } = require('./runners/JasmineTestRunner'); return new JasmineTestRunner(config.jasmineNodeOpts, this.loader); } + + private useMocha(config: Config): TestRunner { + const { MochaTestRunner } = require('./runners/MochaTestRunner'); + return new MochaTestRunner(config.mochaOpts, this.loader); + } } diff --git a/packages/protractor/src/adapter/run.ts b/packages/protractor/src/adapter/run.ts index 8ccca44122d..31c5a1dfcb8 100644 --- a/packages/protractor/src/adapter/run.ts +++ b/packages/protractor/src/adapter/run.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + import { serenity } from '@serenity-js/core'; import { ModuleLoader } from '@serenity-js/core/lib/io'; import { Runner } from 'protractor'; diff --git a/packages/protractor/src/adapter/runners/MochaTestRunner.ts b/packages/protractor/src/adapter/runners/MochaTestRunner.ts new file mode 100644 index 00000000000..d33699881f2 --- /dev/null +++ b/packages/protractor/src/adapter/runners/MochaTestRunner.ts @@ -0,0 +1,18 @@ +import { ModuleLoader } from '@serenity-js/core/lib/io'; +import { MochaAdapter, MochaConfig } from '@serenity-js/mocha/lib/adapter'; // tslint:disable-line:no-submodule-imports +import { TestRunner } from './TestRunner'; + +/** + * @private + */ +export class MochaTestRunner implements TestRunner { + constructor( + private readonly config: MochaConfig, + private readonly loader: ModuleLoader, + ) { + } + + run(pathsToScenarios: string[]): Promise { + return new MochaAdapter(this.config, this.loader).run(pathsToScenarios); + } +}