UNPKG

siesta-lite

Version:

Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers

646 lines (474 loc) 25.3 kB
/* Siesta 5.6.1 Copyright(c) 2009-2022 Bryntum AB https://bryntum.com/contact https://bryntum.com/products/siesta/license */ Class('Siesta.Launcher.Dispatcher', { does : [ Siesta.Launcher.Dispatcher.TaskQueue, Siesta.Launcher.Dispatcher.Reporter, Siesta.Launcher.Role.CanPrintWithLauncher, Siesta.Launcher.Role.CanLaunchSimulatorServer, Siesta.Launcher.Role.CanLaunchInstrumentationProxy, Siesta.Util.Role.CanGetType ], has : { launcher : { required : true }, // an array of runners created by the launcher for this dispatcher // idea is that some of them can fail on the "setup" step, so those who successeded // will be in the "runners" attribute givenRunners : { required : true }, // code was originally written for multiple runners, then it turned out, its never used // (always only one runner), but we still keep it this way, in case we go crazy and will // support several runners, like BS and SL in the same time runners : Joose.I.Array, options : { is : 'rw', init : Joose.I.Object }, projectUrl : { required : true }, testSuiteName : null, // is used for JUnit report (only) hostName : null, startDate : null, endDate : null, allDoneCallback : null, screenshotCompareConfig : null, reRunFailed : false, projectConfig : null, streamAssertions : false, printState : Joose.I.Object, breakOnFail : false, forceExit : false, wsServer : null, hasNativeSimulation : false }, methods : { addRunner : function (runner) { this.runners.push(runner) runner.dispatcher = this runner.maxWorkers = this.options[ 'max-workers' ] || runner.maxWorkers // the `page-size` name is deprecated runner.pagesPerChunk = this.options[ 'chunk-size' ] || this.options[ 'page-size' ] || runner.pagesPerChunk }, // promised method finalizeCommand : function (command, page, result) { var me = this return page.executeSmallScriptPromised( "return Siesta.my.activeHarness.onCommandDone(" + command.id + ", " + JSON.stringify(result) + ")" ).then(function (e) { if (e) me.printError("ERROR DURING COMMAND FINALIZATION: " + e) }, function (e) { me.printError("ERROR DURING COMMAND FINALIZATION: " + e) }) }, // promised method // all commands supposed to resolve to object with { success : true/false }, { error : String } (if failed) // plus some other properties executeCommandFromPage : function (command, page) { var me = this me.debug("Received command: " + JSON.stringify(command)) if (!page[ command.name ]) { return me.finalizeCommand({ success : false, error : 'unknown command' }) } return page[ command.name ](command).then(function (res) { return me.finalizeCommand(command, page, res) }, function (e) { me.printError(JSON.stringify(e)) return me.finalizeCommand(command, page, { success : false, error : e + '' }) }) }, canPrintUpdateImmediately : function (update) { return !this.reRunFailed || this.options.verbose || update.type == 'Siesta.Result.Diagnostic' || update.type == 'Siesta.Result.Assertion' && (update.passed || update.isTodo) }, // promised method consumeTestStateUpdate : function (state, page) { var me = this if (me.forceExit) return 'force_exit' var failed = false if (this.streamAssertions) { var eventLog = state.eventLog eventLog && Joose.A.each(eventLog, function (event) { if (event.isLog) { if (me.typeOf(event.data) == 'String') me.print(event.data) else me.warn(event.data.text) } else if (event.isUpdate) { var update = me.consumeStreamedUpdate(event.data, page) if (update.type == 'Siesta.Result.Assertion' && !update.passed && !update.isTodo) failed = true // top level Siesta.Result.SubTest instance if (update.type == 'Siesta.Result.SubTest' && !update.parentId) me.printTestHeader(update) if (me.canPrintUpdateImmediately(update)) me.printStreamedUpdate(update, me.printState) } else if (event.isResult) { var el = me.consumeTestResult(event.data, page) if (el.result.ERROR || !el.result.passed && !el.result.isTodo) failed = true if (!me.reRunFailed || el.canPrintResultImmediately()) me.printTestResult(el, false, true) } }) } else { var log = state.log log && Joose.A.each(log, function (logElement) { if (me.typeOf(logElement) == 'String') me.print(logElement) else me.warn(logElement.text) }) var testResults = state.testResults testResults && Joose.A.each(testResults, function (testResult) { var el = me.consumeTestResult(testResult, page) if (el.result.ERROR || !el.result.passed && !el.result.isTodo) failed = true // do not print failed tests in case of "reRunFailed" is enabled if (!me.reRunFailed || el.canPrintResultImmediately()) me.printTestResult(el) }) } if (me.options.coverage) { var nyc = this.getNyc() state.coverageResult && state.coverageResult.forEach(function (coverage) { me.nycWriteCoverageFile(nyc, coverage) }) } var commands = state.commands var promise = Promise.resolve() // a command has been send from the test page commands && Joose.A.each(commands, function (command) { promise = promise.then(function () { return me.executeCommandFromPage(command, page) }) }) return promise.then(function () { if (failed && me.breakOnFail) { me.forceExit = true return 'force_exit' } // indicates the last state available if (state.exitStatus != null) { if (state.exitStatus == 'focus_lost') { return 'focus_lost' } return 'all_done' } }) }, launchPage : function (page, chunkTask, needToOpenHarness, runner) { var me = this if (needToOpenHarness) page.openHarness(me.projectUrl, function (error) { if (error) { me.releaseChunkTask(chunkTask) page.close().then(function () { me.launchRunner(runner) }) } else runChunk() }) else runChunk() function runChunk() { page.runChunk(chunkTask, function (e, notLaunchedById) { if (me.forceExit) { if (me.allDoneCallback) { me.allDoneCallback({ exitCode : 1, lastPage : page }) me.allDoneCallback = null } return } me.releaseChunkTask(chunkTask, notLaunchedById) if (me.allDone()) { var reallyAllDone = me.onAllTestsProcessed(page) // note, that the last page is kept opened for coverage report generation if (reallyAllDone) { return } else { me.projectConfig.isRerunningFailedTests = true } } // ideally we need to properly close the page, before starting a new one (to conform to max-workers limitation) // however, in practice "page.close()" can just hang, so in such cases we just call "page.close()" // and continue if (e && page.pageShouldNotBeUsedAfterException(e)) { me.launchRunner(runner) page.close() } else page.close().then(function () { me.launchRunner(runner) }) }) } }, onAllTestsProcessed : function (lastPage) { var me = this // reset the failed tests statuses if (me.reRunFailed) { var res = me.reviseFailedTests() me.projectConfig.failedTestsCount = res.failedCount me.projectConfig.totalTestsCount = res.totalCount } if (me.allDone()) { if (me.reRunFailed) { me.forEachTestElement(function (el) { if (!el.resultPrinted) me.printTestResult(el, true) }) } me.allDoneCallback({ exitCode : me.allPassed() ? 0 : 1, lastPage : lastPage }) return true } return false }, launchRunner : function (runner, veryFirstPage) { var me = this if (me.launcher.shutDownStarted || runner.isPageCreationPaused) return me.debug("Launch runner, pagesCount: " + runner.pageCount + ", max : " + runner.maxWorkers + ", pages left: " + runner.getPageIdList()) while (veryFirstPage || runner.canCreatePage()) { var chunkTask = this.getChunkTask(runner.pagesPerChunk) if (!chunkTask) break if (veryFirstPage) { this.launchPage(veryFirstPage, chunkTask, false, runner) veryFirstPage = null } else { var pageRequestAccepted = runner.requestPage(function (page, chunkTask) { if (page) me.launchPage(page, chunkTask, true, runner) else { me.releaseChunkTask(chunkTask, true) if (runner.pageCreationFailuresCount >= 3) { me.printError("Page creation failed after retry, test suite can not continue execution") me.launcher.gracefulShutdown() return } else { me.printError("Page creation has failed, retry in 5s") runner.pausePageCreation(5000, function () { me.launchRunner(runner) }) } } }, chunkTask) if (!pageRequestAccepted) { me.releaseChunkTask(chunkTask, true) break } } } }, launch : function (firstRunner, veryFirstPage) { var me = this Joose.A.each(this.runners, function (runner) { if (runner == firstRunner) me.launchRunner(runner, veryFirstPage) else me.launchRunner(runner) }) }, getTotalNumberOfPagesIncludingReserved : function () { var count = 0 Joose.A.each(this.runners, function (runner) { count += runner.pageCount + runner.reservedPageCount }) return count }, forEachAssertion : function (testInfo, func, scope, options, parentTests) { options = options || {} scope = scope || this parentTests = parentTests || [] parentTests.push(testInfo) var ignoreTodoAssertions = options.ignoreTodoAssertions var includeDiagnostic = options.includeDiagnostic for (var i = 0; i < testInfo.assertions.length; i++) { var assertion = testInfo.assertions[ i ] if (assertion.type == 'Siesta.Result.Assertion' && (!assertion.isTodo || !ignoreTodoAssertions)) if (func.call(scope, assertion, parentTests) === false) return false if (assertion.type == 'Siesta.Result.Diagnostic' && includeDiagnostic) if (func.call(scope, assertion, parentTests) === false) return false if (assertion.type == 'Siesta.Result.SubTest') if (this.forEachAssertion(assertion, func, scope, options, parentTests.slice()) === false) return false } }, start : function () { this.startDate = new Date() var me = this var options = me.options var upstreamProxyConfig = options[ 'proxy-host' ] ? { host : options[ 'proxy-host' ], port : options[ 'proxy-port' ] } : null // for BS we don't need to use upstream proxy, as it is already passed to BS tunnel as --proxy-host --proxy-port // BS tunnel uses --local-proxy-host, --local-proxy-port for browser traffic if (options.coverage && options.bs) upstreamProxyConfig = null // setup the instrumentation proxy as the 1st thing, since its port is needed to Puppeteer runner for example var cont = options.coverage ? me.setupInstrumentationProxy(upstreamProxyConfig, options.coverage) : Promise.resolve() return cont.then(function () { var promises = [] Joose.A.each(me.givenRunners, function (runner) { runner.dispatcher = me promises.push(runner.setup()) }) return Promise.all(promises) }).then(function (results) { Joose.A.each(results, function (result) { if (result instanceof Error) me.warn("Error setting up the runner: " + result) else me.addRunner(result) }) }).then(function () { if (me.runners.length == 0) { me.printError("No runners available") return { exitCode : 3 } } else { me.debug("Dispatcher setup starting") return me.setup(me.runners[ 0 ]).then(function (setupRes) { me.debug("Dispatcher setup completed, launching the suite") var config = setupRes.config var firstRunner = setupRes.firstRunner var firstPage = setupRes.firstPage return new Promise(function (resolve, reject) { me.allDoneCallback = resolve me.onTestSuiteStart(config.userAgent, config.platform) me.launch(firstRunner, firstPage) }) }, function (e) { me.printError("Setup failed: " + e) return { exitCode : e.errCode || 3 } }) } }).then(function (res) { var exitCode = res.exitCode var lastPage = res.lastPage me.debug("Recevied results for all tests in the suite, proceeding to finalization") me.endDate = new Date() if (exitCode == 0 || exitCode == 1) { me.onTestSuiteEnd() return me.processReports(lastPage).then(function () { me.debug("Reports processed (if any)") if (lastPage) return lastPage.close().then(function () { return exitCode }) else return exitCode }) } else return exitCode }) }, processReports : async function (page) { var me = this me.debug("Generating reports (if any)") var options = this.options var launcher = this.launcher var reportFile = options.reportFile var reportFormat = options.reportFormat if (reportFile) { Joose.A.each(reportFile, function (value, index) { launcher.saveReport( reportFormat[ index ], reportFile[ index ], me.generateReport({ format : reportFormat[ index ] }) ) }) } if (options.coverage && this.launcher.manuallyProcessCoverageResults) { await this.nycReport(this.getNyc(), (path) => { const fs = require('fs') if (fs.existsSync(path)) // this is for the pre-instrumented codebase return fs.readFileSync(path, 'utf8') else return this.instrumentedSources[ path ] || 'No sources available for ' + path }) } return Promise.resolve() }, randomizeArray : function (array) { var randomArray = new Array(array.length) array.forEach(function (el, index) { randomArray[ index ] = { index : index, random : Math.random() } }) randomArray.sort(function (a, b) { return a.random - b.random }) var result = [] randomArray.forEach(function (el) { result.push(array[ el.index ]) }) return result }, setup : function (firstRunner) { var me = this var mySiestaVersion = me.launcher.getSiestaVersion() var options = me.options return new Promise(function (RESOLVE, REJECT) { firstRunner.requestPage(function (page, arg, e) { if (page) { page.openHarness(me.projectUrl, function (err) { if (err) { page.close().then(function () { var e = new Error("Error while opening project page: " + err) e.errCode = 5 REJECT(e) }) } else { page.getConfigInfo(options[ 'include' ], options[ 'exclude' ], options[ 'filter' ], function (e, config) { if (e) { page.close().then(function () { REJECT(new Error("Error getting the test suite information: " + e)) }) return } if (config.VERSION && mySiestaVersion && config.VERSION != mySiestaVersion) { me.printError("Siesta version on project page [" + config.VERSION + "] does not match Siesta version of the launcher [" + mySiestaVersion + "]") page.close().then(function () { REJECT(new Error("Siesta versions mismatch")) }) return } me.breakOnFail = config.breakOnFail if (me.breakOnFail) me.reRunFailed = false me.hostName = config.hostName me.testSuiteName = config.title me.screenshotCompareConfig = config.screenshotCompareConfig me.structure = config.structure var desc = config.descriptors if (!desc.sharedContextGroups.length && !desc.mustRunSequential.length) { var style = me.style() me.printError( "Found no tests to run. Check your project file, glob pattern,\n" + style.bold("--include") + ", " + style.bold("--exclude") + " and " + style.bold("--filter") + " command line options.\n" + "For help, launch with " + style.bold("--help") ) page.close().then(function () { var err = new Error("No tests to run") err.errCode = 4 REJECT(err) }) return } var maxAttempts = Number(options[ 'restart-attempts' ]) + 1 if (isNaN(maxAttempts)) maxAttempts = 2 var randomize = Boolean(options[ 'randomize-tests-order' ]) me.sharedContextGroups = Joose.A.map(desc.sharedContextGroups, function (group) { return new Siesta.Launcher.Dispatcher.Group({ elements : randomize ? me.randomizeArray(group.items) : group.items, maxProcessedCount : maxAttempts }) }) me.regularTestsGroup = new Siesta.Launcher.Dispatcher.Group({ elements : randomize ? me.randomizeArray(desc.mustRunSequential) : desc.mustRunSequential, maxProcessedCount : maxAttempts }) // TODO a global flag, saved on dispatcher instance // should instead decide per page me.hasNativeSimulation = config.hasNativeSimulation var cont = config.hasNativeSimulation && me.launcher.sharedNativeSimulator ? me.setupNativeEventsSimulator() : Promise.resolve() cont.then( function () { RESOLVE({ firstRunner : firstRunner, firstPage : page, config : config }) }, function (e) { REJECT('Error while setting up native events simulation websocket server: ' + e) } ) }) } }) // eof page.openHarness } else REJECT(new Error("Can't create first page, runner: " + firstRunner + ", exception: " + e)) }) }) } // eof setup } })