(function(window) { /* eslint no-unused-vars: ["error", { "varsIgnorePattern": "(createSpecFilter|createStartFn)" }] */ 'use strict' // Save link to native Date object // before it might be mocked by the user var _Date = Date /** * Decision maker for whether a stack entry is considered external to jasmine and karma. * @param {String} entry Error stack entry. * @return {Boolean} True if external, False otherwise. */ function isExternalStackEntry (entry) { return !!entry && // entries related to jasmine and karma-jasmine: !/\/(jasmine-core|karma-jasmine)\//.test(entry) && // karma specifics, e.g. "at http://localhost:7018/karma.js:185" !/\/(karma.js|context.html):/.test(entry) } /** * Returns relevant stack entries. * @param {Array} stack frames * @return {Array} A list of relevant stack entries. */ function getRelevantStackFrom (stack) { var filteredStack = [] var relevantStack = [] for (var i = 0; i < stack.length; i += 1) { if (isExternalStackEntry(stack[i])) { filteredStack.push(stack[i]) } } // If the filtered stack is empty, i.e. the error originated entirely from within jasmine or karma, then the whole stack // should be relevant. if (filteredStack.length === 0) { filteredStack = stack } for (i = 0; i < filteredStack.length; i += 1) { if (filteredStack[i]) { relevantStack.push(filteredStack[i]) } } return relevantStack } /** * Custom formatter for a failed step. * * Different browsers report stack trace in different ways. This function * attempts to provide a concise, relevant error message by removing the * unnecessary stack traces coming from the testing framework itself as well * as possible repetition. * * @see https://github.com/karma-runner/karma-jasmine/issues/60 * @param {Object} step Step object with stack and message properties. * @return {String} Formatted step. */ function formatFailedStep (step) { var relevantMessage = [] var relevantStack = [] // Safari/Firefox seems to have no stack trace, // so we just return the error message and if available // construct a stacktrace out of filename and lineno: if (!step.stack) { if (step.filename) { var stackframe = step.filename if (step.lineno) { stackframe = stackframe + ':' + step.lineno } relevantStack.push(stackframe) } relevantMessage.push(step.message) return relevantMessage.concat(relevantStack).join('\n') } // Remove the message prior to processing the stack to prevent issues like // https://github.com/karma-runner/karma-jasmine/issues/79 var stackframes = step.stack.split('\n') var messageOnStack = null if (stackframes[0].indexOf(step.message) !== -1) { // Remove the message if it is in the stack string (eg Chrome) messageOnStack = stackframes.shift() } // Filter frames var relevantStackFrames = getRelevantStackFrom(stackframes) if (messageOnStack) { // Put the message back if we removed it. relevantStackFrames.unshift(messageOnStack) } else { // The stack did not have the step.message so add it. relevantStackFrames.unshift(step.message) } return relevantStackFrames.join('\n') } function debugUrl (description) { // A link to re-run just one failed test case. return window.location.origin + '/debug.html?spec=' + encodeURIComponent(description) } function SuiteNode (name, parent) { this.name = name this.parent = parent this.children = [] this.addChild = function (name) { var suite = new SuiteNode(name, this) this.children.push(suite) return suite } } function processSuite (suite, pointer) { var child var childPointer for (var i = 0; i < suite.children.length; i++) { child = suite.children[i] if (child.children) { childPointer = pointer[child.description] = { _: [] } processSuite(child, childPointer) } else { if (!pointer._) { pointer._ = [] } pointer._.push(child.description) } } } function getAllSpecNames (topSuite) { var specNames = {} processSuite(topSuite, specNames) return specNames } /** * Very simple reporter for Jasmine. */ function KarmaReporter (tc, jasmineEnv) { var currentSuite = new SuiteNode() var startTimeCurrentSpec = new _Date().getTime() function handleGlobalErrors (result) { if (result.failedExpectations && result.failedExpectations.length) { var message = 'An error was thrown in afterAll' var steps = result.failedExpectations for (var i = 0, l = steps.length; i < l; i++) { message += '\n' + formatFailedStep(steps[i]) } tc.error(message) } } /** * Jasmine 2.0 dispatches the following events: * * - jasmineStarted * - jasmineDone * - suiteStarted * - suiteDone * - specStarted * - specDone */ this.jasmineStarted = function (data) { // TODO(vojta): Do not send spec names when polling. tc.info({ event: 'jasmineStarted', total: data.totalSpecsDefined, specs: getAllSpecNames(jasmineEnv.topSuite()) }) } this.jasmineDone = function (result) { result = result || {} // Any errors in top-level afterAll blocks are given here. handleGlobalErrors(result) // Remove functions from called back results to avoid IPC errors in Electron // https://github.com/twolfson/karma-electron/issues/47 var cleanedOrder if (result.order) { cleanedOrder = {} var orderKeys = Object.getOwnPropertyNames(result.order) for (var i = 0; i < orderKeys.length; i++) { var orderKey = orderKeys[i] if (typeof result.order[orderKey] !== 'function') { cleanedOrder[orderKey] = result.order[orderKey] } } } tc.complete({ order: cleanedOrder, coverage: window.__coverage__ }) } this.suiteStarted = function (result) { currentSuite = currentSuite.addChild(result.description) tc.info({ event: 'suiteStarted', result: result }) } this.suiteDone = function (result) { // In the case of xdescribe, only "suiteDone" is fired. // We need to skip that. if (result.description !== currentSuite.name) { return } // Any errors in afterAll blocks are given here, except for top-level // afterAll blocks. handleGlobalErrors(result) currentSuite = currentSuite.parent tc.info({ event: 'suiteDone', result: result }) } this.specStarted = function () { startTimeCurrentSpec = new _Date().getTime() } this.specDone = function (specResult) { var skipped = specResult.status === 'disabled' || specResult.status === 'pending' || specResult.status === 'excluded' var result = { fullName: specResult.fullName, description: specResult.description, id: specResult.id, log: [], skipped: skipped, disabled: specResult.status === 'disabled' || specResult.status === 'excluded', pending: specResult.status === 'pending', success: specResult.failedExpectations.length === 0, suite: [], time: skipped ? 0 : new _Date().getTime() - startTimeCurrentSpec, executedExpectationsCount: specResult.failedExpectations.length + specResult.passedExpectations.length, passedExpectations: specResult.passedExpectations, properties: specResult.properties } // generate ordered list of (nested) suite names var suitePointer = currentSuite while (suitePointer.parent) { result.suite.unshift(suitePointer.name) suitePointer = suitePointer.parent } if (!result.success) { var steps = specResult.failedExpectations for (var i = 0, l = steps.length; i < l; i++) { result.log.push(formatFailedStep(steps[i])) } if (typeof window !== 'undefined' && window.location && window.location.origin) { // Report the name of fhe failing spec so the reporter can emit a debug url. result.debug_url = debugUrl(specResult.fullName) } } // When failSpecWithNoExpectations is true, Jasmine will report specs without expectations as failed if (result.executedExpectationsCount === 0 && specResult.status === 'failed') { result.success = false result.log.push('Spec has no expectations') } tc.result(result) delete specResult.startTime } } /** * Extract grep option from karma config * @param {[Array|string]} clientArguments The karma client arguments * @return {string} The value of grep option by default empty string */ var getGrepOption = function (clientArguments) { var grepRegex = /^--grep=(.*)$/ if (Object.prototype.toString.call(clientArguments) === '[object Array]') { var indexOfGrep = indexOf(clientArguments, '--grep') if (indexOfGrep !== -1) { return clientArguments[indexOfGrep + 1] } return map(filter(clientArguments, function (arg) { return grepRegex.test(arg) }), function (arg) { return arg.replace(grepRegex, '$1') })[0] || '' } else if (typeof clientArguments === 'string') { var match = /--grep=([^=]+)/.exec(clientArguments) return match ? match[1] : '' } } var createRegExp = function (filter) { filter = filter || '' if (filter === '') { return new RegExp() // to match all } var regExp = /^[/](.*)[/]([gmixXsuUAJD]*)$/ // pattern to check whether the string is RegExp pattern var parts = regExp.exec(filter) if (parts === null) { return new RegExp(filter.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')) // escape functional symbols } var patternExpression = parts[1] var patternSwitches = parts[2] return new RegExp(patternExpression, patternSwitches) } function getGrepSpecsToRun (clientConfig, specs) { var grepOption = getGrepOption(clientConfig.args) if (grepOption) { var regExp = createRegExp(grepOption) return filter(specs, function specFilter (spec) { return regExp.test(spec.getFullName()) }) } } function parseQueryParams (location) { var params = {} if (location && Object.prototype.hasOwnProperty.call(location, 'search')) { var pairs = location.search.substr(1).split('&') for (var i = 0; i < pairs.length; i++) { var keyValue = pairs[i].split('=') params[decodeURIComponent(keyValue[0])] = decodeURIComponent(keyValue[1]) } } return params } function getId (s) { return s.id } function getSpecsByName (specs, name) { specs = specs.filter(function (s) { return s.name.indexOf(name) !== -1 }) if (specs.length === 0) { throw new Error('No spec found with name: "' + name + '"') } return specs } function getDebugSpecToRun (location, specs) { var queryParams = parseQueryParams(location) var spec = queryParams.spec if (spec) { // A single spec has been requested by name for debugging. return getSpecsByName(specs, spec) } } function getSpecsToRunForCurrentShard (specs, shardIndex, totalShards) { if (specs.length < totalShards) { throw new Error( 'More shards (' + totalShards + ') than test specs (' + specs.length + ')') } // Just do a simple sharding strategy of dividing the number of specs // equally. var firstSpec = Math.floor(specs.length * shardIndex / totalShards) var lastSpec = Math.floor(specs.length * (shardIndex + 1) / totalShards) return specs.slice(firstSpec, lastSpec) } function getShardedSpecsToRun (specs, clientConfig) { var shardIndex = clientConfig.shardIndex var totalShards = clientConfig.totalShards if (shardIndex != null && totalShards != null) { // Sharded mode - Run only the subset of the specs corresponding to the // current shard. return getSpecsToRunForCurrentShard( specs, Number(shardIndex), Number(totalShards)) } } /** * Create jasmine spec filter * @param {Object} clientConfig karma config * @param {!Object} jasmineEnv */ var KarmaSpecFilter = function (clientConfig, jasmineEnv) { /** * Walk the test suite tree depth first and collect all test specs * @param {!Object} jasmineEnv * @return {!Array} All possible tests. */ function getAllSpecs (jasmineEnv) { var specs = [] var stack = [jasmineEnv.topSuite()] var currentNode while ((currentNode = stack.pop())) { if (currentNode.children) { // jasmine.Suite stack = stack.concat(currentNode.children) } else if (currentNode.id) { // jasmine.Spec specs.unshift(currentNode) } } return specs } /** * Filter the specs with URL search params and config. * @param {!Object} location property 'search' from URL. * @param {!Object} clientConfig karma client config * @param {!Object} jasmineEnv * @return {!Array} */ function getSpecsToRun (location, clientConfig, jasmineEnv) { var specs = getAllSpecs(jasmineEnv).map(function (spec) { spec.name = spec.getFullName() return spec }) if (!specs || !specs.length) { return [] } return getGrepSpecsToRun(clientConfig, specs) || getDebugSpecToRun(location, specs) || getShardedSpecsToRun(specs, clientConfig) || specs } this.specIdsToRun = getSpecsToRun(window.location, clientConfig, jasmineEnv).map(getId) this.matches = function (spec) { return this.specIdsToRun.indexOf(spec.id) !== -1 } } /** * Configure jasmine specFilter * * This function is invoked from the wrapper. * @see adapter.wrapper * * @param {Object} config The karma config * @param {Object} jasmineEnv jasmine environment object */ var createSpecFilter = function (config, jasmineEnv) { var karmaSpecFilter = new KarmaSpecFilter(config, jasmineEnv) var specFilter = function (spec) { return karmaSpecFilter.matches(spec) } return specFilter } /** * Karma starter function factory. * * This function is invoked from the wrapper. * @see adapter.wrapper * * @param {Object} karma Karma runner instance. * @param {Object} [jasmineEnv] Optional Jasmine environment for testing. * @return {Function} Karma starter function. */ function createStartFn (karma, jasmineEnv) { // This function will be assigned to `window.__karma__.start`: return function () { var clientConfig = karma.config || {} var jasmineConfig = clientConfig.jasmine || {} jasmineEnv = jasmineEnv || window.jasmine.getEnv() jasmineConfig.specFilter = createSpecFilter(clientConfig, jasmineEnv) jasmineEnv.configure(jasmineConfig) window.jasmine.DEFAULT_TIMEOUT_INTERVAL = jasmineConfig.timeoutInterval || window.jasmine.DEFAULT_TIMEOUT_INTERVAL jasmineEnv.addReporter(new KarmaReporter(karma, jasmineEnv)) jasmineEnv.execute() } } function indexOf (collection, find, i /* opt */) { if (collection.indexOf) { return collection.indexOf(find, i) } if (i === undefined) { i = 0 } if (i < 0) { i += collection.length } if (i < 0) { i = 0 } for (var n = collection.length; i < n; i++) { if (i in collection && collection[i] === find) { return i } } return -1 } function filter (collection, filter, that /* opt */) { if (collection.filter) { return collection.filter(filter, that) } var other = [] var v for (var i = 0, n = collection.length; i < n; i++) { if (i in collection && filter.call(that, v = collection[i], i, collection)) { other.push(v) } } return other } function map (collection, mapper, that /* opt */) { if (collection.map) { return collection.map(mapper, that) } var other = new Array(collection.length) for (var i = 0, n = collection.length; i < n; i++) { if (i in collection) { other[i] = mapper.call(that, collection[i], i, collection) } } return other } window.__karma__.start = createStartFn(window.__karma__) })(typeof window !== 'undefined' ? window : global);