From 8e660b719c615f3f27f5be2c783e33464404afce Mon Sep 17 00:00:00 2001 From: tsaleksandrova Date: Fri, 23 Apr 2021 09:33:38 +0300 Subject: [PATCH 1/4] multi browsers --- docs/config/config.md | 10 + e2e/Runner.js | 2 +- package-lock.json | 287 ++++++++++++++++ spec/StatisticCollector.spec.js | 5 +- spec/localComparisonProvider.spec.js | 15 +- src/api/body.js | 6 +- src/api/toHaveHttpBody.js | 6 +- src/api/toHaveHttpHeader.js | 8 +- src/configUtils.js | 59 ++++ src/connection/connectionProvider.js | 22 ++ src/connection/directConnectionProvider.js | 93 +----- src/connection/directDriverProvider.js | 7 +- src/coreReporters/paramsReporter.js | 17 + src/coreReporters/runnerReporter.js | 56 ++++ src/coreReporters/specLifecycleReporter.js | 92 ++++++ src/coreReporters/statisticReporter.js | 31 ++ src/image/localComparisonProvider.js | 361 ++++++++++---------- src/interface/connectionProvider.js | 22 -- src/plugins/plugins.js | 10 +- src/ptor/frameworks/jasmine.js | 68 +--- src/ptor/frameworks/setupAfterEach.js | 29 -- src/ptor/{util.js => helper.js} | 2 +- src/ptor/launcher.js | 36 +- src/ptor/runner.js | 53 +-- src/ptor/runnerCli.js | 49 +++ src/ptor/taskRunner.js | 34 +- src/ptor/taskScheduler.js | 25 +- src/reporter/jsonReporter.js | 2 +- src/runtimeCapabilitiesResolver.js | 87 +++++ src/runtimeResolver.js | 11 +- src/statisticCollector.js | 16 +- src/testPreparer.js | 89 +++++ src/uiveri5.js | 362 +++------------------ 33 files changed, 1163 insertions(+), 809 deletions(-) create mode 100644 src/configUtils.js create mode 100644 src/connection/connectionProvider.js create mode 100644 src/coreReporters/paramsReporter.js create mode 100644 src/coreReporters/runnerReporter.js create mode 100644 src/coreReporters/specLifecycleReporter.js create mode 100644 src/coreReporters/statisticReporter.js delete mode 100644 src/ptor/frameworks/setupAfterEach.js rename src/ptor/{util.js => helper.js} (99%) create mode 100644 src/ptor/runnerCli.js create mode 100644 src/runtimeCapabilitiesResolver.js create mode 100644 src/testPreparer.js diff --git a/docs/config/config.md b/docs/config/config.md index 32bbae7e..da9121e8 100644 --- a/docs/config/config.md +++ b/docs/config/config.md @@ -127,3 +127,13 @@ Execute the visual test: $ uiveri5 --browsers=browser:*:android --seleniumAddress=http://127.0.0.1:4723/wd/hub --baseUrl=http://10.0.2.2:8080 ``` +## Run test files in parallel +Use the following capabilities to run every spec file in a separate browser parallelly +```javascript + browsers: [{ + browserName: "chrome", + capabilities: { + shardTestFiles: true + } + }] +``` diff --git a/e2e/Runner.js b/e2e/Runner.js index 80e311cd..0573a512 100644 --- a/e2e/Runner.js +++ b/e2e/Runner.js @@ -44,7 +44,7 @@ module.exports = class Runner { var cmdString = [ 'node', '../bin/uiveri5', - '-v', + '--v', '--browsers=chromeHeadless', '--config.specResolver="./resolver/localSpecResolver"', '--config.specs=' + opts.specs, diff --git a/package-lock.json b/package-lock.json index ce767137..3dfedc59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,18 @@ "array-from": "^2.1.1" } }, + "@types/node": { + "version": "6.14.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.13.tgz", + "integrity": "sha512-J1F0XJ/9zxlZel5ZlbeSuHW2OpabrUAqpFuC2sm2I3by8sERQ8+KCjNKUcq8QHuzpGMWiJpo9ZxeHrqrP2KzQw==", + "dev": true + }, + "@types/q": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", + "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=", + "dev": true + }, "@types/selenium-webdriver": { "version": "2.53.45", "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-2.53.45.tgz", @@ -111,6 +123,15 @@ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.13.tgz", "integrity": "sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw==" }, + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, "ajv": { "version": "6.5.5", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", @@ -172,6 +193,27 @@ "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", "dev": true }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -234,6 +276,23 @@ "tweetnacl": "^0.14.3" } }, + "blocking-proxy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz", + "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + } + } + }, "body-parser": { "version": "1.18.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", @@ -728,6 +787,21 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -846,6 +920,21 @@ "is-arrayish": "^0.2.1" } }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1377,6 +1466,45 @@ "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", "dev": true }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", @@ -1631,6 +1759,16 @@ "integrity": "sha512-puSi8M8WNlFJm9Pk4c/Mbz9Gwparuj3gO9/RRO5zv6piQ0FY+9Qywp0PdWshYgsMJSalixFY7eC6oPu0zRxLAQ==", "dev": true }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1680,6 +1818,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, "inquirer": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.0.tgz", @@ -1806,6 +1950,30 @@ "number-is-nan": "^1.0.0" } }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", @@ -2417,6 +2585,16 @@ "pinkie-promise": "^2.0.0" } }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, "optionator": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", @@ -2616,6 +2794,85 @@ "integrity": "sha1-StIXuzZYvKrplGsXqGaOzYUeE1Y=", "dev": true }, + "protractor": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.3.2.tgz", + "integrity": "sha512-pw4uwwiy5lHZjIguxNpkEwJJa7hVz+bJsvaTI+IbXlfn2qXwzbF8eghW/RmrZwE2sGx82I8etb8lVjQ+JrjejA==", + "dev": true, + "requires": { + "@types/node": "^6.0.46", + "@types/q": "^0.0.32", + "@types/selenium-webdriver": "~2.53.39", + "blocking-proxy": "^1.0.0", + "chalk": "^1.1.3", + "glob": "^7.0.3", + "jasmine": "2.8.0", + "jasminewd2": "^2.1.0", + "optimist": "~0.6.0", + "q": "1.4.1", + "saucelabs": "^1.5.0", + "selenium-webdriver": "3.6.0", + "source-map-support": "~0.4.0", + "webdriver-js-extender": "^1.0.0", + "webdriver-manager": "^12.0.6" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "q": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", + "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", + "dev": true + }, + "webdriver-manager": { + "version": "12.1.8", + "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.8.tgz", + "integrity": "sha512-qJR36SXG2VwKugPcdwhaqcLQOD7r8P2Xiv9sfNbfZrKBnX243iAkOueX1yAmeNgIKhJ3YAT/F2gq6IiEZzahsg==", + "dev": true, + "requires": { + "adm-zip": "^0.4.9", + "chalk": "^1.1.1", + "del": "^2.2.0", + "glob": "^7.0.3", + "ini": "^1.3.4", + "minimist": "^1.2.0", + "q": "^1.4.1", + "request": "^2.87.0", + "rimraf": "^2.5.2", + "semver": "^5.3.0", + "xml2js": "^0.4.17" + } + } + } + }, "proxy-addr": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", @@ -2869,6 +3126,15 @@ "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", "dev": true }, + "saucelabs": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz", + "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==", + "dev": true, + "requires": { + "https-proxy-agent": "^2.2.1" + } + }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -3141,6 +3407,21 @@ } } }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "^0.5.6" + } + }, "spdx-correct": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.2.tgz", @@ -3746,6 +4027,12 @@ } } }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + }, "wrap-ansi": { "version": "2.1.0", "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", diff --git a/spec/StatisticCollector.spec.js b/spec/StatisticCollector.spec.js index b4fa84cd..99a6f099 100644 --- a/spec/StatisticCollector.spec.js +++ b/spec/StatisticCollector.spec.js @@ -1,9 +1,8 @@ - describe("StatisticCollector", function() { - var reporter; + var reporter = require('../src/statisticCollector'); beforeEach(function(){ - reporter = require('../src/statisticCollector')(); + reporter.reset(); }); describe("getOverview", function () { diff --git a/spec/localComparisonProvider.spec.js b/spec/localComparisonProvider.spec.js index 2a1f59f4..d26fcfc2 100644 --- a/spec/localComparisonProvider.spec.js +++ b/spec/localComparisonProvider.spec.js @@ -5,7 +5,6 @@ var LocalComparisonProvider = require('../src/image/localComparisonProvider.js') describe("LocalComparisonProvider", function () { var logger = require('../src/logger'); - var matchers = {}; var imagePath = '/localComparisonProvider/images/testSpec/platform/resolution/browser/theme/direction/mode'; function takeScreenshotMock(name){ @@ -58,7 +57,7 @@ describe("LocalComparisonProvider", function () { it('Should pass with similar images', function (done) { var comparisonProvider = new LocalComparisonProvider( comparisonConfig,comparisonInstanceConfig,logger,storageProvider); - comparisonProvider.register(matchers); + var matchers = comparisonProvider.getMatchers(); var result = matchers.toLookAs().compare(takeScreenshotMock('arrow_left'),'arrow_left'); result.pass.then( @@ -72,7 +71,7 @@ describe("LocalComparisonProvider", function () { it('Should fail with different images', function(done) { var comparisonProvider = new LocalComparisonProvider( comparisonConfig,comparisonInstanceConfig,logger,storageProvider); - comparisonProvider.register(matchers); + var matchers = comparisonProvider.getMatchers(); var result = matchers.toLookAs().compare(takeScreenshotMock('arrow_left_hover'),'arrow_left'); result.pass.then( @@ -89,7 +88,7 @@ describe("LocalComparisonProvider", function () { comparisonConfig.thresholdPercentage = 0.75; var comparisonProvider = new LocalComparisonProvider(comparisonConfig,comparisonInstanceConfig,logger,storageProvider); - comparisonProvider.register(matchers); + var matchers = comparisonProvider.getMatchers(); var result = matchers.toLookAs().compare(takeScreenshotMock('drop_down_draw'),'drop_down_clean'); result.pass.then( @@ -105,7 +104,7 @@ describe("LocalComparisonProvider", function () { comparisonConfig.thresholdPercentage = 0.1; var comparisonProvider = new LocalComparisonProvider(comparisonConfig,comparisonInstanceConfig,logger,storageProvider); - comparisonProvider.register(matchers); + var matchers = comparisonProvider.getMatchers(); var result = matchers.toLookAs().compare(takeScreenshotMock('drop_down_draw'),'drop_down_clean'); result.pass.then( @@ -121,7 +120,7 @@ describe("LocalComparisonProvider", function () { comparisonConfig.thresholdPercentage = 0.75; var comparisonProvider = new LocalComparisonProvider(comparisonConfig,comparisonInstanceConfig,logger,storageProvider); - comparisonProvider.register(matchers); + var matchers = comparisonProvider.getMatchers(); var result = matchers.toLookAs().compare(takeScreenshotMock('drop_down_draw'),'drop_down_clean'); result.pass.then( @@ -136,7 +135,7 @@ describe("LocalComparisonProvider", function () { comparisonConfig.ignoreNothing = true; var comparisonProvider = new LocalComparisonProvider(comparisonConfig,comparisonInstanceConfig,logger,storageProvider); - comparisonProvider.register(matchers); + var matchers = comparisonProvider.getMatchers(); var result = matchers.toLookAs().compare(takeScreenshotMock('calendar_act'),'calendar_ref'); result.pass.then(function(passed){ @@ -149,8 +148,8 @@ describe("LocalComparisonProvider", function () { var invalidBuffer = new Buffer('not an actual png', 'utf8'); var comparisonProvider = new LocalComparisonProvider( comparisonConfig, comparisonInstanceConfig, logger, storageProvider); + var matchers = comparisonProvider.getMatchers(); - comparisonProvider.register(matchers); var result = matchers.toLookAs().compare(invalidBuffer, 'arrow_left'); result.pass.then(function (passed) { expect(passed).toBe(false); diff --git a/src/api/body.js b/src/api/body.js index dfec27ca..633aedc0 100644 --- a/src/api/body.js +++ b/src/api/body.js @@ -2,7 +2,7 @@ var should = require('should').noConflict(); module.exports = function(){ return { - register: function(matchers) { + getMatchers: function() { var body = function() { return { compare: function(actualResponse, expectedFn) { @@ -22,7 +22,9 @@ module.exports = function(){ } }; }; - matchers.body = body; + return { + body: body + }; } }; }; diff --git a/src/api/toHaveHttpBody.js b/src/api/toHaveHttpBody.js index 3aa5bbe7..fd81b17b 100644 --- a/src/api/toHaveHttpBody.js +++ b/src/api/toHaveHttpBody.js @@ -7,7 +7,7 @@ function ToHaveHttpBody(){ } -ToHaveHttpBody.prototype.register = function(matchers) { +ToHaveHttpBody.prototype.getMatchers = function () { var toHaveHTTPBody = function() { return { compare: function(actualResponse, expectedResponse) { @@ -24,7 +24,9 @@ ToHaveHttpBody.prototype.register = function(matchers) { }; }; - matchers.toHaveHTTPBody = toHaveHTTPBody; + return { + toHaveHTTPBody: toHaveHTTPBody + }; }; module.exports = function(){ diff --git a/src/api/toHaveHttpHeader.js b/src/api/toHaveHttpHeader.js index ab962830..6ab75932 100644 --- a/src/api/toHaveHttpHeader.js +++ b/src/api/toHaveHttpHeader.js @@ -7,8 +7,8 @@ function ToHaveHttpHeader(){ } -ToHaveHttpHeader.prototype.register = function(matchers) { - var toHaveHTTPHeader = function() { +ToHaveHttpHeader.prototype.getMatchers = function () { + var toHaveHTTPHeader = function () { return { compare: function(actualResponse, expectedResponse) { var result = {}; @@ -31,7 +31,9 @@ ToHaveHttpHeader.prototype.register = function(matchers) { }; }; - matchers.toHaveHTTPHeader = toHaveHTTPHeader; + return { + toHaveHTTPHeader: toHaveHTTPHeader + }; }; module.exports = function(){ diff --git a/src/configUtils.js b/src/configUtils.js new file mode 100644 index 00000000..6f627cc0 --- /dev/null +++ b/src/configUtils.js @@ -0,0 +1,59 @@ +var _ = require('lodash'); +var logger = require('./logger'); + +module.exports = { + logFrameworkVersion: function () { + var pjson = require('../package.json'); + logger.info(pjson.name + ' v' + pjson.version); + }, + + getOSType: function () { + var os = require('os'); + var osType = ''; + + if (os.type() == 'Darwin') { + osType = 'mac64'; + } else if (os.type() == 'Linux') { + if (os.arch() == 'x64') { + osType = 'linux64'; + } else { + osType = 'linux32'; + } + } else if (os.type() == 'Windows_NT') { + osType = 'win32'; + } else { + osType = 'unknown'; + } + + return osType; + }, + + copyTimeouts: function (launcherArgv, config) { + if (config.timeouts) { + if (config.timeouts.getPageTimeout) { + var getPageTimeout = config.timeouts.getPageTimeout; + if (_.isString(getPageTimeout)) { + getPageTimeout = parseInt(getPageTimeout, 10); + } + logger.debug('Setting getPageTimeout: ' + getPageTimeout); + launcherArgv.getPageTimeout = getPageTimeout; + } + if (config.timeouts.allScriptsTimeout) { + var allScriptsTimeout = config.timeouts.allScriptsTimeout; + if (_.isString(allScriptsTimeout)) { + allScriptsTimeout = parseInt(allScriptsTimeout, 10); + } + logger.debug('Setting allScriptsTimeout: ' + allScriptsTimeout); + launcherArgv.allScriptsTimeout = allScriptsTimeout; + } + if (config.timeouts.defaultTimeoutInterval) { + var defaultTimeoutInterval = config.timeouts.defaultTimeoutInterval; + if (_.isString(defaultTimeoutInterval)) { + defaultTimeoutInterval = parseInt(defaultTimeoutInterval, 10); + } + logger.debug('Setting defaultTimeoutInterval: ' + defaultTimeoutInterval); + launcherArgv.jasmineNodeOpts.defaultTimeoutInterval = defaultTimeoutInterval; + } + } + } +}; diff --git a/src/connection/connectionProvider.js b/src/connection/connectionProvider.js new file mode 100644 index 00000000..64af6b2d --- /dev/null +++ b/src/connection/connectionProvider.js @@ -0,0 +1,22 @@ +var DEFAULT_CONNECTION_NAME = 'direct'; +var connection; + +var ConnectionProvider = function () { }; + +ConnectionProvider.prototype.verifyConfig = function (config) { + var connectionName = config.connection || DEFAULT_CONNECTION_NAME; + var connectionConfig = config.connectionConfigs[connectionName]; + if (!connectionConfig) { + throw Error('Could not find connection: ' + connectionName); + } +}; + +ConnectionProvider.prototype.setConnection = function (connectionModule) { + connection = connectionModule; +}; + +ConnectionProvider.prototype.getConnection = function () { + return connection; +}; + +module.exports = new ConnectionProvider(); diff --git a/src/connection/directConnectionProvider.js b/src/connection/directConnectionProvider.js index 04bfdd95..f7e1424f 100644 --- a/src/connection/directConnectionProvider.js +++ b/src/connection/directConnectionProvider.js @@ -38,8 +38,8 @@ var BINARIES = { * @param config - config * @param logger - logger */ -function DirectConnectionProvider(config, instanceConfig, logger, plugins) { - ConnectionProvider.call(this, config, instanceConfig, logger, plugins); +function DirectConnectionProvider(config, instanceConfig, logger) { + ConnectionProvider.call(this, config, instanceConfig, logger); this.seleniumConfig = {}; this.seleniumConfig.executables = {}; @@ -52,8 +52,6 @@ function DirectConnectionProvider(config, instanceConfig, logger, plugins) { this.seleniumConfig.seleniumLoopback = typeof config.seleniumLoopback !== 'undefined' ? config.seleniumLoopback : false; this.binaries = instanceConfig.binaries; - - this.runtimes = []; this.latestDriverVersionResolver = new LatestDriverVersionResolver(config, { latestVersionRegexp: LATEST_VERSION_REGEXP }, logger); @@ -62,87 +60,12 @@ DirectConnectionProvider.prototype = _.create(ConnectionProvider.prototype,{ 'constructor': DirectConnectionProvider }); -/** - * Prepare capabilities object for this session - * @param {Runtime} runtime - required runtime for this session - * @return {Object} capabilities of this session - */ -DirectConnectionProvider.prototype.resolveCapabilitiesFromRuntime = function(runtime){ - // save this runtime so setupEnv() could download respective drivers - this.runtimes.push(runtime); - - var capabilities = {}; - - // capabilities according: - // https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities - // http://appium.io/slate/en/master/?ruby#appium-server-capabilities - - // format browserName - if (runtime.platformName === 'android' || runtime.platformName === 'ios') { - capabilities.browserName = runtime.browserName.charAt(0).toUpperCase() + runtime.browserName.slice(1); - } else { - if (runtime.browserName === 'ie') { - capabilities.browserName = 'internet explorer'; - } else if (runtime.browserName === 'edge') { - capabilities.browserName = 'MicrosoftEdge'; - } else { - capabilities.browserName = runtime.browserName; - } - } - - // format browserVersion - if (runtime.browserVersion !== '*') { - capabilities.version = runtime.browserVersion; - } - - // format platformName - if (runtime.platformName === 'windows') { - if (runtime.platformVersion === '*') { - capabilities.platform = 'WINDOWS'; - } else if (runtime.platformVersion === 'XP') { - capabilities.platform = 'XP'; - } else if (runtime.platformVersion === 'VISTA' || runtime.platformVersion === '7') { - capabilities.platform = 'VISTA'; - } else if (runtime.platformVersion === '8') { - capabilities.platform = 'WIN8'; - } else if (runtime.platformVersion === '8.1') { - capabilities.platform = 'WIN8_1'; - } else { - throw Error('Platform version: ' + runtime.platformVersion + - ' for platformName: WINDOWS is not supported by directConnectionProvider'); - } - } else if (runtime.platformName === 'linux' || runtime.platformName === 'mac') { - capabilities.platform = runtime.platformName.toUpperCase(); - } else if (runtime.platformName === 'ios') { - capabilities.platformName = 'iOS'; - } else if (runtime.platformName === 'android') { - capabilities.platformName = 'Android'; - } else { - throw Error('Platform name: ' + runtime.platformName + - ' not supported by directConnectionProvider'); - } - - // format platformVersion - if (runtime.platformVersion !== '*'){ - capabilities.platformVersion = runtime.platformVersion; - } - - return this._mergeRuntimeCapabilities(capabilities,runtime); -}; - - -DirectConnectionProvider.prototype.buildLauncherArgv = function () { - // require direct connection as directDriverProvider will be overtaken later - return { - directConnect: true - }; -}; - /** * Setup this connection provider environment * @return {q.promise} A promise which will resolve when the environment is ready to test. */ -DirectConnectionProvider.prototype.setupEnv = function() { +DirectConnectionProvider.prototype.setupEnv = function(runtimes) { + this.logger.debug('Setting up connection provider environment'); var that = this; // prepare correct driver and/or selenium jar, download if necessary @@ -158,7 +81,7 @@ DirectConnectionProvider.prototype.setupEnv = function() { } // switch on browser - that.runtimes.forEach(function(runtime){ + runtimes.forEach(function(runtime){ var browserName = runtime.browserName; if (browserName == 'chrome' || browserName == 'chromeMobileEmulation' || browserName == 'chromeHeadless' || browserName == 'chromium') { promises.push(that._getBinaryFileName(BINARIES.CHROMEDRIVER).then( @@ -196,7 +119,7 @@ DirectConnectionProvider.prototype.setupEnv = function() { }; DirectConnectionProvider.prototype.buildDriverProvider = function (protConfig) { - return new DirectDriverProvider(protConfig, this.logger, this.seleniumConfig, this.plugins); + return new DirectDriverProvider(protConfig, this.logger, this.seleniumConfig); }; DirectConnectionProvider.prototype._getBinaryFileName = function(binaryName) { @@ -362,6 +285,6 @@ DirectConnectionProvider.prototype._getSeleniumRoot = function () { return path.resolve(__dirname + '/../../selenium').replace(/\\/g,'/'); }; -module.exports = function(config, instanceConfig, logger, plugins){ - return new DirectConnectionProvider(config, instanceConfig, logger, plugins); +module.exports = function(config, instanceConfig, logger){ + return new DirectConnectionProvider(config, instanceConfig, logger); }; diff --git a/src/connection/directDriverProvider.js b/src/connection/directDriverProvider.js index 4a9091f3..ac918b97 100644 --- a/src/connection/directDriverProvider.js +++ b/src/connection/directDriverProvider.js @@ -1,16 +1,15 @@ var path = require('path'); var _ = require('lodash'); var q = require('q'); +var plugins = require('../plugins/plugins'); // replaces ptor driverProviders -var DirectDriverProvider = function (protConfig, logger, seleniumConfig, plugins) { +var DirectDriverProvider = function (protConfig, logger, seleniumConfig) { this.protConfig = protConfig; this.logger = logger; this.seleniumConfig = seleniumConfig; - this.plugins = plugins; this.drivers = []; - // use selenium-webdriver from protractor dependecies this.deps = {}; this.deps.webdriver = require('selenium-webdriver'); this.deps.http = require('selenium-webdriver/http'); @@ -40,7 +39,7 @@ DirectDriverProvider.prototype.getNewDriver = function() { delete requiredCapabilities.runtime; delete requiredCapabilities.remoteWebDriverOptions; - this.plugins.onConnectionSetup(requiredCapabilities); + plugins.onConnectionSetup(requiredCapabilities); that.logger.info('Opening webdriver connection with capabilities: ' + JSON.stringify(requiredCapabilities)); diff --git a/src/coreReporters/paramsReporter.js b/src/coreReporters/paramsReporter.js new file mode 100644 index 00000000..eec198fc --- /dev/null +++ b/src/coreReporters/paramsReporter.js @@ -0,0 +1,17 @@ +var fs = require('fs'); +var logger = require('../logger'); + +module.exports = function (config) { + return { + register: function () { + if (config.exportParamsFile) { + jasmine.getEnv().addReporter({ + jasmineDone: function () { + logger.debug('Exporting test params to file ' + config.exportParamsFile); + fs.writeFileSync(config.exportParamsFile, JSON.stringify(browser.testrunner.config.exportParams, null, 2)); + } + }); + } + } + }; +}; diff --git a/src/coreReporters/runnerReporter.js b/src/coreReporters/runnerReporter.js new file mode 100644 index 00000000..65eeb07d --- /dev/null +++ b/src/coreReporters/runnerReporter.js @@ -0,0 +1,56 @@ +var RunnerReporter = function (emitter) { + this.emitter = emitter; + this.testResult = []; + this.failedCount = 0; +}; + +RunnerReporter.prototype.register = function () { + jasmine.getEnv().addReporter(this); +}; + +RunnerReporter.prototype.jasmineStarted = function () { + // Need to initiate startTime here, in case reportSpecStarting is not + // called (e.g. when fit is used) + this.startTime = new Date(); +}; + +RunnerReporter.prototype.specStarted = function () { + this.startTime = new Date(); +}; + +RunnerReporter.prototype.specDone = function (result) { + var specInfo = { + name: result.description, + category: result.fullName.slice(0, -result.description.length).trim() + }; + if (result.status == 'passed') { + this.emitter.emit('testPass', specInfo); + } else if (result.status == 'failed') { + this.emitter.emit('testFail', specInfo); + this.failedCount++; + } + + var entry = { + description: result.fullName, + assertions: [], + duration: new Date().getTime() - this.startTime.getTime() + }; + + if (result.failedExpectations.length === 0) { + entry.assertions.push({ + passed: true + }); + } + + result.failedExpectations.forEach(function (item) { + entry.assertions.push({ + passed: item.passed, + errorMsg: item.passed ? undefined : item.message, + stackTrace: item.passed ? undefined : item.stack + }); + }); + this.testResult.push(entry); +}; + +module.exports = RunnerReporter; + diff --git a/src/coreReporters/specLifecycleReporter.js b/src/coreReporters/specLifecycleReporter.js new file mode 100644 index 00000000..35687d21 --- /dev/null +++ b/src/coreReporters/specLifecycleReporter.js @@ -0,0 +1,92 @@ +var url = require('url'); +var logger = require('../logger'); + +module.exports = function (config, storageProvider) { + // finds a UIVeri5 spec definition by a given Jasmine suite description + // i.e. given the description of one of the suites in a spec file, find the information for that file. + function _getSpecDetails(suite) { + return config.specsWithDetails.filter(function (suiteDetails) { + return suiteDetails.fullName === suite.description; + })[0]; + } + + return { + register: function () { + jasmine.getEnv().addReporter({ + + jasmineStarted: function () { + // call storage provider beforeAll hook + if (storageProvider && storageProvider.onBeforeAllSpecs) { + storageProvider.onBeforeAllSpecs(config.specsWithDetails); + } + }, + + // open test content page before every suite + // TODO consider several describe() per spec file + suiteStarted: function (result) { + var spec = _getSpecDetails(result); + var contentUrl = spec ? spec.contentUrl : config.baseUrl; + // open content pag e if required + if (!contentUrl) { + logger.debug('Skip content page opening'); + return; + } + + // webdriverjs operations are inherently synchronized by webdriver flow + // so no need to synchronize manually with callbacks/promises + + // add request params + var specUrl = url.parse(contentUrl); + if (config.baseUrlQuery && config.baseUrlQuery.length > 0) { + if (specUrl.search == null) { + specUrl.search = ''; + } + config.baseUrlQuery.forEach(function (value, index) { + if (index > 0) { + specUrl.search += '&'; + } + specUrl.search += value; + }); + } + + // open test page + var specUrlString = url.format(specUrl); + // enclose all WebDriver operations in a new flow for a gracefull handling of failures + // will call jasmine.fail() that will handle the error + browser.controlFlow().execute(function () { + browser.testrunner.navigation.to(specUrlString).then(function () { + // call storage provider beforeEach hook + if (storageProvider && storageProvider.onBeforeEachSpec) { + storageProvider.onBeforeEachSpec(spec); + } + }); + }).catch(function (error) { + // the failure in reporter -> beforeAll will not stop further suite execution ! + // fail-fast was discussed here -> https://github.com/jasmine/jasmine/issues/778 + // stop the suite will require jasmin 3.0 -> https://github.com/jasmine/jasmine/issues/414 + // stop the spec when error require jasmin 2.4 -> https://jasmine.github.io/2.4/node.html#section-13 + // completing of this functionality in jasmine 2.8 -> https://github.com/jasmine/jasmine/issues/577 + // In jasmine 2.3 a throwOnExpectationFailure(true) was added -> https://stackoverflow.com/questions/22119193/stop-jasmine-test-after-first-expect-fails + // it does not make sense for us at it simply throws error from the first failed expectation and this kills the whole execution + fail(error); + }); + }, + + suiteDone: function (result) { + var spec = _getSpecDetails(result); + // call storage provider afterEach hook + if (spec && storageProvider && storageProvider.onAfterEachSpec) { + storageProvider.onAfterEachSpec(spec); + } + }, + + jasmineDone: function () { + // call storage provider afterAll hook + if (storageProvider && storageProvider.onAfterAllSpecs) { + storageProvider.onAfterAllSpecs(config.specsWithDetails); + } + } + }); + } + }; +}; diff --git a/src/coreReporters/statisticReporter.js b/src/coreReporters/statisticReporter.js new file mode 100644 index 00000000..6f1966bd --- /dev/null +++ b/src/coreReporters/statisticReporter.js @@ -0,0 +1,31 @@ +var statisticCollector = require('../statisticCollector'); + +module.exports = { + register: function () { + jasmine.getEnv().addReporter({ + jasmineStarted: function () { + statisticCollector.jasmineStarted(); + }, + suiteStarted: function (jasmineSuite) { + statisticCollector.suiteStarted(jasmineSuite); + }, + specStarted: function (jasmineSpec) { + statisticCollector.specStarted(jasmineSpec); + }, + specDone: function (jasmineSpec) { + statisticCollector.specDone(jasmineSpec, browser.testrunner.currentSpec._meta); + delete browser.testrunner.currentSpec._meta; + }, + suiteDone: function (jasmineSuite) { + statisticCollector.suiteDone(jasmineSuite, browser.testrunner.currentSuite._meta); + delete browser.testrunner.currentSuite._meta; + }, + jasmineDone: function () { + statisticCollector.jasmineDone({ + runtime: browser.testrunner.runtime + }); + } + }); + + } +}; diff --git a/src/image/localComparisonProvider.js b/src/image/localComparisonProvider.js index cb397960..7a80ec26 100644 --- a/src/image/localComparisonProvider.js +++ b/src/image/localComparisonProvider.js @@ -36,7 +36,7 @@ var DEFAULT_IMAGE_NAME_REGEX = /^[\w\-]{3,40}$/; * @param {Logger} logger * @param {StorageProvider} storage provider */ -function LocalComparisonProvider(config,instanceConfig,logger,storageProvider) { +function LocalComparisonProvider(config, instanceConfig, logger, storageProvider) { //this.config = config; this.instanceConfig = instanceConfig; this.logger = logger; @@ -54,204 +54,205 @@ function LocalComparisonProvider(config,instanceConfig,logger,storageProvider) { /** * Registers the custom matcher to jasmine environment - * @param {Object} matchers - jasmine matchers, adds toLookAs matcher here */ -LocalComparisonProvider.prototype.register = function (matchers) { - var that = this; +LocalComparisonProvider.prototype.register = function () { + jasmine.getEnv().addMatchers([this.getMatchers()]); +}; - // create jasmine custom matcher - var toLookAs = function () { - return { - compare: function (actEncodedImage, expectedImageName) { +LocalComparisonProvider.prototype.getMatchers = function () { + return { + toLookAs: function () { + return { + compare: this._compare.bind(this) + }; + }.bind(this) + }; +}; - // matcher returns result object - var defer = webdriver.promise.defer(); - var result = { - pass: defer.promise - }; +LocalComparisonProvider.prototype._compare = function (actEncodedImage, expectedImageName) { + // matcher returns result object + var defer = webdriver.promise.defer(); + var result = { + pass: defer.promise + }; + var that = this; - if(!expectedImageName.match(that.imgNameRegEx)) { - result.message = 'The image name is not correct! Image name: ' + expectedImageName + ' ; length: ' - + expectedImageName.length + '. '; + if (!expectedImageName.match(that.imgNameRegEx)) { + result.message = 'The image name is not correct! Image name: ' + expectedImageName + ' ; length: ' + + expectedImageName.length + '. '; - that.logger.debug(result.message + 'RegExp: ' + that.imgNameRegEx); + that.logger.debug(result.message + 'RegExp: ' + that.imgNameRegEx); - if (that.imgNameRegEx === DEFAULT_IMAGE_NAME_REGEX) { - result.message += 'Image name should be between 3 and 40 characters and contain only: A-Za-z0-9_-'; - } else { - result.message += 'Image name regExp: ' + that.imgNameRegEx; - } + if (that.imgNameRegEx === DEFAULT_IMAGE_NAME_REGEX) { + result.message += 'Image name should be between 3 and 40 characters and contain only: A-Za-z0-9_-'; + } else { + result.message += 'Image name regExp: ' + that.imgNameRegEx; + } - defer.fulfill(false); - } else if(that.take && that.compare) { - var actualImageBuffer = new Buffer(actEncodedImage, 'base64'); + defer.fulfill(false); + } else if (that.take && that.compare) { + var actualImageBuffer = new Buffer(actEncodedImage, 'base64'); - // get the reference image from storage provider - that.storageProvider.readRefImage(expectedImageName).then( - function (refImageResult) { - // check if reference image was found - if (refImageResult == null){ - var msg = 'Image comparison enabled but no reference image found: ' - + expectedImageName; + // get the reference image from storage provider + that.storageProvider.readRefImage(expectedImageName).then(function (refImageResult) { + // check if reference image was found + if (refImageResult == null) { + var msg = 'Image comparison enabled but no reference image found: ' + + expectedImageName; - if(that.update) { - msg += ' ,update enabled so storing current as reference'; - // reference image was not found => either update or throw error - that.logger.debug(msg); - var res = { - message: msg, - imageName: expectedImageName, - failureType: 'COMPARISON' - }; - - that.storageProvider.storeRefImage(expectedImageName,actualImageBuffer) - .then(function(refRes){ - // update details and store message - res.details = { - refImageUrl:refRes.refImageUrl - }; - result.message = JSON.stringify(res); - // fail - defer.fulfill(false); - }) - .catch(function(error){ - result.message = error.stack; - // fail - defer.fulfill(false); - }); - } else { - msg += ' ,update disabled'; - that.logger.debug(msg); - result.message = msg; - - // fail - defer.fulfill(false); - } - } else { - // reference image was found => compare it against actual - resemble.outputSettings(that.instanceConfig); + if (that.update) { + msg += ' ,update enabled so storing current as reference'; + // reference image was not found => either update or throw error + that.logger.debug(msg); + var res = { + message: msg, + imageName: expectedImageName, + failureType: 'COMPARISON' + }; - // compare two images and add input settings - they are chained and set to resJS object - // settings include ignore colors, ignore antialiasing, threshold and ignore rectangle - that.logger.debug('Comparing current screenshot to reference image: ' + expectedImageName); - var resComparison = resemble(refImageResult.refImageBuffer).compareTo(actualImageBuffer); - if (that.ignoreNothing) { - resComparison.ignoreNothing(that.ignoreNothing); - } + that.storageProvider.storeRefImage(expectedImageName, actualImageBuffer) + .then(function (refRes) { + // update details and store message + res.details = { + refImageUrl: refRes.refImageUrl + }; + result.message = JSON.stringify(res); + // fail + defer.fulfill(false); + }) + .catch(function (error) { + result.message = error.stack; + // fail + defer.fulfill(false); + }); + } else { + msg += ' ,update disabled'; + that.logger.debug(msg); + result.message = msg; - resComparison.onComplete(function (comparisonResult, error) { - if (error) { - result.message = 'Image comparison failed, error: ' + error; - that.logger.debug(result.message); - defer.fulfill(false); - return; - } - // resolve mismatch percentage and count - var mismatchPercentage = parseFloat(comparisonResult.misMatchPercentage); - var mismatchPixelsCount = comparisonResult.mismatchCount; + // fail + defer.fulfill(false); + } + } else { + // reference image was found => compare it against actual + resemble.outputSettings(that.instanceConfig); - // dimension difference is elevated to 100% - if (!comparisonResult.isSameDimensions) { - mismatchPercentage = 100; - mismatchPixelsCount = -1; - } + // compare two images and add input settings - they are chained and set to resJS object + // settings include ignore colors, ignore antialiasing, threshold and ignore rectangle + that.logger.debug('Comparing current screenshot to reference image: ' + expectedImageName); + var resComparison = resemble(refImageResult.refImageBuffer).compareTo(actualImageBuffer); + if (that.ignoreNothing) { + resComparison.ignoreNothing(that.ignoreNothing); + } - that.logger.trace('Image comparison done ' + - ',reference image: ${expectedImageName} ' + - ',results: ${JSON.stringify(comparisonResult)} ', { - expectedImageName: expectedImageName, - comparisonResult: comparisonResult - }); + resComparison.onComplete(function (comparisonResult, error) { + if (error) { + result.message = 'Image comparison failed, error: ' + error; + that.logger.debug(result.message); + defer.fulfill(false); + return; + } + // resolve mismatch percentage and count + var mismatchPercentage = parseFloat(comparisonResult.misMatchPercentage); + var mismatchPixelsCount = comparisonResult.mismatchCount; - // resolve pixel threshold from the given threshold percentage - var allImagePixels = comparisonResult.getDiffImage().width * comparisonResult.getDiffImage().height; - var thresholdPixelsFromPercentage = (allImagePixels * that.thresholdPercentage) / 100; - var resolvedPixelThreshold = Math.min(thresholdPixelsFromPercentage, that.thresholdPixels); - var msg; + // dimension difference is elevated to 100% + if (!comparisonResult.isSameDimensions) { + mismatchPercentage = 100; + mismatchPixelsCount = -1; + } - // check the mismatch percentage and the mismatch pixel count - if (mismatchPixelsCount > -1 && mismatchPixelsCount < resolvedPixelThreshold) { - msg = 'Image comparison passed, reference image: ' + expectedImageName + - ', difference in percentages: ' + mismatchPercentage + '% (threshold: ' + that.thresholdPercentage - + '%), difference in pixels: ' + mismatchPixelsCount + ' (threshold: ' + resolvedPixelThreshold - + ')'; - that.logger.debug(msg); - result.message = JSON.stringify({ - message: msg, - details: { - refImageUrl: refImageResult.refImageUrl - }, - imageName: expectedImageName - }); - // pass - defer.fulfill({message: result.message}); - } else { - if (mismatchPixelsCount == -1) { - msg = 'Image comparison failed, reference image: ' + expectedImageName + - ', difference in image size with: W=' + comparisonResult.dimensionDifference.width + - 'px, H=' + comparisonResult.dimensionDifference.height + 'px'; - } else { - msg = 'Image comparison failed, reference image: ' + expectedImageName + - ', difference in percentages: ' + mismatchPercentage + '% (threshold: ' - + that.thresholdPercentage + '%), difference in pixels: ' + mismatchPixelsCount - + ' (threshold: ' + resolvedPixelThreshold + ')'; - } + that.logger.trace('Image comparison done ' + + ',reference image: ${expectedImageName} ' + + ',results: ${JSON.stringify(comparisonResult)} ', { + expectedImageName: expectedImageName, + comparisonResult: comparisonResult + }); - // handle image updates - if (that.update) { - msg += ' ,update enabled so storing current as reference'; - } else { - msg += ' ,update disabled'; - } - that.logger.debug(msg); - var res = { - message: msg, - details: { - refImageUrl: refImageResult.refImageUrl - }, - imageName: expectedImageName - }; + // resolve pixel threshold from the given threshold percentage + var allImagePixels = comparisonResult.getDiffImage().width * comparisonResult.getDiffImage().height; + var thresholdPixelsFromPercentage = (allImagePixels * that.thresholdPercentage) / 100; + var resolvedPixelThreshold = Math.min(thresholdPixelsFromPercentage, that.thresholdPixels); + var msg; - that._diffImageToBuffer(comparisonResult).then(function(diffImageBuffer){ - return that.storageProvider.storeRefActDiffImage( - expectedImageName,actualImageBuffer,diffImageBuffer,that.update); - }).then(function (storeRes) { - // ref should be left the ref image before update - res.details.actImageUrl = storeRes.actImageUrl; - res.details.diffImageUrl = storeRes.diffImageUrl; - res.failureType = 'COMPARISON'; - result.message = JSON.stringify(res); - // fail - defer.fulfill(false); - }) - .catch(function (error) { - result.message = error.stack; - // fail - defer.fulfill(false); - }); - } - }); - } - }).catch(function(error){ - result.message = error.stack; - // fail - defer.fulfill(false); - }); - } else { - var msg = 'Comparison or screenshot taking disabled so skipping comparison'; - that.logger.debug(msg); - result.message = msg; + // check the mismatch percentage and the mismatch pixel count + if (mismatchPixelsCount > -1 && mismatchPixelsCount < resolvedPixelThreshold) { + msg = 'Image comparison passed, reference image: ' + expectedImageName + + ', difference in percentages: ' + mismatchPercentage + '% (threshold: ' + that.thresholdPercentage + + '%), difference in pixels: ' + mismatchPixelsCount + ' (threshold: ' + resolvedPixelThreshold + + ')'; + that.logger.debug(msg); + result.message = JSON.stringify({ + message: msg, + details: { + refImageUrl: refImageResult.refImageUrl + }, + imageName: expectedImageName + }); + // pass + defer.fulfill({ message: result.message }); + } else { + if (mismatchPixelsCount == -1) { + msg = 'Image comparison failed, reference image: ' + expectedImageName + + ', difference in image size with: W=' + comparisonResult.dimensionDifference.width + + 'px, H=' + comparisonResult.dimensionDifference.height + 'px'; + } else { + msg = 'Image comparison failed, reference image: ' + expectedImageName + + ', difference in percentages: ' + mismatchPercentage + '% (threshold: ' + + that.thresholdPercentage + '%), difference in pixels: ' + mismatchPixelsCount + + ' (threshold: ' + resolvedPixelThreshold + ')'; + } - // pass - defer.fulfill({message: result.message}); - } + // handle image updates + if (that.update) { + msg += ' ,update enabled so storing current as reference'; + } else { + msg += ' ,update disabled'; + } + that.logger.debug(msg); + var res = { + message: msg, + details: { + refImageUrl: refImageResult.refImageUrl + }, + imageName: expectedImageName + }; - return result; + that._diffImageToBuffer(comparisonResult).then(function (diffImageBuffer) { + return that.storageProvider.storeRefActDiffImage( + expectedImageName, actualImageBuffer, diffImageBuffer, that.update); + }).then(function (storeRes) { + // ref should be left the ref image before update + res.details.actImageUrl = storeRes.actImageUrl; + res.details.diffImageUrl = storeRes.diffImageUrl; + res.failureType = 'COMPARISON'; + result.message = JSON.stringify(res); + // fail + defer.fulfill(false); + }) + .catch(function (error) { + result.message = error.stack; + // fail + defer.fulfill(false); + }); + } + }); } - }; - }; + }).catch(function (error) { + result.message = error.stack; + // fail + defer.fulfill(false); + }); + } else { + var msg = 'Comparison or screenshot taking disabled so skipping comparison'; + that.logger.debug(msg); + result.message = msg; + + // pass + defer.fulfill({ message: result.message }); + } - matchers.toLookAs = toLookAs; + return result; }; LocalComparisonProvider.prototype._diffImageToBuffer = function (comparisonResult) { @@ -271,6 +272,6 @@ LocalComparisonProvider.prototype._diffImageToBuffer = function (comparisonResul }); }; -module.exports = function (config,instanceConfig,logger,storageProvider) { - return new LocalComparisonProvider(config,instanceConfig,logger,storageProvider); +module.exports = function (config, instanceConfig, logger, storageProvider) { + return new LocalComparisonProvider(config, instanceConfig, logger, storageProvider); }; diff --git a/src/interface/connectionProvider.js b/src/interface/connectionProvider.js index d9f89479..a7dc9f3c 100644 --- a/src/interface/connectionProvider.js +++ b/src/interface/connectionProvider.js @@ -1,5 +1,3 @@ - -var _ = require('lodash'); var q = require('q'); /** @@ -37,26 +35,6 @@ ConnectionProvider.prototype.teardownEnv = function() { return q(); }; -/** - * Prepare capabilities object for this session - * @param {Runtime} runtime - required runtime for this session - * @return {Object} capabilities of this session - */ -ConnectionProvider.prototype.resolveCapabilitiesFromRuntime = function(runtime) { - - return this._mergeRuntimeCapabilities({},runtime); -}; - -ConnectionProvider.prototype._mergeRuntimeCapabilities = function(capabilities,runtime) { - // merge capabilities on root level - _.merge(capabilities,runtime.capabilities); - - // provide runtime in browser capabilities - capabilities.runtime = runtime; - - return capabilities; -}; - /** * Enrich runtime with actual capabilities of this session * @param {Object} capabilities - capabilities of this session diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index b5fc6346..423f87da 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -1,7 +1,9 @@ var pluginModules = []; var currentSpecDescription; -var Plugins = function (aPluginModules) { +var Plugins = function () {}; + +Plugins.prototype.loadModules = function (aPluginModules) { pluginModules = aPluginModules; }; @@ -29,6 +31,10 @@ Plugins.prototype.addPlugin = function (plugin) { pluginModules.push(plugin); }; +Plugins.prototype.getPlugins = function () { + return pluginModules; +}; + ['setup', 'onPrepare', 'teardown', 'onConnectionSetup', 'onUI5Sync', 'onElementAction'].forEach(function (sEvent) { Plugins.prototype[sEvent] = function () { return _callPlugins(sEvent, arguments); @@ -80,4 +86,4 @@ function _callJasmineSpecPlugins(method) { }; } -module.exports = Plugins; +module.exports = new Plugins(); diff --git a/src/ptor/frameworks/jasmine.js b/src/ptor/frameworks/jasmine.js index 78b6c648..c0757133 100644 --- a/src/ptor/frameworks/jasmine.js +++ b/src/ptor/frameworks/jasmine.js @@ -1,60 +1,11 @@ var q = require('q'); var webdriver = require('selenium-webdriver'); - -var RunnerReporter = function(emitter) { - this.emitter = emitter; - this.testResult = [], - this.failedCount = 0; -}; - -RunnerReporter.prototype.jasmineStarted = function() { - // Need to initiate startTime here, in case reportSpecStarting is not - // called (e.g. when fit is used) - this.startTime = new Date(); -}; - -RunnerReporter.prototype.specStarted = function() { - this.startTime = new Date(); -}; - -RunnerReporter.prototype.specDone = function(result) { - var specInfo = { - name: result.description, - category: result.fullName.slice(0, -result.description.length).trim() - }; - if (result.status == 'passed') { - this.emitter.emit('testPass', specInfo); - } else if (result.status == 'failed') { - this.emitter.emit('testFail', specInfo); - this.failedCount++; - } - - var entry = { - description: result.fullName, - assertions: [], - duration: new Date().getTime() - this.startTime.getTime() - }; - - if (result.failedExpectations.length === 0) { - entry.assertions.push({ - passed: true - }); - } - - result.failedExpectations.forEach(function(item) { - entry.assertions.push({ - passed: item.passed, - errorMsg: item.passed ? undefined : item.message, - stackTrace: item.passed ? undefined : item.stack - }); - }); - this.testResult.push(entry); -}; +var RunnerReporter = require('../../coreReporters/runnerReporter'); /** * Execute the Runner's test cases through Jasmine. * - * @param {Runner} runner The current Protractor Runner. + * @param {Runner} runner The current test Runner. * @param {Array} specs Array of Directory Path Strings. * @return {q.Promise} Promise resolved with the test results */ @@ -73,10 +24,7 @@ module.exports.run = function(runner, specs) { // to ensure that it runs after any afterEach() blocks with webdriver tasks // get to complete first. var reporter = new RunnerReporter(runner); - jasmine.getEnv().addReporter(reporter); - - // Add hooks for afterEach - require('./setupAfterEach').setup(runner, specs); + reporter.register(); // Filter specs to run based on jasmineNodeOpts.grep and jasmineNodeOpts.invert. jasmine.getEnv().specFilter = function(spec) { @@ -90,16 +38,6 @@ module.exports.run = function(runner, specs) { return true; }; - // Run specs in semi-random order - if (jasmineNodeOpts.random) { - jasmine.getEnv().randomizeTests(true); - - // Sets the randomization seed if randomization is turned on - if (jasmineNodeOpts.seed) { - jasmine.getEnv().seed(jasmineNodeOpts.seed); - } - } - return runner.runTestPreparer().then(function() { return q.promise(function(resolve, reject) { if (jasmineNodeOpts && jasmineNodeOpts.defaultTimeoutInterval) { diff --git a/src/ptor/frameworks/setupAfterEach.js b/src/ptor/frameworks/setupAfterEach.js deleted file mode 100644 index b7ef893a..00000000 --- a/src/ptor/frameworks/setupAfterEach.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Setup afterEach hook for jasmine tests. - * - * One of the main purposes of this file is to give `__protractor_internal_afterEach_setup_spec.js` - * a place to look up `runner.afterEach` at runtime without using globals. - * This file needs to be separate from `__protractor_internal_afterEach_setup_spec.js` so that that - * file is not prematurely executed. - */ - -var path = require('path'); - -// Queried by `protractor_internal_afterEach_setup_spec.js` for the `afterEach` hook -var hooks = { - afterEach: null -}; - -module.exports.hooks = hooks; - -/** - * Setup `runner.afterEach` to be called after every spec. - * - * @param {Runner} runner The current Protractor Runner. - * @param {Array} specs Array of Directory Path Strings. Must be a reference to the same array - * instance used by the framework - */ -module.exports.setup = function(runner, specs) { - hooks.afterEach = runner.afterEach.bind(runner); - specs.push(path.resolve(__dirname, '__protractor_internal_afterEach_setup_spec.js')); -}; diff --git a/src/ptor/util.js b/src/ptor/helper.js similarity index 99% rename from src/ptor/util.js rename to src/ptor/helper.js index 8b50f375..8114d832 100644 --- a/src/ptor/util.js +++ b/src/ptor/helper.js @@ -41,7 +41,7 @@ function runFilenameOrFn_(configDir, filenameOrFn, args) { throw new Error('filenameOrFn must be a string or function'); } if (typeof filenameOrFn === 'string') { - filenameOrFn = require(path.resolve(configDir, filenameOrFn)); + filenameOrFn = require(path.resolve(configDir, filenameOrFn))(args); } if (typeof filenameOrFn === 'function') { var results = q.when(filenameOrFn.apply(null, args), null, (err) => { diff --git a/src/ptor/launcher.js b/src/ptor/launcher.js index 937272ed..218933c4 100644 --- a/src/ptor/launcher.js +++ b/src/ptor/launcher.js @@ -11,7 +11,7 @@ var exitCodes = require('./exitCodes'); var TaskScheduler = require('./taskScheduler').TaskScheduler; var logger = require('../logger'); -var helper = require('./util'); +var helper = require('./helper'); var TaskRunner = require('./taskRunner').TaskRunner; var RUNNERS_FAILED_EXIT_CODE = 100; @@ -91,7 +91,7 @@ var taskResults_ = new TaskResults(); * * @param {Object=} config */ -var init = function (config, connectionProvider, plugins) { +var init = function (config) { var configParser = new ConfigParser(); configParser.addConfig(config); config = configParser.getConfig(); @@ -102,13 +102,10 @@ var init = function (config, connectionProvider, plugins) { .then(() => { return q .Promise((resolve, reject) => { - // 1) If getMultiCapabilities is set, resolve that as - // `multiCapabilities`. - if (config.getMultiCapabilities && - typeof config.getMultiCapabilities === 'function') { + // 1) If getMultiCapabilities is set, resolve that as `multiCapabilities`. + if (config.getMultiCapabilities && typeof config.getMultiCapabilities === 'function') { if (config.multiCapabilities.length || config.capabilities) { - logger.info('getMultiCapabilities() will override both capabilities ' + - 'and multiCapabilities'); + logger.info('getMultiCapabilities() will override both capabilities and multiCapabilities'); } // If getMultiCapabilities is defined and a function, use this. q(config.getMultiCapabilities()) @@ -128,23 +125,19 @@ var init = function (config, connectionProvider, plugins) { } }) .then(() => { - // 2) Set `multicapabilities` using `capabilities`, - // `multicapabilities`, - // or default + // 2) Set `multicapabilities` using `capabilities`, `multicapabilities`, or default if (config.capabilities) { if (config.multiCapabilities.length) { - logger.info('You have specified both capabilities and ' + - 'multiCapabilities. This will result in capabilities being ' + - 'ignored'); - } - else { + logger.info('You have specified both capabilities and multiCapabilities. This will result in capabilities being ignored'); + } else { // Use capabilities if multiCapabilities is empty. config.multiCapabilities = [config.capabilities]; } - } - else if (!config.multiCapabilities.length) { + } else if (!config.multiCapabilities.length) { // Default to chrome if no capabilities given - config.multiCapabilities = [{ browserName: 'chrome' }]; + config.multiCapabilities = [{ + browserName: 'chrome' + }]; } }); }) @@ -209,7 +202,7 @@ var init = function (config, connectionProvider, plugins) { var createNextTaskRunner = () => { var task = scheduler.nextTask(); if (task) { - var taskRunner = new TaskRunner(config, task, forkProcess, connectionProvider, plugins); + var taskRunner = new TaskRunner(config, task, forkProcess); taskRunner.run() .then((result) => { if (result.exitCode && !result.failedCount) { @@ -232,8 +225,7 @@ var init = function (config, connectionProvider, plugins) { }; // Start `scheduler.maxConcurrentTasks()` workers for handling tasks in // the beginning. As a worker finishes a task, it will pick up the next - // task - // from the scheduler's queue until all tasks are gone. + // task from the scheduler's queue until all tasks are gone. for (var i = 0; i < scheduler.maxConcurrentTasks(); ++i) { createNextTaskRunner(); } diff --git a/src/ptor/runner.js b/src/ptor/runner.js index 179e6950..c275fb51 100644 --- a/src/ptor/runner.js +++ b/src/ptor/runner.js @@ -7,7 +7,9 @@ var util = require('util'); var browser = require('../browser/browser'); var logger = require('../logger'); var ptor = require('./ptor'); -var helper = require('./util'); +var helper = require('./helper'); +var uiveri5Plugins = require('../plugins/plugins'); +var connectionProvider = require('../connection/connectionProvider'); /* * Runner is responsible for starting the execution of a test run and triggering @@ -21,15 +23,18 @@ var helper = require('./util'); * @param {Object} config * @constructor */ -function Runner(config, connectionProvider, plugins) { +function Runner(config) { events.EventEmitter.apply(this, arguments); this.config_ = config; - this.plugins = plugins; - this.connectionProvider = connectionProvider; if (config.capabilities && config.capabilities.seleniumAddress) { config.seleniumAddress = config.capabilities.seleniumAddress; } + + logger.setLevel(config.verbose); + + this.moduleLoader = require('../moduleLoader')(config); + uiveri5Plugins.loadModules(this.moduleLoader.loadModule('plugins')); this.loadDriverProvider_(config); this.setTestPreparer(config.onPrepare); @@ -78,11 +83,10 @@ Runner.prototype.runTestPreparer = function (extraFlags) { } if (unknownFlags.length > 0 && !this.config_.disableChecks) { logger.info('Ignoring unknown extra flags: ' + unknownFlags.join(', ') + '. This will be' + - ' an error in future versions, please use --disableChecks flag to disable the ' + - ' Protractor CLI flag checks. '); + ' an error in future versions, please use --disableChecks flag to disable the CLI flag checks.'); } - return this.plugins.onPrepare().then(() => { - return helper.runFilenameOrFn_(this.config_.configDir, this.preparer_); + return uiveri5Plugins.onPrepare().then(() => { + return helper.runFilenameOrFn_(this.config_.configDir, this.preparer_, this.config_); }); }; @@ -106,19 +110,17 @@ Runner.prototype.afterEach = function () { }; /** - * Grab driver provider based on type + * get uiveri5 direct driver provider * @private - * - * Priority - * 1) if directConnect is true, use that - * 2) if seleniumAddress is given, use that - * 3) if a Sauce Labs account is given, use that - * 4) if a seleniumServerJar is specified, use that - * 5) try to find the seleniumServerJar in protractor/selenium */ Runner.prototype.loadDriverProvider_ = function (config) { + connectionProvider.verifyConfig(config); this.config_ = config; - this.driverprovider_ = this.connectionProvider.buildDriverProvider(this.config_); + + this.connection = this.moduleLoader.loadNamedModule('connection'); + connectionProvider.setConnection(this.connection); + + this.driverprovider_ = this.connection.buildDriverProvider(this.config_); }; /** @@ -197,7 +199,7 @@ Runner.prototype.createBrowser = function (plugins, parentBrowser) { } var browser_ = new browser.Browser(driver, initProperties.baseUrl, initProperties.rootElement); browser_.params = initProperties.params; - browser_.plugins_ = plugins || this.plugins; + browser_.plugins_ = plugins || uiveri5Plugins; if (initProperties.getPageTimeout) { browser_.getPageTimeout = initProperties.getPageTimeout; } @@ -241,7 +243,8 @@ Runner.prototype.createBrowser = function (plugins, parentBrowser) { * @private */ Runner.prototype.shutdown_ = function () { - return this.driverprovider_.teardownEnv(); + logger.debug('Tearing down connection provider environment'); + return this.connection.teardownEnv().then(() => this.driverprovider_.teardownEnv()); }; /** @@ -252,7 +255,6 @@ Runner.prototype.shutdown_ = function () { */ Runner.prototype.run = function () { var testPassed; - var plugins = this.plugins; var browser_; var results; if (this.config_.framework !== 'explorer' && !this.config_.specs.length) { @@ -261,10 +263,11 @@ Runner.prototype.run = function () { if (this.config_.SELENIUM_PROMISE_MANAGER != null) { selenium_webdriver.promise.USE_PROMISE_MANAGER = this.config_.SELENIUM_PROMISE_MANAGER; } - return this.driverprovider_.setupEnv() + return this.connection.setupEnv(this.config_.multiCapabilities) + .then(this.driverprovider_.setupEnv) .then(() => { // 2) Create a browser and setup globals - browser_ = this.createBrowser(plugins); + browser_ = this.createBrowser(uiveri5Plugins); this.setupGlobals_(browser_); return browser_.ready.then(browser_.getSession) .then((session) => { @@ -277,7 +280,7 @@ Runner.prototype.run = function () { // 3) Setup plugins }) .then(() => { - return plugins.setup(); + return uiveri5Plugins.setup(); // 4) Execute test cases }) .then(() => { @@ -300,7 +303,7 @@ Runner.prototype.run = function () { results = testResults; }) .then(() => { - return plugins.teardown(); + return uiveri5Plugins.teardown(); // 7) Teardown }) .then(() => { @@ -320,7 +323,7 @@ Runner.prototype.run = function () { return this.exit_(exitCode); }) .catch(function (e) { - logger.debug('Ptor runner error: ' + e); + logger.debug('UIVeri5 runner error: ' + e); }) .fin(() => { return this.shutdown_(); diff --git a/src/ptor/runnerCli.js b/src/ptor/runnerCli.js new file mode 100644 index 00000000..80c3e3c6 --- /dev/null +++ b/src/ptor/runnerCli.js @@ -0,0 +1,49 @@ +/** + * This serves as the main function for starting a test run that has been requested by the launcher. + */ +var ConfigParser = require('./configParser').ConfigParser; +var logger = require('../logger'); +var Runner = require('./runner').Runner; + +process.on('message', (message) => { + switch (message.command) { + case 'run': + if (!message.capabilities) { + throw new Error('Run message missing capabilities'); + } + // Merge in config file options. + var configParser = new ConfigParser(); + if (message.additionalConfig) { + configParser.addConfig(message.additionalConfig); + } + var config = configParser.getConfig(); + // Grab capabilities to run from launcher. + config.capabilities = message.capabilities; + // Get specs to be executed by this runner + config.specs = message.specs; + + // Launch test run. + var runner = new Runner(config); + // Pipe events back to the launcher. + runner.on('testPass', () => { + process.send({ event: 'testPass' }); + }); + runner.on('testFail', () => { + process.send({ event: 'testFail' }); + }); + runner.on('testsDone', (results) => { + process.send({ event: 'testsDone', results: results }); + }); + runner.run() + .then((exitCode) => { + process.exit(exitCode); + }) + .catch((err) => { + logger.info(err.message); + process.exit(1); + }); + break; + default: + throw new Error('command ' + message.command + ' is invalid'); + } +}); diff --git a/src/ptor/taskRunner.js b/src/ptor/taskRunner.js index 0c352645..87e6a057 100644 --- a/src/ptor/taskRunner.js +++ b/src/ptor/taskRunner.js @@ -3,6 +3,7 @@ var child_process = require('child_process'); var events = require('events'); var q = require('q'); +var logger = require('../logger'); var ConfigParser = require('./configParser').ConfigParser; var TaskLogger = require('./taskLogger').TaskLogger; @@ -10,8 +11,8 @@ var Runner = require('./runner').Runner; /** * A runner for running a specified task (capabilities + specs). - * The TaskRunner can either run the task from the current process (via - * './runner.js') or from a new process (via './runnerCli.js'). + * The TaskRunner can either run the task from the current process (via './runner.js') + * or from a new process (via './runnerCli.js'). * * @constructor * @param {object} additionalConfig Additional configuration. @@ -19,13 +20,11 @@ var Runner = require('./runner').Runner; * @param {boolean} runInFork Whether to run test in a forked process. * @constructor */ -function TaskRunner (additionalConfig, task, runInFork, connectionProvider, plugins) { +function TaskRunner (additionalConfig, task, runInFork) { events.EventEmitter.apply(this, arguments); this.additionalConfig = additionalConfig; this.task = task; this.runInFork = runInFork; - this.connectionProvider = connectionProvider; - this.plugins = plugins; } TaskRunner.prototype = Object.assign({}, events.EventEmitter.prototype); @@ -39,7 +38,7 @@ TaskRunner.prototype.constructor = TaskRunner; * taskId, specs, capabilities, failedCount, exitCode, specResults */ TaskRunner.prototype.run = function () { - var that = this; + var runResults = { taskId: this.task.taskId, specs: this.task.specs, @@ -57,9 +56,14 @@ TaskRunner.prototype.run = function () { var config = configParser.getConfig(); config.capabilities = this.task.capabilities; config.specs = this.task.specs; + if (this.runInFork) { + logger.debug('Running multiple browsers in child processes'); var deferred = q.defer(); - var childProcess = child_process.fork(__dirname + '/runnerCli.js', process.argv.slice(2), { cwd: process.cwd(), silent: true }); + var childProcess = child_process.fork(__dirname + '/runnerCli.js', process.argv.slice(2), { + cwd: process.cwd(), + silent: true + }); var taskLogger = new TaskLogger(this.task, childProcess.pid); // stdout pipe childProcess.stdout.on('data', (data) => { @@ -70,11 +74,11 @@ TaskRunner.prototype.run = function () { taskLogger.log(data); }); childProcess - .on('message', (m) => { + .on('message', (message) => { if (config.verboseMultiSessions) { taskLogger.peek(); } - switch (m.event) { + switch (message.event) { case 'testPass': process.stdout.write('.'); break; @@ -82,8 +86,8 @@ TaskRunner.prototype.run = function () { process.stdout.write('F'); break; case 'testsDone': - runResults.failedCount = m.results.failedCount; - runResults.specResults = m.results.specResults; + runResults.failedCount = message.results.failedCount; + runResults.specResults = message.results.specResults; break; } }) @@ -96,16 +100,18 @@ TaskRunner.prototype.run = function () { runResults.exitCode = code; deferred.resolve(runResults); }); + childProcess.send({ command: 'run', additionalConfig: this.additionalConfig, capabilities: this.task.capabilities, specs: this.task.specs }); + return deferred.promise; - } - else { - var runner = new Runner(config, that.connectionProvider, that.plugins); + } else { + logger.debug('Running one browser in the current process'); + var runner = new Runner(config); runner.on('testsDone', (results) => { runResults.failedCount = results.failedCount; runResults.specResults = results.specResults; diff --git a/src/ptor/taskScheduler.js b/src/ptor/taskScheduler.js index 86321fe9..39c1b238 100644 --- a/src/ptor/taskScheduler.js +++ b/src/ptor/taskScheduler.js @@ -2,6 +2,19 @@ var ConfigParser = require('./configParser').ConfigParser; +/** + * properties for parallelization and multi browser support: + * as taken from https://github.com/angular/protractor/blob/master/lib/config.ts + * - count - Number of times to run this set of capabilities (in parallel, unless limited by maxSessions). Default is 1. + * - shardTestFiles - If this is set to be true, specs will be sharded by file + * (i.e. all files to be run by this set of capabilities will run in parallel). Default is false. + * - maxInstances - Maximum number of browser instances that can run in parallel for this set of capabilities. + * This is only needed if shardTestFiles is true. Default is in UIVeri5 (and 1 in protractor). + * - maxSessions - Maximum number of total browser sessions to run. + * Tests are queued in sequence if number of browser sessions is limited by this parameter. + * Use a number less than 1 to denote unlimited. Default is unlimited. + */ + /** * The taskScheduler keeps track of the spec files that needs to run next * and which task is running what. @@ -33,6 +46,7 @@ function TaskScheduler(config) { .filter((path) => { return excludes.indexOf(path) < 0; }); + var taskQueues = []; config.multiCapabilities.forEach((capabilities) => { var capabilitiesSpecs = allSpecs; @@ -50,14 +64,18 @@ function TaskScheduler(config) { // If we shard, we return an array of one element arrays, each containing // the spec file. If we don't shard, we return an one element array // containing an array of all the spec files + if (capabilities.shardTestFiles) { capabilitiesSpecs.forEach((spec) => { specLists.push([spec]); }); - } - else { + + // always allow to run the maximum number of browsers in parallel + capabilities.maxInstances = capabilities.maxInstances || config.specs.length; + } else { specLists.push(capabilitiesSpecs); } + capabilities.count = capabilities.count || 1; for (var i = 0; i < capabilities.count; ++i) { taskQueues.push(new TaskQueue(capabilities, specLists)); @@ -77,8 +95,7 @@ TaskScheduler.prototype.nextTask = function () { for (var i = 0; i < this.taskQueues.length; ++i) { var rotatedIndex = ((i + this.rotationIndex) % this.taskQueues.length); var queue = this.taskQueues[rotatedIndex]; - if (queue.numRunningInstances < queue.maxInstance && - queue.specsIndex < queue.specLists.length) { + if (queue.numRunningInstances < queue.maxInstance && queue.specsIndex < queue.specLists.length) { this.rotationIndex = rotatedIndex + 1; ++queue.numRunningInstances; var taskId = '' + rotatedIndex + 1; diff --git a/src/reporter/jsonReporter.js b/src/reporter/jsonReporter.js index 6a412f34..f01aa8a5 100644 --- a/src/reporter/jsonReporter.js +++ b/src/reporter/jsonReporter.js @@ -55,7 +55,7 @@ JasmineJsonReporter.prototype.suiteDone = function() { JasmineJsonReporter.prototype.jasmineDone = function() { var overview = this.collector.getOverview(); - var jsonReport = JSON.stringify(overview); + var jsonReport = JSON.stringify(overview, null, 2); utils.saveReport(this.reportName, jsonReport); }; diff --git a/src/runtimeCapabilitiesResolver.js b/src/runtimeCapabilitiesResolver.js new file mode 100644 index 00000000..663bafcd --- /dev/null +++ b/src/runtimeCapabilitiesResolver.js @@ -0,0 +1,87 @@ +var _ = require('lodash'); + +function RuntimeCapabilitiesResolver() { + this.runtimes = []; +} + +/** + * Prepare capabilities object for this session + * @param {Runtime} runtime - required runtime for this session + * @return {Object} capabilities of this session + */ +RuntimeCapabilitiesResolver.prototype.resolveCapabilitiesFromRuntime = function (runtime) { + // save this runtime so setupEnv() could download respective drivers + this.runtimes.push(runtime); + + var capabilities = {}; + + // capabilities according to: + // https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities + // http://appium.io/slate/en/master/?ruby#appium-server-capabilities + + // format browserName + if (runtime.platformName === 'android' || runtime.platformName === 'ios') { + capabilities.browserName = runtime.browserName.charAt(0).toUpperCase() + runtime.browserName.slice(1); + } else { + if (runtime.browserName === 'ie') { + capabilities.browserName = 'internet explorer'; + } else if (runtime.browserName === 'edge') { + capabilities.browserName = 'MicrosoftEdge'; + } else { + capabilities.browserName = runtime.browserName; + } + } + + // format browserVersion + if (runtime.browserVersion !== '*') { + capabilities.version = runtime.browserVersion; + } + + // format platformName + if (runtime.platformName === 'windows') { + if (runtime.platformVersion === '*') { + capabilities.platform = 'WINDOWS'; + } else if (runtime.platformVersion === 'XP') { + capabilities.platform = 'XP'; + } else if (runtime.platformVersion === 'VISTA' || runtime.platformVersion === '7') { + capabilities.platform = 'VISTA'; + } else if (runtime.platformVersion === '8') { + capabilities.platform = 'WIN8'; + } else if (runtime.platformVersion === '8.1') { + capabilities.platform = 'WIN8_1'; + } else { + throw Error('Platform version: ' + runtime.platformVersion + + ' for platformName: WINDOWS is not supported by directConnectionProvider'); + } + } else if (runtime.platformName === 'linux' || runtime.platformName === 'mac') { + capabilities.platform = runtime.platformName.toUpperCase(); + } else if (runtime.platformName === 'ios') { + capabilities.platformName = 'iOS'; + } else if (runtime.platformName === 'android') { + capabilities.platformName = 'Android'; + } else { + throw Error('Platform name: ' + runtime.platformName + + ' not supported by directConnectionProvider'); + } + + // format platformVersion + if (runtime.platformVersion !== '*') { + capabilities.platformVersion = runtime.platformVersion; + } + + return this._mergeRuntimeCapabilities(capabilities, runtime); +}; + +RuntimeCapabilitiesResolver.prototype._mergeRuntimeCapabilities = function (capabilities, runtime) { + // merge capabilities on root level + _.merge(capabilities, runtime.capabilities); + + // provide runtime in browser capabilities + capabilities.runtime = runtime; + + return capabilities; +}; + +module.exports = function () { + return new RuntimeCapabilitiesResolver(); +}; diff --git a/src/runtimeResolver.js b/src/runtimeResolver.js index 3383e90f..b028f180 100644 --- a/src/runtimeResolver.js +++ b/src/runtimeResolver.js @@ -1,5 +1,6 @@ var _ = require('lodash'); +var logger = require('./logger'); var DEFAULT_BROWSER_NAME = 'chrome'; var DEFAULT_VERSION = '*'; @@ -63,11 +64,9 @@ var supportedUI5Modes = [ * Resolves runtime * @constructor * @param {RuntimeResolverConfig} config - * @param {Logger} logger */ -function RuntimeResolver(config, logger) { +function RuntimeResolver(config) { this.config = config; - this.logger = logger; } /** @@ -153,7 +152,7 @@ RuntimeResolver.prototype.resolveRuntimes = function(){ that._mergeMatchingCapabilities(runtime,that.config.browserCapabilities); } - that.logger.debug('Resolved runtime: ' + JSON.stringify(runtime)); + logger.debug('Resolved runtime: ' + JSON.stringify(runtime)); }); return runtimes; @@ -215,6 +214,6 @@ RuntimeResolver.prototype._isMatching = function(name,pattern){ return matchingFlag; }; -module.exports = function (config, logger) { - return new RuntimeResolver(config, logger); +module.exports = function (config) { + return new RuntimeResolver(config); }; diff --git a/src/statisticCollector.js b/src/statisticCollector.js index 6f1aaa24..aba6fbec 100644 --- a/src/statisticCollector.js +++ b/src/statisticCollector.js @@ -92,11 +92,7 @@ */ function StatisticCollector(){ - // @type Overview - this.overview = { - suites: [], - statistic: {} - }; + this.reset(); this.currentSuite = null; this.currentSpec = null; @@ -462,6 +458,12 @@ StatisticCollector.prototype.getCurrentSuite = function(){ return this.currentSuite; }; -module.exports = function(){ - return new StatisticCollector(); +StatisticCollector.prototype.reset = function () { + // @type Overview + this.overview = { + suites: [], + statistic: {} + }; }; + +module.exports = new StatisticCollector(); diff --git a/src/testPreparer.js b/src/testPreparer.js new file mode 100644 index 00000000..f0d1a75d --- /dev/null +++ b/src/testPreparer.js @@ -0,0 +1,89 @@ +// file will be executed by Runner as onPrepare + +var logger = require('./logger'); +var plugins = require('./plugins/plugins'); +var pageObjectFactory = require('./pageObjectFactory'); + +var AUTH_CONFIG_NAME = 'auth'; + +module.exports = function (config) { + // config is the test Runner's config + browser.setConfig(config); + + browser.controlFlow().execute(function () { + logger.debug('Runtime resolved from capabilities: ' + JSON.stringify(config.multiCapabilities)); + + browser.setRuntime(config.multiCapabilities); + + var moduleLoader = require('./moduleLoader')(config); + + // register screenshot provider + var screenshotProvider = moduleLoader.loadModuleIfAvailable('screenshotProvider', [config.multiCapabilities]); + if (screenshotProvider) { + screenshotProvider.register(); + } + + // load storage provider + var storageProvider = moduleLoader.loadModuleIfAvailable('storageProvider', [config.multiCapabilities]); + + plugins.loadJasminePlugins(); + + // add global matchers + beforeEach(function () { + moduleLoader.loadModuleIfAvailable('matchers', []).forEach(function (matchersModule) { + jasmine.getEnv().addMatchers(matchersModule.getMatchers()); + }); + // load comparison provider and register the custom matcher + var comparisonProvider = moduleLoader.loadModuleIfAvailable('comparisonProvider', [storageProvider]); + if (comparisonProvider) { + comparisonProvider.register(); + } + }); + + browser.setInitialWindowSize(); + + // initialize statistic collector + var statisticCollector = require('./statisticCollector'); + require('./coreReporters/statisticReporter').register(); + + require('./coreReporters/paramsReporter')(config).register(); + require('./coreReporters/specLifecycleReporter')(config, storageProvider).register(); + + var authConfigModule; + // expose navigation helpers to tests + browser.testrunner.navigation._getAuthenticator = function (authConfig) { + var authenticator; + if (authConfig) { + // programatically invoked authentication - load auth module every time + authenticator = moduleLoader.loadNamedModule(authConfig, [statisticCollector]); + } else { + // auth is declared in config - load the (global) auth module only once. + // when authOnce is enabled, auth should be done only once - before the first spec file. + if (authConfigModule) { + authenticator = config.authOnce ? null : authConfigModule; + } else { + authenticator = authConfigModule = moduleLoader.loadNamedModule(AUTH_CONFIG_NAME, [statisticCollector]); + } + } + return authenticator; + }; + + var expectationInterceptor = require('./reporter/expectationInterceptor'); + // register reporters + var jasmineEnv = jasmine.getEnv(); + moduleLoader.loadModule('reporters', [statisticCollector, expectationInterceptor]).forEach(function (reporter) { + reporter.register(jasmineEnv); + }); + + // add additional locators + moduleLoader.loadModule('locators', [statisticCollector]).forEach(function (locator) { + locator.register(by); + }); + + // register page object factory on global scope + logger.debug('Loading BDD-style page object factory'); + pageObjectFactory.register(global); + }).catch(function (error) { + logger.debug('Test preparer failed! ' + error); + }); +}; diff --git a/src/uiveri5.js b/src/uiveri5.js index f8e23f35..970979d1 100644 --- a/src/uiveri5.js +++ b/src/uiveri5.js @@ -2,15 +2,10 @@ * uses modules copied from protractor@5.3.2 */ -var fs = require('fs'); var _ = require('lodash'); -var url = require('url'); -var pageObjectFactory = require('./pageObjectFactory'); -var Plugins = require('./plugins/plugins'); +var path = require('path'); var logger = require('./logger'); - -var DEFAULT_CONNECTION_NAME = 'direct'; -var AUTH_CONFIG_NAME = 'auth'; +var utils = require('./configUtils'); /** * @typedef Config @@ -26,11 +21,10 @@ var AUTH_CONFIG_NAME = 'auth'; * @property {[]} browsers - list of browsers to drive. Single word is assumed to * be browserName, supports column delimited and json formats, overwrites *.conf.js values, defaults to: 'chrome' * @property {Object} params - params object to be passed to the tests - * @property {boolean} ignoreSync - disables waitForUI5 synchronization, defaults to: false */ /** - * Runs visual tests + * Runs e2e tests * @param {Config} config - configs * @return {Promise} resolved on success or rejected with error */ @@ -38,45 +32,30 @@ function run(config) { logger.setLevel(config.verbose); - // log framework version - var pjson = require('../package.json'); - logger.info(pjson.name + ' v' + pjson.version); + utils.logFrameworkVersion(); - // log config object so far - logger.debug('Config from command-line: ${JSON.stringify(config)}',{config:config}); + logger.debug('Config from command-line: ' + JSON.stringify({ config: config })); // merge in config files var configParser = require('./configParser')(logger); config = configParser.mergeConfigs(config); - config.osTypeString = (function() { - var os = require('os'); - var osType = ''; - - if (os.type() == 'Darwin') { - osType = 'mac64'; - } else if (os.type() == 'Linux') { - if (os.arch() == 'x64') { - osType = 'linux64'; - } else { - osType = 'linux32'; - } - } else if (os.type() == 'Windows_NT') { - osType = 'win32'; - } else { - osType = 'unknown'; - } - - return osType; - })(); + config.osTypeString = utils.getOSType(); var moduleLoader = require('./moduleLoader')(config); - var plugins = new Plugins(moduleLoader.loadModule('plugins')); // resolve runtime and set browsers with capabilities - var runtimeResolver = require('./runtimeResolver')(config,logger); + var runtimeResolver = require('./runtimeResolver')(config, logger); config.runtimes = runtimeResolver.resolveRuntimes(); + // resolve browsers capabilities from runtime + var runtimeCapabilitiesResolver = require('./runtimeCapabilitiesResolver')(); + var multiCapabilities = config.runtimes.map(function (runtime) { + // prepare capabilities from runtime for this specific connection type + return runtimeCapabilitiesResolver.resolveCapabilitiesFromRuntime(runtime); + }); + logger.debug('Resolved multiCapabilities: ' + JSON.stringify({ multiCapabilities: multiCapabilities })); + // resolve all placeholders in config configParser.resolvePlaceholders(config); @@ -84,9 +63,8 @@ function run(config) { logger.setLevel(config.verbose); // log config object so far - logger.debug('Config after resolving config file and profile: ${JSON.stringify(config)}',{config:config}); + logger.debug('Config after resolving config file and profile: ' + JSON.stringify({ config: config })); - // log cwd logger.debug('Current working directory: ' + process.cwd()); // load spec resolver @@ -95,298 +73,36 @@ function run(config) { // resolve specs. // specs refers to the collection of found .spec.js files (a file containing Jasmine suites) logger.info('Resolving specs'); - return specResolver.resolve().then(function(specs){ - if (!specs || specs.length==0){ + return specResolver.resolve().then(function (specs) { + if (!specs || specs.length == 0) { throw Error('No specs found'); } - // resolve connection - var connectionName = config.connection || DEFAULT_CONNECTION_NAME; - var connectionConfig = config.connectionConfigs[connectionName]; - if (!connectionConfig){ - throw Error('Could not find connection: ' + connectionName); - } - - // create connectionProvider - var connectionProvider = moduleLoader.loadNamedModule('connection', [plugins]); - - // prepare executor args - var launcherArgv = connectionProvider.buildLauncherArgv(); - - // enable debug logs - launcherArgv.troubleshoot = config.verbose > 0; - - // add baseUrl - launcherArgv.baseUrl = config.baseUrl; - - // use jasmine 2.0 - launcherArgv.framework = 'jasmine2'; - launcherArgv.jasmineNodeOpts = {}; - - // disable default jasmine console reporter - launcherArgv.jasmineNodeOpts.print = function() {}; - - // copy timeouts - if (config.timeouts){ - if (config.timeouts.getPageTimeout){ - var getPageTimeout = config.timeouts.getPageTimeout; - if(_.isString(getPageTimeout)){ - getPageTimeout = parseInt(getPageTimeout,10); - } - logger.debug('Setting getPageTimeout: ' + getPageTimeout); - launcherArgv.getPageTimeout = getPageTimeout; - } - if (config.timeouts.allScriptsTimeout){ - var allScriptsTimeout = config.timeouts.allScriptsTimeout; - if(_.isString(allScriptsTimeout)){ - allScriptsTimeout = parseInt(allScriptsTimeout,10); - } - logger.debug('Setting allScriptsTimeout: ' + allScriptsTimeout); - launcherArgv.allScriptsTimeout = allScriptsTimeout; - } - if (config.timeouts.defaultTimeoutInterval){ - var defaultTimeoutInterval = config.timeouts.defaultTimeoutInterval; - if(_.isString(defaultTimeoutInterval)){ - defaultTimeoutInterval = parseInt(defaultTimeoutInterval,10); - } - logger.debug('Setting defaultTimeoutInterval: ' + defaultTimeoutInterval); - launcherArgv.jasmineNodeOpts.defaultTimeoutInterval = defaultTimeoutInterval; - } - } - - // set specs - launcherArgv.specs = []; - specs.forEach(function(spec){ - launcherArgv.specs.push(spec.testPath); + var launcherArgv = _.extend(config, { + specs: _.map(specs, 'testPath'), + specsWithDetails: specs, + multiCapabilities: multiCapabilities, + // require direct connection as directDriverProvider will be overtaken later + directConnect: true, + troubleshoot: config.verbose > 0, + baseUrl: config.baseUrl, + // use jasmine 2.0 + framework: 'jasmine2', + jasmineNodeOpts: { + // disable default jasmine console reporter + print: function () { } + }, + // onPrepare is executed after test env setup and just before test execution starts (possibly in child process) + // use filename relative to launcher - instead of function, to support multi browser + onPrepare: path.join(__dirname, './testPreparer') }); - // resolve browsers capabilities from runtime - launcherArgv.multiCapabilities = config.runtimes.map(function(runtime){ - // prepare capabilities from runtime for this specific connection type - return connectionProvider.resolveCapabilitiesFromRuntime(runtime); - }); - logger.debug('Resolved multiCapabilities: ' + JSON.stringify(launcherArgv.multiCapabilities)); - - // TODO see driverProvider - // no way to implement concurrent executions with current driverProvider impl - // launcherArgv.maxSessions = 1; - - // execute after test env setup and just before test execution starts - launcherArgv.onPrepare = function () { - plugins.loadJasminePlugins(); - - browser.setConfig(config); - - var matchers = {}; - var storageProvider; - - // register a hook to be called after webdriver is created ( may not be connected yet ) - browser.getProcessedConfig().then(function (processedConfig) { - var runtime = connectionProvider.resolveRuntimeFromCapabilities(processedConfig.capabilities); - logger.debug('Runtime resolved from capabilities: ' + JSON.stringify(runtime)); - - browser.setRuntime(runtime); - - // register screenshot provider - var screenshotProvider = moduleLoader.loadModuleIfAvailable('screenshotProvider', [runtime]); - if (screenshotProvider) { - screenshotProvider.register(); - } - - // load storage provider - storageProvider = moduleLoader.loadModuleIfAvailable('storageProvider', [runtime]); + utils.copyTimeouts(launcherArgv, config); - // load comparison provider and register the custom matcher - var comparisonProvider = moduleLoader.loadModuleIfAvailable('comparisonProvider', [storageProvider]); - if (comparisonProvider) { - comparisonProvider.register(matchers); - } + logger.info('Executing ' + specs.length + ' specs'); - moduleLoader.loadModuleIfAvailable('matchers', []).forEach(function(matcher){ - matcher.register(matchers); - }); - - browser.setInitialWindowSize(); - }); - - // add global matchers - beforeEach(function() { - jasmine.getEnv().addMatchers(matchers); - }); - - // hook into specs lifecycle - // open test content page before every suite - jasmine.getEnv().addReporter({ - - jasmineStarted: function(){ - // call storage provider beforeAll hook - if (storageProvider && storageProvider.onBeforeAllSpecs){ - storageProvider.onBeforeAllSpecs(specs); - } - }, - - //TODO consider several describe() per spec file - suiteStarted: function(result){ - - // enclose all WebDriver operations in a new flow for a gracefull handling of failures - // will call jasmine.fail() that will handle the error - browser.controlFlow().execute(function() { - - var spec = _getSpecDetails(result); - var contentUrl = spec ? spec.contentUrl : config.baseUrl; - - // open content page if required - if (!contentUrl) { - logger.debug('Skip content page opening'); - return; - } - - // webdriverjs operations are inherently synchronized by webdriver flow - // so no need to synchronize manually with callbacks/promises - - // add request params - var specUrl = url.parse(contentUrl); - if (config.baseUrlQuery && config.baseUrlQuery.length >0){ - if (specUrl.search == null) { - specUrl.search = ''; - } - config.baseUrlQuery.forEach(function(value,index){ - if (index > 0){ - specUrl.search += '&'; - } - specUrl.search += value; - }); - } - - // open test page - var specUrlString = url.format(specUrl); - browser.testrunner.navigation.to(specUrlString).then(function () { - // call storage provider beforeEach hook - if (storageProvider && storageProvider.onBeforeEachSpec) { - storageProvider.onBeforeEachSpec(spec); - } - }); - }).catch(function(error){ - // the failure in reporter -> beforeAll will not stop further suite execution ! - // fail-fast was discussed here -> https://github.com/jasmine/jasmine/issues/778 - // stop the suite will require jasmin 3.0 -> https://github.com/jasmine/jasmine/issues/414 - // stop the spec when error require jasmin 2.4 -> https://jasmine.github.io/2.4/node.html#section-13 - // completing of this functionality in jasmine 2.8 -> https://github.com/jasmine/jasmine/issues/577 - // In jasmine 2.3 a throwOnExpectationFailure(true) was added -> https://stackoverflow.com/questions/22119193/stop-jasmine-test-after-first-expect-fails - // it does not make sense for us at it simply throws error from the first failed expectation and this kills the whole execution - fail(error); - }); - }, - - suiteDone: function(result){ - var spec = _getSpecDetails(result); - // call storage provider afterEach hook - if (spec && storageProvider && storageProvider.onAfterEachSpec){ - storageProvider.onAfterEachSpec(spec); - } - }, - - jasmineDone: function(){ - // call storage provider afterAll hook - if (storageProvider && storageProvider.onAfterAllSpecs){ - storageProvider.onAfterAllSpecs(specs); - } - } - }); - - // initialize statistic collector - var statisticCollector = require('./statisticCollector')(); - jasmine.getEnv().addReporter({ - jasmineStarted: function(){ - statisticCollector.jasmineStarted(); - }, - suiteStarted: function(jasmineSuite){ - statisticCollector.suiteStarted(jasmineSuite); - }, - specStarted: function(jasmineSpec){ - statisticCollector.specStarted(jasmineSpec); - }, - specDone: function(jasmineSpec){ - statisticCollector.specDone(jasmineSpec, browser.testrunner.currentSpec._meta); - delete browser.testrunner.currentSpec._meta; - }, - suiteDone: function(jasmineSuite){ - statisticCollector.suiteDone(jasmineSuite, browser.testrunner.currentSuite._meta); - delete browser.testrunner.currentSuite._meta; - }, - jasmineDone: function(){ - statisticCollector.jasmineDone({runtime:browser.testrunner.runtime}); - } - }); - - if (config.exportParamsFile) { - jasmine.getEnv().addReporter({ - jasmineDone: function () { - logger.debug('Exporting test params to file ' + config.exportParamsFile); - fs.writeFileSync(config.exportParamsFile, JSON.stringify(browser.testrunner.config.exportParams, null, 2)); - } - }); - } - - var authConfigModule; - // expose navigation helpers to tests - browser.testrunner.navigation._getAuthenticator = function (authConfig) { - var authenticator; - if (authConfig) { - // programatically invoked authentication - load auth module every time - authenticator = moduleLoader.loadNamedModule(authConfig, [statisticCollector]); - } else { - // auth is declared in config - load the (global) auth module only once. - // when authOnce is enabled, auth should be done only once - before the first spec file. - if (authConfigModule) { - authenticator = config.authOnce ? null : authConfigModule; - } else { - authenticator = authConfigModule = moduleLoader.loadNamedModule(AUTH_CONFIG_NAME, [statisticCollector]); - } - } - return authenticator; - }; - - var expectationInterceptor = require('./reporter/expectationInterceptor'); - // register reporters - var jasmineEnv = jasmine.getEnv(); - moduleLoader.loadModule('reporters',[statisticCollector, expectationInterceptor]).forEach(function(reporter){ - reporter.register(jasmineEnv); - }); - - // add additional locators - moduleLoader.loadModule('locators', [statisticCollector]).forEach(function (locator) { - locator.register(by); - }); - - }; - - launcherArgv.afterLaunch = function(){ - // teardown connection provider env - logger.debug('Tearing down connection provider environment'); - return connectionProvider.teardownEnv(); - }; - - // finds a UIVeri5 spec definition by a given Jasmine suite description - // i.e. given the description of one of the suites in a spec file, find the information for that file. - function _getSpecDetails(suite){ - return specs.filter(function (suiteDetails) { - return suiteDetails.fullName === suite.description; - })[0]; - } - - // register page object factory on global scope - logger.debug('Loading BDD-style page object factory'); - pageObjectFactory.register(global); - - // setup connection provider env - logger.debug('Setting up connection provider environment'); - return connectionProvider.setupEnv().then(function(){ - logger.info('Executing ' + specs.length + ' specs'); - - var launcher = require('./ptor/launcher'); - launcher.init(launcherArgv, connectionProvider, plugins); - }); + var launcher = require('./ptor/launcher'); + return launcher.init(launcherArgv); }); } From bd7c2a36f681128b346ec9244d1214de5c4698c0 Mon Sep 17 00:00:00 2001 From: tsaleksandrova Date: Fri, 23 Apr 2021 09:33:38 +0300 Subject: [PATCH 2/4] feat: restart after suite --- docs/config/config.md | 10 ++++ src/browser/browser.js | 24 ++++++++-- src/coreReporters/runnerReporter.js | 13 +++++ src/ptor/runner.js | 73 +++++++++++------------------ src/testPreparer.js | 28 +---------- 5 files changed, 71 insertions(+), 77 deletions(-) diff --git a/docs/config/config.md b/docs/config/config.md index da9121e8..105b7804 100644 --- a/docs/config/config.md +++ b/docs/config/config.md @@ -137,3 +137,13 @@ Use the following capabilities to run every spec file in a separate browser para } }] ``` + +## Restart browser between suites +Disabled by default. +Note the current limitation that each spec file should contain exactly one suite. +```javascript +exports.config = { + restartBrowserBetweenSuites: true +} +``` +Protractor's `restartBrowserBetweenTests` is also available and false by default. diff --git a/src/browser/browser.js b/src/browser/browser.js index 44640c31..926b21db 100644 --- a/src/browser/browser.js +++ b/src/browser/browser.js @@ -10,9 +10,11 @@ var expectedConditions = require('../element/expectedConditions'); var logger = require('../logger'); var elementUtil = require('./elementUtil'); var clientsideScripts = require('../scripts/clientsidescripts'); +var statisticCollector = require('../statisticCollector'); var DEFAULT_RESET_URL = 'data:text/html,'; var DEFAULT_GET_PAGE_TIMEOUT = 10000; +var AUTH_CONFIG_NAME = 'auth'; /** * @alias browser @@ -88,11 +90,25 @@ var Browser = function (webdriverInstance, opt_baseUrl) { }, navigation: { waitForRedirect: browser._waitForRedirect.bind(browser), + // expose navigation helpers to tests to: browser._navigateTo.bind(browser), - _getAuthenticator: function () { - // override in uiveri5.js - needs moduleLoader and statisticCollector - return null; - } + _getAuthenticator: function (authConfig) { + var moduleLoader = require('../moduleLoader')(this.testrunner.config); + var authenticator; + if (authConfig) { + // programatically invoked authentication - load auth module every time + authenticator = moduleLoader.loadNamedModule(authConfig, [statisticCollector]); + } else { + // if auth is declared in config, load the (global) auth module only once. + // when authOnce is enabled, auth should be done only once - before the first spec file. + if (this.authenticator) { + authenticator = this.testrunner.config.authOnce ? null : this.authenticator; + } else { + authenticator = this.authenticator = moduleLoader.loadNamedModule(AUTH_CONFIG_NAME, [statisticCollector]); + } + } + return authenticator; + }.bind(this) } }; diff --git a/src/coreReporters/runnerReporter.js b/src/coreReporters/runnerReporter.js index 65eeb07d..9da08c90 100644 --- a/src/coreReporters/runnerReporter.js +++ b/src/coreReporters/runnerReporter.js @@ -12,6 +12,19 @@ RunnerReporter.prototype.jasmineStarted = function () { // Need to initiate startTime here, in case reportSpecStarting is not // called (e.g. when fit is used) this.startTime = new Date(); + + browser.plugins_.addPlugin({ + specDone: function () { + if (this.emitter.config.restartBrowserBetweenTests) { + browser.restartSync(); + } + }.bind(this), + suiteDone: function () { + if (this.emitter.config.restartBrowserBetweenSuites) { + browser.restartSync(); + } + }.bind(this) + }); }; RunnerReporter.prototype.specStarted = function () { diff --git a/src/ptor/runner.js b/src/ptor/runner.js index c275fb51..88044234 100644 --- a/src/ptor/runner.js +++ b/src/ptor/runner.js @@ -1,7 +1,6 @@ 'use strict'; var events = require('events'); -var q = require('q'); var selenium_webdriver = require('selenium-webdriver'); var util = require('util'); var browser = require('../browser/browser'); @@ -25,7 +24,7 @@ var connectionProvider = require('../connection/connectionProvider'); */ function Runner(config) { events.EventEmitter.apply(this, arguments); - this.config_ = config; + this.config = config; if (config.capabilities && config.capabilities.seleniumAddress) { config.seleniumAddress = config.capabilities.seleniumAddress; @@ -49,7 +48,7 @@ Runner.prototype.constructor = Runner; * @param {int} Standard unix exit code */ Runner.prototype.exit_ = function (exitCode) { - return helper.runFilenameOrFn_(this.config_.configDir, this.config_.onCleanUp, [exitCode]) + return helper.runFilenameOrFn_(this.config.configDir, this.config.onCleanUp, [exitCode]) .then((returned) => { if (typeof returned === 'number') { return returned; @@ -77,50 +76,31 @@ Runner.prototype.setTestPreparer = function (filenameOrFn) { * are finished. */ Runner.prototype.runTestPreparer = function (extraFlags) { - var unknownFlags = this.config_.unknownFlags_ || []; + var unknownFlags = this.config.unknownFlags_ || []; if (extraFlags) { unknownFlags = unknownFlags.filter((f) => extraFlags.indexOf(f) === -1); } - if (unknownFlags.length > 0 && !this.config_.disableChecks) { + if (unknownFlags.length > 0 && !this.config.disableChecks) { logger.info('Ignoring unknown extra flags: ' + unknownFlags.join(', ') + '. This will be' + ' an error in future versions, please use --disableChecks flag to disable the CLI flag checks.'); } return uiveri5Plugins.onPrepare().then(() => { - return helper.runFilenameOrFn_(this.config_.configDir, this.preparer_, this.config_); + return helper.runFilenameOrFn_(this.config.configDir, this.preparer_, this.config); }); }; -/** - * Called after each test finishes. - * - * Responsible for `restartBrowserBetweenTests` - * - * @public - * @return {q.Promise} A promise that will resolve when the work here is done - */ -Runner.prototype.afterEach = function () { - var ret; - this.frameworkUsesAfterEach = true; - if (this.config_.restartBrowserBetweenTests) { - this.restartPromise = this.restartPromise || q(ptor.protractor.browser.restart()); - ret = this.restartPromise; - this.restartPromise = undefined; - } - return ret || q(); -}; - /** * get uiveri5 direct driver provider * @private */ Runner.prototype.loadDriverProvider_ = function (config) { connectionProvider.verifyConfig(config); - this.config_ = config; + this.config = config; this.connection = this.moduleLoader.loadNamedModule('connection'); connectionProvider.setConnection(this.connection); - this.driverprovider_ = this.connection.buildDriverProvider(this.config_); + this.driverprovider_ = this.connection.buildDriverProvider(this.config); }; /** @@ -129,7 +109,7 @@ Runner.prototype.loadDriverProvider_ = function (config) { * @return {Object} config */ Runner.prototype.getConfig = function () { - return this.config_; + return this.config; }; /** @@ -152,7 +132,7 @@ Runner.prototype.setupGlobals_ = function (browser_) { ptor.protractor.element = browser_.element; ptor.protractor.by = ptor.protractor.By = browser.Browser.By; ptor.protractor.ExpectedConditions = browser_.ExpectedConditions; - if (!this.config_.noGlobals) { + if (!this.config.noGlobals) { // Export protractor to the global namespace to be used in tests. global.browser = browser_; global.$ = browser_.$; @@ -182,7 +162,7 @@ Runner.prototype.setupGlobals_ = function (browser_) { * @public */ Runner.prototype.createBrowser = function (plugins, parentBrowser) { - var config = this.config_; + var config = this.config; var driver = this.driverprovider_.getNewDriver(); var initProperties = { baseUrl: config.baseUrl, @@ -198,14 +178,22 @@ Runner.prototype.createBrowser = function (plugins, parentBrowser) { initProperties.allScriptsTimeout = parentBrowser.allScriptsTimeout; } var browser_ = new browser.Browser(driver, initProperties.baseUrl, initProperties.rootElement); + + browser_.setConfig(config); + + logger.debug('Runtime resolved from capabilities: ' + JSON.stringify(config.multiCapabilities)); + browser_.setRuntime(config.multiCapabilities); + browser_.params = initProperties.params; browser_.plugins_ = plugins || uiveri5Plugins; + if (initProperties.getPageTimeout) { browser_.getPageTimeout = initProperties.getPageTimeout; } if (initProperties.allScriptsTimeout) { browser_.allScriptsTimeout = initProperties.allScriptsTimeout; } + browser_.ready = browser_.ready .then(() => { @@ -214,9 +202,11 @@ Runner.prototype.createBrowser = function (plugins, parentBrowser) { .then(() => { return browser_; }); + browser_.getProcessedConfig = () => { return selenium_webdriver.promise.when(config); }; + var replaceBrowser = () => { var newBrowser = this.createBrowser(plugins); if (browser_ === ptor.protractor.browser) { @@ -224,12 +214,15 @@ Runner.prototype.createBrowser = function (plugins, parentBrowser) { } return newBrowser; }; + browser_.restart = () => { // Note: because tests are not paused at this point, any async // calls here are not guaranteed to complete before the tests resume. return browser_.restartSync().ready; }; + browser_.restartSync = () => { + logger.debug('Restarting browser'); this.driverprovider_.quitDriver(browser_.driver); return replaceBrowser(); }; @@ -257,13 +250,10 @@ Runner.prototype.run = function () { var testPassed; var browser_; var results; - if (this.config_.framework !== 'explorer' && !this.config_.specs.length) { + if (!this.config.specs.length) { throw new Error('Spec patterns did not match any files.'); } - if (this.config_.SELENIUM_PROMISE_MANAGER != null) { - selenium_webdriver.promise.USE_PROMISE_MANAGER = this.config_.SELENIUM_PROMISE_MANAGER; - } - return this.connection.setupEnv(this.config_.multiCapabilities) + return this.connection.setupEnv(this.config.multiCapabilities) .then(this.driverprovider_.setupEnv) .then(() => { // 2) Create a browser and setup globals @@ -286,18 +276,9 @@ Runner.prototype.run = function () { .then(() => { // Do the framework setup here so that jasmine globals are // available to the onPrepare function. - if (this.config_.restartBrowserBetweenTests) { - var restartDriver = () => { - if (!this.frameworkUsesAfterEach) { - this.restartPromise = q(browser_.restart()); - } - }; - this.on('testPass', restartDriver); - this.on('testFail', restartDriver); - } - logger.debug('Running with spec files ' + this.config_.specs); + logger.debug('Running with spec files ' + this.config.specs); var jasmine = require('./frameworks/jasmine'); - return jasmine.run(this, this.config_.specs); + return jasmine.run(this, this.config.specs); }) .then((testResults) => { results = testResults; diff --git a/src/testPreparer.js b/src/testPreparer.js index f0d1a75d..bd91f14c 100644 --- a/src/testPreparer.js +++ b/src/testPreparer.js @@ -4,17 +4,10 @@ var logger = require('./logger'); var plugins = require('./plugins/plugins'); var pageObjectFactory = require('./pageObjectFactory'); -var AUTH_CONFIG_NAME = 'auth'; - +// config is the test Runner's config module.exports = function (config) { - // config is the test Runner's config - browser.setConfig(config); browser.controlFlow().execute(function () { - logger.debug('Runtime resolved from capabilities: ' + JSON.stringify(config.multiCapabilities)); - - browser.setRuntime(config.multiCapabilities); - var moduleLoader = require('./moduleLoader')(config); // register screenshot provider @@ -49,25 +42,6 @@ module.exports = function (config) { require('./coreReporters/paramsReporter')(config).register(); require('./coreReporters/specLifecycleReporter')(config, storageProvider).register(); - var authConfigModule; - // expose navigation helpers to tests - browser.testrunner.navigation._getAuthenticator = function (authConfig) { - var authenticator; - if (authConfig) { - // programatically invoked authentication - load auth module every time - authenticator = moduleLoader.loadNamedModule(authConfig, [statisticCollector]); - } else { - // auth is declared in config - load the (global) auth module only once. - // when authOnce is enabled, auth should be done only once - before the first spec file. - if (authConfigModule) { - authenticator = config.authOnce ? null : authConfigModule; - } else { - authenticator = authConfigModule = moduleLoader.loadNamedModule(AUTH_CONFIG_NAME, [statisticCollector]); - } - } - return authenticator; - }; - var expectationInterceptor = require('./reporter/expectationInterceptor'); // register reporters var jasmineEnv = jasmine.getEnv(); From af9a362e6ae1b1036707740b581de58cb9073cb7 Mon Sep 17 00:00:00 2001 From: tsaleksandrova Date: Thu, 20 May 2021 16:24:22 +0300 Subject: [PATCH 3/4] add params --- docs/config/config.md | 37 ++++- package-lock.json | 287 -------------------------------------- src/ptor/launcher.js | 7 +- src/ptor/taskRunner.js | 2 - src/ptor/taskScheduler.js | 19 ++- 5 files changed, 50 insertions(+), 302 deletions(-) diff --git a/docs/config/config.md b/docs/config/config.md index 105b7804..8642e9f2 100644 --- a/docs/config/config.md +++ b/docs/config/config.md @@ -127,23 +127,46 @@ Execute the visual test: $ uiveri5 --browsers=browser:*:android --seleniumAddress=http://127.0.0.1:4723/wd/hub --baseUrl=http://10.0.2.2:8080 ``` -## Run test files in parallel -Use the following capabilities to run every spec file in a separate browser parallelly +## Browser instances +Use the following capabilities to run every spec file in a separate browser parallely: ```javascript browsers: [{ browserName: "chrome", capabilities: { - shardTestFiles: true + runSpecFilesInParallelBrowsers: true } }] ``` -## Restart browser between suites -Disabled by default. -Note the current limitation that each spec file should contain exactly one suite. +Use the following capabilities to run every spec file in a separate browser consequetively: +```javascript + browsers: [{ + browserName: "chrome", + capabilities: { + restartBrowserBetweenSpecFiles: true + } + }] +``` + +Use the following capabilities to configure browser startup: +```javascript + browsers: [{ + browserName: "chrome", + capabilities: { + shardTestFiles: true, // execute each spec file in a separate browser; default = false + maxInstances: 5, // max number of browsers (with current capabilities) to run in parallel; default = 1 + maxSessions: 5, // max number of browsers (total) to run in parallel; default = unlimited + count: 1 // execute full set of specs multiple times with this browser config; default = 1 + } + }] +``` + +Restart between every `describe` block: ```javascript exports.config = { restartBrowserBetweenSuites: true } ``` -Protractor's `restartBrowserBetweenTests` is also available and false by default. + +Protractor's `restartBrowserBetweenTests` is also available. +All of the above options are disabled by default. diff --git a/package-lock.json b/package-lock.json index 3dfedc59..ce767137 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,18 +73,6 @@ "array-from": "^2.1.1" } }, - "@types/node": { - "version": "6.14.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.13.tgz", - "integrity": "sha512-J1F0XJ/9zxlZel5ZlbeSuHW2OpabrUAqpFuC2sm2I3by8sERQ8+KCjNKUcq8QHuzpGMWiJpo9ZxeHrqrP2KzQw==", - "dev": true - }, - "@types/q": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", - "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=", - "dev": true - }, "@types/selenium-webdriver": { "version": "2.53.45", "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-2.53.45.tgz", @@ -123,15 +111,6 @@ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.13.tgz", "integrity": "sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw==" }, - "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "dev": true, - "requires": { - "es6-promisify": "^5.0.0" - } - }, "ajv": { "version": "6.5.5", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", @@ -193,27 +172,6 @@ "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", "dev": true }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -276,23 +234,6 @@ "tweetnacl": "^0.14.3" } }, - "blocking-proxy": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz", - "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - } - } - }, "body-parser": { "version": "1.18.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", @@ -787,21 +728,6 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", - "dev": true, - "requires": { - "globby": "^5.0.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "rimraf": "^2.2.8" - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -920,21 +846,6 @@ "is-arrayish": "^0.2.1" } }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", - "dev": true - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "dev": true, - "requires": { - "es6-promise": "^4.0.3" - } - }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1466,45 +1377,6 @@ "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", "dev": true }, - "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", @@ -1759,16 +1631,6 @@ "integrity": "sha512-puSi8M8WNlFJm9Pk4c/Mbz9Gwparuj3gO9/RRO5zv6piQ0FY+9Qywp0PdWshYgsMJSalixFY7eC6oPu0zRxLAQ==", "dev": true }, - "https-proxy-agent": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", - "dev": true, - "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - } - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1818,12 +1680,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, "inquirer": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.0.tgz", @@ -1950,30 +1806,6 @@ "number-is-nan": "^1.0.0" } }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "dev": true, - "requires": { - "is-path-inside": "^1.0.0" - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } - }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", @@ -2585,16 +2417,6 @@ "pinkie-promise": "^2.0.0" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, "optionator": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", @@ -2794,85 +2616,6 @@ "integrity": "sha1-StIXuzZYvKrplGsXqGaOzYUeE1Y=", "dev": true }, - "protractor": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.3.2.tgz", - "integrity": "sha512-pw4uwwiy5lHZjIguxNpkEwJJa7hVz+bJsvaTI+IbXlfn2qXwzbF8eghW/RmrZwE2sGx82I8etb8lVjQ+JrjejA==", - "dev": true, - "requires": { - "@types/node": "^6.0.46", - "@types/q": "^0.0.32", - "@types/selenium-webdriver": "~2.53.39", - "blocking-proxy": "^1.0.0", - "chalk": "^1.1.3", - "glob": "^7.0.3", - "jasmine": "2.8.0", - "jasminewd2": "^2.1.0", - "optimist": "~0.6.0", - "q": "1.4.1", - "saucelabs": "^1.5.0", - "selenium-webdriver": "3.6.0", - "source-map-support": "~0.4.0", - "webdriver-js-extender": "^1.0.0", - "webdriver-manager": "^12.0.6" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "q": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", - "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", - "dev": true - }, - "webdriver-manager": { - "version": "12.1.8", - "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.8.tgz", - "integrity": "sha512-qJR36SXG2VwKugPcdwhaqcLQOD7r8P2Xiv9sfNbfZrKBnX243iAkOueX1yAmeNgIKhJ3YAT/F2gq6IiEZzahsg==", - "dev": true, - "requires": { - "adm-zip": "^0.4.9", - "chalk": "^1.1.1", - "del": "^2.2.0", - "glob": "^7.0.3", - "ini": "^1.3.4", - "minimist": "^1.2.0", - "q": "^1.4.1", - "request": "^2.87.0", - "rimraf": "^2.5.2", - "semver": "^5.3.0", - "xml2js": "^0.4.17" - } - } - } - }, "proxy-addr": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", @@ -3126,15 +2869,6 @@ "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", "dev": true }, - "saucelabs": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz", - "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==", - "dev": true, - "requires": { - "https-proxy-agent": "^2.2.1" - } - }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -3407,21 +3141,6 @@ } } }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "dev": true, - "requires": { - "source-map": "^0.5.6" - } - }, "spdx-correct": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.2.tgz", @@ -4027,12 +3746,6 @@ } } }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - }, "wrap-ansi": { "version": "2.1.0", "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", diff --git a/src/ptor/launcher.js b/src/ptor/launcher.js index 218933c4..3bb6c434 100644 --- a/src/ptor/launcher.js +++ b/src/ptor/launcher.js @@ -191,13 +191,18 @@ var init = function (config) { }); }; var totalTasks = scheduler.numTasksOutstanding(); - var forkProcess = false; + var forkProcess; if (totalTasks > 1) { forkProcess = true; + logger.debug('Running multiple browsers in child processes'); if (config.debug) { throw new exitCodes.ConfigError(logger, 'Cannot run in debug mode with multiCapabilities, count > 1, or sharding'); } + } else { + forkProcess = false; + logger.debug('Running one browser in the current process'); } + var deferred = q.defer(); // Resolved when all tasks are completed var createNextTaskRunner = () => { var task = scheduler.nextTask(); diff --git a/src/ptor/taskRunner.js b/src/ptor/taskRunner.js index 87e6a057..63c235ab 100644 --- a/src/ptor/taskRunner.js +++ b/src/ptor/taskRunner.js @@ -58,7 +58,6 @@ TaskRunner.prototype.run = function () { config.specs = this.task.specs; if (this.runInFork) { - logger.debug('Running multiple browsers in child processes'); var deferred = q.defer(); var childProcess = child_process.fork(__dirname + '/runnerCli.js', process.argv.slice(2), { cwd: process.cwd(), @@ -110,7 +109,6 @@ TaskRunner.prototype.run = function () { return deferred.promise; } else { - logger.debug('Running one browser in the current process'); var runner = new Runner(config); runner.on('testsDone', (results) => { runResults.failedCount = results.failedCount; diff --git a/src/ptor/taskScheduler.js b/src/ptor/taskScheduler.js index 39c1b238..0f469673 100644 --- a/src/ptor/taskScheduler.js +++ b/src/ptor/taskScheduler.js @@ -9,7 +9,7 @@ var ConfigParser = require('./configParser').ConfigParser; * - shardTestFiles - If this is set to be true, specs will be sharded by file * (i.e. all files to be run by this set of capabilities will run in parallel). Default is false. * - maxInstances - Maximum number of browser instances that can run in parallel for this set of capabilities. - * This is only needed if shardTestFiles is true. Default is in UIVeri5 (and 1 in protractor). + * This is only needed if shardTestFiles is true. Default is 1. * - maxSessions - Maximum number of total browser sessions to run. * Tests are queued in sequence if number of browser sessions is limited by this parameter. * Use a number less than 1 to denote unlimited. Default is unlimited. @@ -60,18 +60,27 @@ function TaskScheduler(config) { return capabilitiesSpecExcludes.indexOf(path) < 0; }); } + + if (capabilities.restartBrowserBetweenSpecFiles) { + // run every spec file in a separate browser - consequetively + capabilities.shardTestFiles = true; + capabilities.maxInstances = 1; + } + if (capabilities.runSpecFilesInParallelBrowsers) { + // run every spec file in a separate browser - parallely + capabilities.shardTestFiles = true; + // run the maximum number of browsers in parallel + capabilities.maxInstances = allSpecs.length; + } + var specLists = []; // If we shard, we return an array of one element arrays, each containing // the spec file. If we don't shard, we return an one element array // containing an array of all the spec files - if (capabilities.shardTestFiles) { capabilitiesSpecs.forEach((spec) => { specLists.push([spec]); }); - - // always allow to run the maximum number of browsers in parallel - capabilities.maxInstances = capabilities.maxInstances || config.specs.length; } else { specLists.push(capabilitiesSpecs); } From 1119b6715454685d0a062cbe811527a270a781ed Mon Sep 17 00:00:00 2001 From: tsaleksandrova Date: Fri, 21 May 2021 09:59:08 +0300 Subject: [PATCH 4/4] root level params --- docs/config/config.md | 47 +++----- src/coreReporters/runnerReporter.js | 13 --- src/coreReporters/statisticReporter.js | 58 +++++----- src/ptor/helper.js | 22 +++- src/ptor/launcher.js | 68 +++++------ src/ptor/runner.js | 15 +-- src/ptor/taskLogger.js | 1 - src/ptor/taskRunner.js | 1 - src/ptor/taskScheduler.js | 154 +++++++------------------ src/statisticCollector.js | 27 +++-- src/testPreparer.js | 2 +- 11 files changed, 166 insertions(+), 242 deletions(-) diff --git a/docs/config/config.md b/docs/config/config.md index 8642e9f2..d3da1361 100644 --- a/docs/config/config.md +++ b/docs/config/config.md @@ -128,45 +128,24 @@ $ uiveri5 --browsers=browser:*:android --seleniumAddress=http://127.0.0.1:4723/w ``` ## Browser instances -Use the following capabilities to run every spec file in a separate browser parallely: +Use `restartBrowserBetweenSpecs` to restart the browser between every spec file. Defaults to false: ```javascript - browsers: [{ - browserName: "chrome", - capabilities: { - runSpecFilesInParallelBrowsers: true - } - }] -``` - -Use the following capabilities to run every spec file in a separate browser consequetively: -```javascript - browsers: [{ - browserName: "chrome", - capabilities: { - restartBrowserBetweenSpecFiles: true - } - }] -``` - -Use the following capabilities to configure browser startup: -```javascript - browsers: [{ - browserName: "chrome", - capabilities: { - shardTestFiles: true, // execute each spec file in a separate browser; default = false - maxInstances: 5, // max number of browsers (with current capabilities) to run in parallel; default = 1 - maxSessions: 5, // max number of browsers (total) to run in parallel; default = unlimited - count: 1 // execute full set of specs multiple times with this browser config; default = 1 - } +exports.config = { + restartBrowserBetweenSpecs: true, + browsers: [{ + browserName: "chrome" }] +} ``` -Restart between every `describe` block: +Use `maxInstances` to run every spec file in a separate browser parallely. Up to `maxInstances` browsers will be running at any given moment. +When a spec is done, the browser instance is destroyed and a new browser is started. +Defaults to 1 - run one browser at a time. ```javascript exports.config = { - restartBrowserBetweenSuites: true + maxInstances: 5, + browsers: [{ + browserName: "chrome" + }] } ``` - -Protractor's `restartBrowserBetweenTests` is also available. -All of the above options are disabled by default. diff --git a/src/coreReporters/runnerReporter.js b/src/coreReporters/runnerReporter.js index 9da08c90..65eeb07d 100644 --- a/src/coreReporters/runnerReporter.js +++ b/src/coreReporters/runnerReporter.js @@ -12,19 +12,6 @@ RunnerReporter.prototype.jasmineStarted = function () { // Need to initiate startTime here, in case reportSpecStarting is not // called (e.g. when fit is used) this.startTime = new Date(); - - browser.plugins_.addPlugin({ - specDone: function () { - if (this.emitter.config.restartBrowserBetweenTests) { - browser.restartSync(); - } - }.bind(this), - suiteDone: function () { - if (this.emitter.config.restartBrowserBetweenSuites) { - browser.restartSync(); - } - }.bind(this) - }); }; RunnerReporter.prototype.specStarted = function () { diff --git a/src/coreReporters/statisticReporter.js b/src/coreReporters/statisticReporter.js index 6f1966bd..f6e8d254 100644 --- a/src/coreReporters/statisticReporter.js +++ b/src/coreReporters/statisticReporter.js @@ -1,31 +1,33 @@ var statisticCollector = require('../statisticCollector'); -module.exports = { - register: function () { - jasmine.getEnv().addReporter({ - jasmineStarted: function () { - statisticCollector.jasmineStarted(); - }, - suiteStarted: function (jasmineSuite) { - statisticCollector.suiteStarted(jasmineSuite); - }, - specStarted: function (jasmineSpec) { - statisticCollector.specStarted(jasmineSpec); - }, - specDone: function (jasmineSpec) { - statisticCollector.specDone(jasmineSpec, browser.testrunner.currentSpec._meta); - delete browser.testrunner.currentSpec._meta; - }, - suiteDone: function (jasmineSuite) { - statisticCollector.suiteDone(jasmineSuite, browser.testrunner.currentSuite._meta); - delete browser.testrunner.currentSuite._meta; - }, - jasmineDone: function () { - statisticCollector.jasmineDone({ - runtime: browser.testrunner.runtime - }); - } - }); - - } +module.exports = function (config) { + return { + register: function () { + jasmine.getEnv().addReporter({ + jasmineStarted: function () { + statisticCollector.jasmineStarted(); + }, + suiteStarted: function (jasmineSuite) { + statisticCollector.suiteStarted(jasmineSuite); + }, + specStarted: function (jasmineSpec) { + statisticCollector.specStarted(jasmineSpec); + }, + specDone: function (jasmineSpec) { + statisticCollector.specDone(jasmineSpec, browser.testrunner.currentSpec._meta); + delete browser.testrunner.currentSpec._meta; + }, + suiteDone: function (jasmineSuite) { + statisticCollector.suiteDone(jasmineSuite, browser.testrunner.currentSuite._meta); + delete browser.testrunner.currentSuite._meta; + }, + jasmineDone: function () { + statisticCollector.jasmineDone({ + runtime: browser.testrunner.runtime, + tempJsonReport: config.tempJsonReport + }); + } + }); + } + }; }; diff --git a/src/ptor/helper.js b/src/ptor/helper.js index 8114d832..c169f64f 100644 --- a/src/ptor/helper.js +++ b/src/ptor/helper.js @@ -1,6 +1,7 @@ 'use strict'; var path = require('path'); +var fs = require('fs'); var q = require('q'); var selenium_webdriver = require('selenium-webdriver'); @@ -83,6 +84,24 @@ function falseIfMissing(error) { } } +function writeWhenFree(fileName, content) { + var deferred = q.defer(); + fs.open(fileName, 'w', function (err) { + if (!err) { + fs.writeFile(fileName, content, function () { + deferred.resolve(); + }); + } else if (err.code === 'EBUSY') { + setTimeout(function () { + writeWhenFree(fileName, content); + }, 50); + } else { + deferred.reject(err); + } + }); + return deferred.promise; +} + /** * Return a boolean given boolean value. * @@ -97,5 +116,6 @@ module.exports = { filterStackTrace: filterStackTrace, runFilenameOrFn_: runFilenameOrFn_, falseIfMissing: falseIfMissing, - passBoolean: passBoolean + passBoolean: passBoolean, + writeWhenFree: writeWhenFree }; diff --git a/src/ptor/launcher.js b/src/ptor/launcher.js index 3bb6c434..d2b7ce26 100644 --- a/src/ptor/launcher.js +++ b/src/ptor/launcher.js @@ -21,8 +21,9 @@ var RUNNERS_FAILED_EXIT_CODE = 100; * result, aggregate the results into a summary, count failures, * and save results into a JSON file. */ -function TaskResults() { +function TaskResults(config) { this.results_ = []; + this.config = config; } TaskResults.prototype.add = function (result) { @@ -41,12 +42,15 @@ TaskResults.prototype.totalProcessFailures = function () { }, 0); }; -TaskResults.prototype.saveResults = function (filepath) { - var jsonOutput = this.results_.reduce((jsonOutput, result) => { - return jsonOutput.concat(result.specResults); - }, []); - var json = JSON.stringify(jsonOutput, null, ' '); - fs.writeFileSync(filepath, json); +TaskResults.prototype.removeTempReport = function () { + try { + fs.unlinkSync(this.config.tempJsonReport); + logger.trace('temp JSON report is successfully deleted'); + } catch (err) { + if (err.code !== 'ENOENT') { + logger.trace('Error while trying to delete temp JSON report. Error: ' + err); + } + } }; TaskResults.prototype.reportSummary = function () { @@ -59,31 +63,26 @@ TaskResults.prototype.reportSummary = function () { capabilities.logName : (capabilities.browserName) ? capabilities.browserName : ''; shortName += (capabilities.version) ? capabilities.version : ''; - shortName += (capabilities.logName && capabilities.count < 2) ? '' : ' #' + result.taskId; + if (result.failedCount) { logger.info(shortName + ' failed ' + result.failedCount + ' test(s)'); - } - else if (result.exitCode !== 0) { + } else if (result.exitCode !== 0) { logger.info(shortName + ' failed with exit code: ' + result.exitCode); - } - else { + } else { logger.info(shortName + ' passed'); } }); + if (specFailures && processFailures) { logger.info('overall: ' + specFailures + ' failed spec(s) and ' + processFailures + ' process(es) failed to complete'); - } - else if (specFailures) { + } else if (specFailures) { logger.info('overall: ' + specFailures + ' failed spec(s)'); - } - else if (processFailures) { + } else if (processFailures) { logger.info('overall: ' + processFailures + ' process(es) failed to complete'); } }; -var taskResults_ = new TaskResults(); - /** * Initialize and run the tests. * Exits with 1 on test failure, and RUNNERS_FAILED_EXIT_CODE on unexpected @@ -95,6 +94,8 @@ var init = function (config) { var configParser = new ConfigParser(); configParser.addConfig(config); config = configParser.getConfig(); + + var taskResults_ = new TaskResults(config); logger.debug('Your base url for tests is ' + config.baseUrl); // Run beforeLaunch @@ -119,8 +120,7 @@ var init = function (config) { .catch(err => { reject(err); }); - } - else { + } else { resolve(); } }) @@ -158,8 +158,7 @@ var init = function (config) { var protractorError = e; exitCodes.ProtractorError.log(logger, errorCode, protractorError.message, protractorError.stack); process.exit(errorCode); - } - else { + } else { logger.error(e.message); logger.error(e.stack); process.exit(exitCodes.ProtractorError.CODE); @@ -168,8 +167,7 @@ var init = function (config) { process.on('exit', (code) => { if (code) { logger.error('Process exited with error code ' + code); - } - else if (scheduler.numTasksOutstanding() > 0) { + } else if (scheduler.numTasksOutstanding() > 0) { logger.error('BUG: launcher exited with ' + scheduler.numTasksOutstanding() + ' tasks remaining'); process.exit(RUNNERS_FAILED_EXIT_CODE); @@ -181,8 +179,7 @@ var init = function (config) { .then((returned) => { if (typeof returned === 'number') { process.exit(returned); - } - else { + } else { process.exit(exitCode); } }, (err) => { @@ -222,32 +219,29 @@ var init = function (config) { } logger.info(scheduler.countActiveTasks() + ' instance(s) of WebDriver still running'); }) - .catch((err) => { - logger.error('Error:', err.stack || err.message || err); + .catch((error) => { + logger.error('Error: ' + error); cleanUpAndExit(RUNNERS_FAILED_EXIT_CODE); }); } }; - // Start `scheduler.maxConcurrentTasks()` workers for handling tasks in - // the beginning. As a worker finishes a task, it will pick up the next - // task from the scheduler's queue until all tasks are gone. - for (var i = 0; i < scheduler.maxConcurrentTasks(); ++i) { + // Start `maxInstances` workers for handling tasks in the beginning. + // As a worker finishes a task, it will pick up the next task from the scheduler's queue until all tasks are gone. + for (var i = 0; i < config.maxInstances; ++i) { createNextTaskRunner(); } logger.info('Running ' + scheduler.countActiveTasks() + ' instances of WebDriver'); // By now all runners have completed. deferred.promise .then(function () { - // Save results if desired - if (config.resultJsonOutputFile) { - taskResults_.saveResults(config.resultJsonOutputFile); + if (config.tempJsonReport) { + taskResults_.removeTempReport(); } taskResults_.reportSummary(); var exitCode = 0; if (taskResults_.totalProcessFailures() > 0) { exitCode = RUNNERS_FAILED_EXIT_CODE; - } - else if (taskResults_.totalSpecFailures() > 0) { + } else if (taskResults_.totalSpecFailures() > 0) { exitCode = 1; } return cleanUpAndExit(exitCode); diff --git a/src/ptor/runner.js b/src/ptor/runner.js index 88044234..1ceae11f 100644 --- a/src/ptor/runner.js +++ b/src/ptor/runner.js @@ -9,6 +9,7 @@ var ptor = require('./ptor'); var helper = require('./helper'); var uiveri5Plugins = require('../plugins/plugins'); var connectionProvider = require('../connection/connectionProvider'); +var statisticCollector = require('../statisticCollector'); /* * Runner is responsible for starting the execution of a test run and triggering @@ -290,15 +291,15 @@ Runner.prototype.run = function () { .then(() => { this.emit('testsDone', results); testPassed = results.failedCount === 0; - if (this.driverprovider_.updateJob) { - return this.driverprovider_.updateJob({ 'passed': testPassed }).then(() => { - return this.driverprovider_.teardownEnv(); - }); - } - else { - return this.driverprovider_.teardownEnv(); + + if (this.config.tempJsonReport) { + var overview = JSON.stringify(statisticCollector.getOverview(), null, 2); + return helper.writeWhenFree(this.config.tempJsonReport, overview); } }) + .then(() => { + return this.driverprovider_.teardownEnv(); + }) .then(() => { var exitCode = testPassed ? 0 : 1; return this.exit_(exitCode); diff --git a/src/ptor/taskLogger.js b/src/ptor/taskLogger.js index 489782d6..fdc89a74 100644 --- a/src/ptor/taskLogger.js +++ b/src/ptor/taskLogger.js @@ -68,7 +68,6 @@ TaskLogger.prototype.log = function (data) { (capabilities.browserName) ? capabilities.browserName : ''; tag += (capabilities.version) ? (' ' + capabilities.version) : ''; tag += (capabilities.platform) ? (' ' + capabilities.platform) : ''; - tag += (capabilities.logName && capabilities.count < 2) ? '' : ' #' + this.task.taskId; tag += '] '; data = data.toString(); for (var i = 0; i < data.length; i++) { diff --git a/src/ptor/taskRunner.js b/src/ptor/taskRunner.js index 63c235ab..6ee744e1 100644 --- a/src/ptor/taskRunner.js +++ b/src/ptor/taskRunner.js @@ -3,7 +3,6 @@ var child_process = require('child_process'); var events = require('events'); var q = require('q'); -var logger = require('../logger'); var ConfigParser = require('./configParser').ConfigParser; var TaskLogger = require('./taskLogger').TaskLogger; diff --git a/src/ptor/taskScheduler.js b/src/ptor/taskScheduler.js index 0f469673..fa06d280 100644 --- a/src/ptor/taskScheduler.js +++ b/src/ptor/taskScheduler.js @@ -1,23 +1,13 @@ -'use strict'; - +var path = require('path'); var ConfigParser = require('./configParser').ConfigParser; -/** - * properties for parallelization and multi browser support: - * as taken from https://github.com/angular/protractor/blob/master/lib/config.ts - * - count - Number of times to run this set of capabilities (in parallel, unless limited by maxSessions). Default is 1. - * - shardTestFiles - If this is set to be true, specs will be sharded by file - * (i.e. all files to be run by this set of capabilities will run in parallel). Default is false. - * - maxInstances - Maximum number of browser instances that can run in parallel for this set of capabilities. - * This is only needed if shardTestFiles is true. Default is 1. - * - maxSessions - Maximum number of total browser sessions to run. - * Tests are queued in sequence if number of browser sessions is limited by this parameter. - * Use a number less than 1 to denote unlimited. Default is unlimited. - */ +var TEMP_JSON_REPORT_NAME = path.join(process.cwd(), 'temp.json'); /** - * The taskScheduler keeps track of the spec files that needs to run next - * and which task is running what. + * The taskScheduler keeps track of the spec files that needs to run next and which task is running what. + * Supports only 1 browser in config.multiCapabilities - config.multiCapabilities[0]. Others are ignored. + * TODO: refactor runtime resolver to use capabilities instead of multiCapabilities + * */ function TaskQueue(capabilities, specLists) { // A queue of specs for a particular capacity @@ -25,14 +15,12 @@ function TaskQueue(capabilities, specLists) { this.specLists = specLists; this.numRunningInstances = 0; this.specsIndex = 0; - this.maxInstance = capabilities.maxInstances || 1; } /** * A scheduler to keep track of specs that need running and their associated * capabilities. It will suggest a task (combination of capabilities and spec) - * to run while observing the following config rules: - * multiCapabilities, shardTestFiles, and maxInstance. + * to run while observing the multiCapabilities and global config. * Precondition: multiCapabilities is a non-empty array * (capabilities and getCapabilities will both be ignored) * @@ -47,81 +35,53 @@ function TaskScheduler(config) { return excludes.indexOf(path) < 0; }); - var taskQueues = []; - config.multiCapabilities.forEach((capabilities) => { - var capabilitiesSpecs = allSpecs; - if (capabilities.specs) { - var capabilitiesSpecificSpecs = ConfigParser.resolveFilePatterns(capabilities.specs, false, config.configDir); - capabilitiesSpecs = capabilitiesSpecs.concat(capabilitiesSpecificSpecs); - } - if (capabilities.exclude) { - var capabilitiesSpecExcludes = ConfigParser.resolveFilePatterns(capabilities.exclude, true, config.configDir); - capabilitiesSpecs = capabilitiesSpecs.filter((path) => { - return capabilitiesSpecExcludes.indexOf(path) < 0; - }); - } + var capabilities = config.multiCapabilities[0]; // ignore other browsers - if (capabilities.restartBrowserBetweenSpecFiles) { - // run every spec file in a separate browser - consequetively - capabilities.shardTestFiles = true; - capabilities.maxInstances = 1; - } - if (capabilities.runSpecFilesInParallelBrowsers) { - // run every spec file in a separate browser - parallely - capabilities.shardTestFiles = true; - // run the maximum number of browsers in parallel - capabilities.maxInstances = allSpecs.length; - } + // Maximum number of browser instances that can run in parallel. Each instance will run a single spec file. + // If number of tests > maxInstances, new instances will be created when another one completes. Default is 1. + config.maxInstances = config.maxInstances || 1; - var specLists = []; - // If we shard, we return an array of one element arrays, each containing - // the spec file. If we don't shard, we return an one element array - // containing an array of all the spec files - if (capabilities.shardTestFiles) { - capabilitiesSpecs.forEach((spec) => { - specLists.push([spec]); - }); - } else { - specLists.push(capabilitiesSpecs); - } + var runOneSpecFilePerBrowserInstance = config.restartBrowserBetweenSpecs || config.maxInstances > 1; - capabilities.count = capabilities.count || 1; - for (var i = 0; i < capabilities.count; ++i) { - taskQueues.push(new TaskQueue(capabilities, specLists)); - } - }); - this.taskQueues = taskQueues; - this.rotationIndex = 0; // Helps suggestions to rotate amongst capabilities + var specLists = []; + // when running multiple instances, run one spec file per instance + // If we shard, we return an array of one element arrays, each containing the spec file. + // If we don't shard, we return an one element array containing an array of all the spec files + if (runOneSpecFilePerBrowserInstance) { + config.tempJsonReport = TEMP_JSON_REPORT_NAME; + allSpecs.forEach((spec) => { + specLists.push([spec]); + }); + } else { + specLists.push(allSpecs); + } + + this.taskQueue = new TaskQueue(capabilities, specLists); } /** - * Get the next task that is allowed to run without going over maxInstance. + * Get the next task that is allowed to run without going over maxInstances * * @return {{capabilities: Object, specs: Array., taskId: string, * done: function()}} */ TaskScheduler.prototype.nextTask = function () { - for (var i = 0; i < this.taskQueues.length; ++i) { - var rotatedIndex = ((i + this.rotationIndex) % this.taskQueues.length); - var queue = this.taskQueues[rotatedIndex]; - if (queue.numRunningInstances < queue.maxInstance && queue.specsIndex < queue.specLists.length) { - this.rotationIndex = rotatedIndex + 1; - ++queue.numRunningInstances; - var taskId = '' + rotatedIndex + 1; - if (queue.specLists.length > 1) { - taskId += '-' + queue.specsIndex; - } - var specs = queue.specLists[queue.specsIndex]; - ++queue.specsIndex; - return { - capabilities: queue.capabilities, - specs: specs, - taskId: taskId, - done: function () { - --queue.numRunningInstances; - } - }; + if (this.taskQueue.numRunningInstances < this.config.maxInstances && this.taskQueue.specsIndex < this.taskQueue.specLists.length) { + ++this.taskQueue.numRunningInstances; + var taskId = '1'; + if (this.taskQueue.specLists.length > 1) { + taskId += '-' + this.taskQueue.specsIndex; } + var specs = this.taskQueue.specLists[this.taskQueue.specsIndex]; + ++this.taskQueue.specsIndex; + return { + capabilities: this.taskQueue.capabilities, + specs: specs, + taskId: taskId, + done: function () { + --this.taskQueue.numRunningInstances; + }.bind(this) + }; } return null; }; @@ -132,29 +92,7 @@ TaskScheduler.prototype.nextTask = function () { * @return {number} */ TaskScheduler.prototype.numTasksOutstanding = function () { - var count = 0; - this.taskQueues.forEach((queue) => { - count += queue.numRunningInstances + (queue.specLists.length - queue.specsIndex); - }); - return count; -}; - -/** - * Get maximum number of concurrent tasks required/permitted. - * - * @return {number} - */ -TaskScheduler.prototype.maxConcurrentTasks = function () { - if (this.config.maxSessions && this.config.maxSessions > 0) { - return this.config.maxSessions; - } - else { - var count = 0; - this.taskQueues.forEach((queue) => { - count += Math.min(queue.maxInstance, queue.specLists.length); - }); - return count; - } + return this.taskQueue.numRunningInstances + (this.taskQueue.specLists.length - this.taskQueue.specsIndex); }; /** @@ -163,11 +101,7 @@ TaskScheduler.prototype.maxConcurrentTasks = function () { * @return {number} */ TaskScheduler.prototype.countActiveTasks = function () { - var count = 0; - this.taskQueues.forEach((queue) => { - count += queue.numRunningInstances; - }); - return count; + return this.taskQueue.numRunningInstances; }; module.exports = { diff --git a/src/statisticCollector.js b/src/statisticCollector.js index aba6fbec..7b88851c 100644 --- a/src/statisticCollector.js +++ b/src/statisticCollector.js @@ -1,4 +1,4 @@ - +var fs = require('fs'); /** * @typedef Overview * @type {Object} @@ -102,7 +102,7 @@ function StatisticCollector(){ } StatisticCollector.prototype.jasmineStarted = function(){ - this.overview.statistic.duration = new Date(); // save start time in duration during the run + this.startTime = new Date(); // save start time in duration during the run }; StatisticCollector.prototype.suiteStarted = function(jasmineSuite){ @@ -292,9 +292,15 @@ StatisticCollector.prototype.suiteDone = function(jasmineSuite, suiteMeta){ this.overview.suites.push(this.currentSuite); }; -StatisticCollector.prototype.jasmineDone = function(runMeta){ +StatisticCollector.prototype.jasmineDone = function (runMeta) { + // TODO another way to save progress would be to return the statistics as part of one runner's result, save them in launcher and pass them as args to the next (+ guard against simultaneous write) + if (runMeta && runMeta.tempJsonReport && fs.existsSync(runMeta.tempJsonReport)) { + var intermediateResults = require(runMeta.tempJsonReport); + this.overview.statistic.duration = intermediateResults.statistic.duration; + this.overview.suites = intermediateResults.suites.concat(this.overview.suites); + } // compute duration - this.overview.statistic.duration = new Date() - this.overview.statistic.duration; + this.overview.statistic.duration += new Date() - this.startTime; // compute statistic var failedSuitesCount = 0; @@ -326,9 +332,7 @@ StatisticCollector.prototype.jasmineDone = function(runMeta){ this.overview.status = 'passed'; } - if (runMeta) { - this.overview.meta = runMeta; - } + this.overview.meta = runMeta; // prepare suites statistic this.overview.statistic.suites = { @@ -458,11 +462,16 @@ StatisticCollector.prototype.getCurrentSuite = function(){ return this.currentSuite; }; -StatisticCollector.prototype.reset = function () { +/** + * reset overview to default + */ +StatisticCollector.prototype.reset = function(){ // @type Overview this.overview = { suites: [], - statistic: {} + statistic: { + duration: 0 + } }; }; diff --git a/src/testPreparer.js b/src/testPreparer.js index bd91f14c..b8e4e14d 100644 --- a/src/testPreparer.js +++ b/src/testPreparer.js @@ -37,7 +37,7 @@ module.exports = function (config) { // initialize statistic collector var statisticCollector = require('./statisticCollector'); - require('./coreReporters/statisticReporter').register(); + require('./coreReporters/statisticReporter')(config).register(); require('./coreReporters/paramsReporter')(config).register(); require('./coreReporters/specLifecycleReporter')(config, storageProvider).register();