adapter.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. (function(window) {
  2. /*eslint no-unused-vars: ["error", { "varsIgnorePattern": "(createSpecFilter|createStartFn)" }]*/
  3. 'use strict'
  4. /**
  5. * Decision maker for whether a stack entry is considered external to jasmine and karma.
  6. * @param {String} entry Error stack entry.
  7. * @return {Boolean} True if external, False otherwise.
  8. */
  9. function isExternalStackEntry (entry) {
  10. return !!entry &&
  11. // entries related to jasmine and karma-jasmine:
  12. !/\/(jasmine-core|karma-jasmine)\//.test(entry) &&
  13. // karma specifics, e.g. "at http://localhost:7018/karma.js:185"
  14. !/\/(karma.js|context.html):/.test(entry)
  15. }
  16. /**
  17. * Returns relevant stack entries.
  18. * @param {String} stack Complete error stack trace.
  19. * @return {Array} A list of relevant stack entries.
  20. */
  21. function getRelevantStackFrom (stack) {
  22. var filteredStack = []
  23. var relevantStack = []
  24. stack = stack.split('\n')
  25. for (var i = 0; i < stack.length; i += 1) {
  26. if (isExternalStackEntry(stack[i])) {
  27. filteredStack.push(stack[i])
  28. }
  29. }
  30. // If the filtered stack is empty, i.e. the error originated entirely from within jasmine or karma, then the whole stack
  31. // should be relevant.
  32. if (filteredStack.length === 0) {
  33. filteredStack = stack
  34. }
  35. for (i = 0; i < filteredStack.length; i += 1) {
  36. if (filteredStack[i]) {
  37. relevantStack.push(filteredStack[i])
  38. }
  39. }
  40. return relevantStack
  41. }
  42. /**
  43. * Custom formatter for a failed step.
  44. *
  45. * Different browsers report stack trace in different ways. This function
  46. * attempts to provide a concise, relevant error message by removing the
  47. * unnecessary stack traces coming from the testing framework itself as well
  48. * as possible repetition.
  49. *
  50. * @see https://github.com/karma-runner/karma-jasmine/issues/60
  51. * @param {Object} step Step object with stack and message properties.
  52. * @return {String} Formatted step.
  53. */
  54. function formatFailedStep (step) {
  55. // Safari seems to have no stack trace,
  56. // so we just return the error message:
  57. if (!step.stack) { return step.message }
  58. var relevantMessage = []
  59. var relevantStack = []
  60. // Remove the message prior to processing the stack to prevent issues like
  61. // https://github.com/karma-runner/karma-jasmine/issues/79
  62. var stack = step.stack.replace('Error: ' + step.message, '')
  63. var prefix = (stack === step.stack) ? '' : 'Error: '
  64. var dirtyRelevantStack = getRelevantStackFrom(stack)
  65. // PhantomJS returns multiline error message for errors coming from specs
  66. // (for example when calling a non-existing function). This error is present
  67. // in both `step.message` and `step.stack` at the same time, but stack seems
  68. // preferable, so we iterate relevant stack, compare it to message:
  69. for (var i = 0; i < dirtyRelevantStack.length; i += 1) {
  70. if (typeof step.message === 'string' && step.message.indexOf(dirtyRelevantStack[i]) === -1) {
  71. // Stack entry is not in the message,
  72. // we consider it to be a relevant stack:
  73. relevantStack.push(dirtyRelevantStack[i])
  74. } else {
  75. // Stack entry is already in the message,
  76. // we consider it to be a suitable message alternative:
  77. relevantMessage.push(prefix + dirtyRelevantStack[i])
  78. }
  79. }
  80. // In most cases the above will leave us with an empty message...
  81. if (relevantMessage.length === 0) {
  82. // Let's reuse the original message:
  83. relevantMessage.push(prefix + step.message)
  84. // Now we probably have a repetition case where:
  85. // relevantMessage: ["Expected true to be false."]
  86. // relevantStack: ["Error: Expected true to be false.", ...]
  87. if (relevantStack.length && relevantStack[0].indexOf(step.message) !== -1) {
  88. // The message seems preferable, so we remove the first value from
  89. // the stack to get rid of repetition :
  90. relevantStack.shift()
  91. }
  92. }
  93. // Example output:
  94. // --------------------
  95. // Chrome 40.0.2214 (Mac OS X 10.9.5) xxx should return false 1 FAILED
  96. // Expected true to be false
  97. // at /foo/bar/baz.spec.js:22:13
  98. // at /foo/bar/baz.js:18:29
  99. return relevantMessage.concat(relevantStack).join('\n')
  100. }
  101. function SuiteNode (name, parent) {
  102. this.name = name
  103. this.parent = parent
  104. this.children = []
  105. this.addChild = function (name) {
  106. var suite = new SuiteNode(name, this)
  107. this.children.push(suite)
  108. return suite
  109. }
  110. }
  111. function processSuite (suite, pointer) {
  112. var child
  113. var childPointer
  114. for (var i = 0; i < suite.children.length; i++) {
  115. child = suite.children[i]
  116. if (child.children) {
  117. childPointer = pointer[child.description] = {_: []}
  118. processSuite(child, childPointer)
  119. } else {
  120. if (!pointer._) {
  121. pointer._ = []
  122. }
  123. pointer._.push(child.description)
  124. }
  125. }
  126. }
  127. function getAllSpecNames (topSuite) {
  128. var specNames = {}
  129. processSuite(topSuite, specNames)
  130. return specNames
  131. }
  132. /**
  133. * Very simple reporter for Jasmine.
  134. */
  135. function KarmaReporter (tc, jasmineEnv) {
  136. var currentSuite = new SuiteNode()
  137. // Save link on native Date object
  138. // because user can mock it
  139. var _Date = Date
  140. var startTimeCurrentSpec = new _Date().getTime()
  141. function handleGlobalErrors (result) {
  142. if (result.failedExpectations && result.failedExpectations.length) {
  143. var message = 'An error was thrown in afterAll'
  144. var steps = result.failedExpectations
  145. for (var i = 0, l = steps.length; i < l; i++) {
  146. message += '\n' + formatFailedStep(steps[i])
  147. }
  148. tc.error(message)
  149. }
  150. }
  151. /**
  152. * Jasmine 2.0 dispatches the following events:
  153. *
  154. * - jasmineStarted
  155. * - jasmineDone
  156. * - suiteStarted
  157. * - suiteDone
  158. * - specStarted
  159. * - specDone
  160. */
  161. this.jasmineStarted = function (data) {
  162. // TODO(vojta): Do not send spec names when polling.
  163. tc.info({
  164. total: data.totalSpecsDefined,
  165. specs: getAllSpecNames(jasmineEnv.topSuite())
  166. })
  167. }
  168. this.jasmineDone = function (result) {
  169. result = result || {}
  170. // Any errors in top-level afterAll blocks are given here.
  171. handleGlobalErrors(result)
  172. tc.complete({
  173. order: result.order,
  174. coverage: window.__coverage__
  175. })
  176. }
  177. this.suiteStarted = function (result) {
  178. currentSuite = currentSuite.addChild(result.description)
  179. }
  180. this.suiteDone = function (result) {
  181. // In the case of xdescribe, only "suiteDone" is fired.
  182. // We need to skip that.
  183. if (result.description !== currentSuite.name) {
  184. return
  185. }
  186. // Any errors in afterAll blocks are given here, except for top-level
  187. // afterAll blocks.
  188. handleGlobalErrors(result)
  189. currentSuite = currentSuite.parent
  190. }
  191. this.specStarted = function () {
  192. startTimeCurrentSpec = new _Date().getTime()
  193. }
  194. this.specDone = function (specResult) {
  195. var skipped = specResult.status === 'disabled' || specResult.status === 'pending' || specResult.status === 'excluded'
  196. var result = {
  197. fullName: specResult.fullName,
  198. description: specResult.description,
  199. id: specResult.id,
  200. log: [],
  201. skipped: skipped,
  202. disabled: specResult.status === 'disabled' || specResult.status === 'excluded',
  203. pending: specResult.status === 'pending',
  204. success: specResult.failedExpectations.length === 0,
  205. suite: [],
  206. time: skipped ? 0 : new _Date().getTime() - startTimeCurrentSpec,
  207. executedExpectationsCount: specResult.failedExpectations.length + specResult.passedExpectations.length
  208. }
  209. // generate ordered list of (nested) suite names
  210. var suitePointer = currentSuite
  211. while (suitePointer.parent) {
  212. result.suite.unshift(suitePointer.name)
  213. suitePointer = suitePointer.parent
  214. }
  215. if (!result.success) {
  216. var steps = specResult.failedExpectations
  217. for (var i = 0, l = steps.length; i < l; i++) {
  218. result.log.push(formatFailedStep(steps[i]))
  219. }
  220. }
  221. tc.result(result)
  222. delete specResult.startTime
  223. }
  224. }
  225. /**
  226. * Extract grep option from karma config
  227. * @param {[Array|string]} clientArguments The karma client arguments
  228. * @return {string} The value of grep option by default empty string
  229. */
  230. var getGrepOption = function (clientArguments) {
  231. var grepRegex = /^--grep=(.*)$/
  232. if (Object.prototype.toString.call(clientArguments) === '[object Array]') {
  233. var indexOfGrep = indexOf(clientArguments, '--grep')
  234. if (indexOfGrep !== -1) {
  235. return clientArguments[indexOfGrep + 1]
  236. }
  237. return map(filter(clientArguments, function (arg) {
  238. return grepRegex.test(arg)
  239. }), function (arg) {
  240. return arg.replace(grepRegex, '$1')
  241. })[0] || ''
  242. } else if (typeof clientArguments === 'string') {
  243. var match = /--grep=([^=]+)/.exec(clientArguments)
  244. return match ? match[1] : ''
  245. }
  246. }
  247. var createRegExp = function (filter) {
  248. filter = filter || ''
  249. if (filter === '') {
  250. return new RegExp() // to match all
  251. }
  252. var regExp = /^[/](.*)[/]([gmixXsuUAJD]*)$/ // pattern to check whether the string is RegExp pattern
  253. var parts = regExp.exec(filter)
  254. if (parts === null) {
  255. return new RegExp(filter.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')) // escape functional symbols
  256. }
  257. var patternExpression = parts[1]
  258. var patternSwitches = parts[2]
  259. return new RegExp(patternExpression, patternSwitches)
  260. }
  261. /**
  262. * Create jasmine spec filter
  263. * @param {Object} options Spec filter options
  264. */
  265. var KarmaSpecFilter = function (options) {
  266. var filterPattern = createRegExp(options && options.filterString())
  267. this.matches = function (specName) {
  268. return filterPattern.test(specName)
  269. }
  270. }
  271. /**
  272. * Configure jasmine specFilter
  273. *
  274. * This function is invoked from the wrapper.
  275. * @see adapter.wrapper
  276. *
  277. * @param {Object} config The karma config
  278. * @param {Object} jasmineEnv jasmine environment object
  279. */
  280. var createSpecFilter = function (config, jasmineEnv) {
  281. var karmaSpecFilter = new KarmaSpecFilter({
  282. filterString: function () {
  283. return getGrepOption(config.args)
  284. }
  285. })
  286. var specFilter = function (spec) {
  287. return karmaSpecFilter.matches(spec.getFullName())
  288. }
  289. jasmineEnv.configure({ specFilter: specFilter })
  290. }
  291. /**
  292. * Karma starter function factory.
  293. *
  294. * This function is invoked from the wrapper.
  295. * @see adapter.wrapper
  296. *
  297. * @param {Object} karma Karma runner instance.
  298. * @param {Object} [jasmineEnv] Optional Jasmine environment for testing.
  299. * @return {Function} Karma starter function.
  300. */
  301. function createStartFn (karma, jasmineEnv) {
  302. // This function will be assigned to `window.__karma__.start`:
  303. return function () {
  304. var clientConfig = karma.config || {}
  305. var jasmineConfig = clientConfig.jasmine || {}
  306. jasmineEnv = jasmineEnv || window.jasmine.getEnv()
  307. jasmineEnv.configure(jasmineConfig)
  308. window.jasmine.DEFAULT_TIMEOUT_INTERVAL = jasmineConfig.timeoutInterval ||
  309. window.jasmine.DEFAULT_TIMEOUT_INTERVAL
  310. jasmineEnv.addReporter(new KarmaReporter(karma, jasmineEnv))
  311. jasmineEnv.execute()
  312. }
  313. }
  314. function indexOf (collection, find, i /* opt*/) {
  315. if (collection.indexOf) {
  316. return collection.indexOf(find, i)
  317. }
  318. if (i === undefined) { i = 0 }
  319. if (i < 0) { i += collection.length }
  320. if (i < 0) { i = 0 }
  321. for (var n = collection.length; i < n; i++) {
  322. if (i in collection && collection[i] === find) {
  323. return i
  324. }
  325. }
  326. return -1
  327. }
  328. function filter (collection, filter, that /* opt*/) {
  329. if (collection.filter) {
  330. return collection.filter(filter, that)
  331. }
  332. var other = []
  333. var v
  334. for (var i = 0, n = collection.length; i < n; i++) {
  335. if (i in collection && filter.call(that, v = collection[i], i, collection)) {
  336. other.push(v)
  337. }
  338. }
  339. return other
  340. }
  341. function map (collection, mapper, that /* opt*/) {
  342. if (collection.map) {
  343. return collection.map(mapper, that)
  344. }
  345. var other = new Array(collection.length)
  346. for (var i = 0, n = collection.length; i < n; i++) {
  347. if (i in collection) {
  348. other[i] = mapper.call(that, collection[i], i, collection)
  349. }
  350. }
  351. return other
  352. }
  353. createSpecFilter(window.__karma__.config, jasmine.getEnv())
  354. window.__karma__.start = createStartFn(window.__karma__)
  355. })(typeof window !== 'undefined' ? window : global);