adapter.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. (function(window) {
  2. /* eslint no-unused-vars: ["error", { "varsIgnorePattern": "(createSpecFilter|createStartFn)" }] */
  3. 'use strict'
  4. // Save link to native Date object
  5. // before it might be mocked by the user
  6. var _Date = Date
  7. /**
  8. * Decision maker for whether a stack entry is considered external to jasmine and karma.
  9. * @param {String} entry Error stack entry.
  10. * @return {Boolean} True if external, False otherwise.
  11. */
  12. function isExternalStackEntry (entry) {
  13. return !!entry &&
  14. // entries related to jasmine and karma-jasmine:
  15. !/\/(jasmine-core|karma-jasmine)\//.test(entry) &&
  16. // karma specifics, e.g. "at http://localhost:7018/karma.js:185"
  17. !/\/(karma.js|context.html):/.test(entry)
  18. }
  19. /**
  20. * Returns relevant stack entries.
  21. * @param {Array} stack frames
  22. * @return {Array} A list of relevant stack entries.
  23. */
  24. function getRelevantStackFrom (stack) {
  25. var filteredStack = []
  26. var relevantStack = []
  27. for (var i = 0; i < stack.length; i += 1) {
  28. if (isExternalStackEntry(stack[i])) {
  29. filteredStack.push(stack[i])
  30. }
  31. }
  32. // If the filtered stack is empty, i.e. the error originated entirely from within jasmine or karma, then the whole stack
  33. // should be relevant.
  34. if (filteredStack.length === 0) {
  35. filteredStack = stack
  36. }
  37. for (i = 0; i < filteredStack.length; i += 1) {
  38. if (filteredStack[i]) {
  39. relevantStack.push(filteredStack[i])
  40. }
  41. }
  42. return relevantStack
  43. }
  44. /**
  45. * Custom formatter for a failed step.
  46. *
  47. * Different browsers report stack trace in different ways. This function
  48. * attempts to provide a concise, relevant error message by removing the
  49. * unnecessary stack traces coming from the testing framework itself as well
  50. * as possible repetition.
  51. *
  52. * @see https://github.com/karma-runner/karma-jasmine/issues/60
  53. * @param {Object} step Step object with stack and message properties.
  54. * @return {String} Formatted step.
  55. */
  56. function formatFailedStep (step) {
  57. var relevantMessage = []
  58. var relevantStack = []
  59. // Safari/Firefox seems to have no stack trace,
  60. // so we just return the error message and if available
  61. // construct a stacktrace out of filename and lineno:
  62. if (!step.stack) {
  63. if (step.filename) {
  64. var stackframe = step.filename
  65. if (step.lineno) {
  66. stackframe = stackframe + ':' + step.lineno
  67. }
  68. relevantStack.push(stackframe)
  69. }
  70. relevantMessage.push(step.message)
  71. return relevantMessage.concat(relevantStack).join('\n')
  72. }
  73. // Remove the message prior to processing the stack to prevent issues like
  74. // https://github.com/karma-runner/karma-jasmine/issues/79
  75. var stackframes = step.stack.split('\n')
  76. var messageOnStack = null
  77. if (stackframes[0].indexOf(step.message) !== -1) {
  78. // Remove the message if it is in the stack string (eg Chrome)
  79. messageOnStack = stackframes.shift()
  80. }
  81. // Filter frames
  82. var relevantStackFrames = getRelevantStackFrom(stackframes)
  83. if (messageOnStack) {
  84. // Put the message back if we removed it.
  85. relevantStackFrames.unshift(messageOnStack)
  86. } else {
  87. // The stack did not have the step.message so add it.
  88. relevantStackFrames.unshift(step.message)
  89. }
  90. return relevantStackFrames.join('\n')
  91. }
  92. function debugUrl (description) {
  93. // A link to re-run just one failed test case.
  94. return window.location.origin + '/debug.html?spec=' + encodeURIComponent(description)
  95. }
  96. function SuiteNode (name, parent) {
  97. this.name = name
  98. this.parent = parent
  99. this.children = []
  100. this.addChild = function (name) {
  101. var suite = new SuiteNode(name, this)
  102. this.children.push(suite)
  103. return suite
  104. }
  105. }
  106. function processSuite (suite, pointer) {
  107. var child
  108. var childPointer
  109. for (var i = 0; i < suite.children.length; i++) {
  110. child = suite.children[i]
  111. if (child.children) {
  112. childPointer = pointer[child.description] = { _: [] }
  113. processSuite(child, childPointer)
  114. } else {
  115. if (!pointer._) {
  116. pointer._ = []
  117. }
  118. pointer._.push(child.description)
  119. }
  120. }
  121. }
  122. function getAllSpecNames (topSuite) {
  123. var specNames = {}
  124. processSuite(topSuite, specNames)
  125. return specNames
  126. }
  127. /**
  128. * Very simple reporter for Jasmine.
  129. */
  130. function KarmaReporter (tc, jasmineEnv) {
  131. var currentSuite = new SuiteNode()
  132. var startTimeCurrentSpec = new _Date().getTime()
  133. function handleGlobalErrors (result) {
  134. if (result.failedExpectations && result.failedExpectations.length) {
  135. var message = 'An error was thrown in afterAll'
  136. var steps = result.failedExpectations
  137. for (var i = 0, l = steps.length; i < l; i++) {
  138. message += '\n' + formatFailedStep(steps[i])
  139. }
  140. tc.error(message)
  141. }
  142. }
  143. /**
  144. * Jasmine 2.0 dispatches the following events:
  145. *
  146. * - jasmineStarted
  147. * - jasmineDone
  148. * - suiteStarted
  149. * - suiteDone
  150. * - specStarted
  151. * - specDone
  152. */
  153. this.jasmineStarted = function (data) {
  154. // TODO(vojta): Do not send spec names when polling.
  155. tc.info({
  156. event: 'jasmineStarted',
  157. total: data.totalSpecsDefined,
  158. specs: getAllSpecNames(jasmineEnv.topSuite())
  159. })
  160. }
  161. this.jasmineDone = function (result) {
  162. result = result || {}
  163. // Any errors in top-level afterAll blocks are given here.
  164. handleGlobalErrors(result)
  165. // Remove functions from called back results to avoid IPC errors in Electron
  166. // https://github.com/twolfson/karma-electron/issues/47
  167. var cleanedOrder
  168. if (result.order) {
  169. cleanedOrder = {}
  170. var orderKeys = Object.getOwnPropertyNames(result.order)
  171. for (var i = 0; i < orderKeys.length; i++) {
  172. var orderKey = orderKeys[i]
  173. if (typeof result.order[orderKey] !== 'function') {
  174. cleanedOrder[orderKey] = result.order[orderKey]
  175. }
  176. }
  177. }
  178. tc.complete({
  179. order: cleanedOrder,
  180. coverage: window.__coverage__
  181. })
  182. }
  183. this.suiteStarted = function (result) {
  184. currentSuite = currentSuite.addChild(result.description)
  185. tc.info({
  186. event: 'suiteStarted',
  187. result: result
  188. })
  189. }
  190. this.suiteDone = function (result) {
  191. // In the case of xdescribe, only "suiteDone" is fired.
  192. // We need to skip that.
  193. if (result.description !== currentSuite.name) {
  194. return
  195. }
  196. // Any errors in afterAll blocks are given here, except for top-level
  197. // afterAll blocks.
  198. handleGlobalErrors(result)
  199. currentSuite = currentSuite.parent
  200. tc.info({
  201. event: 'suiteDone',
  202. result: result
  203. })
  204. }
  205. this.specStarted = function () {
  206. startTimeCurrentSpec = new _Date().getTime()
  207. }
  208. this.specDone = function (specResult) {
  209. var skipped = specResult.status === 'disabled' || specResult.status === 'pending' || specResult.status === 'excluded'
  210. var result = {
  211. fullName: specResult.fullName,
  212. description: specResult.description,
  213. id: specResult.id,
  214. log: [],
  215. skipped: skipped,
  216. disabled: specResult.status === 'disabled' || specResult.status === 'excluded',
  217. pending: specResult.status === 'pending',
  218. success: specResult.failedExpectations.length === 0,
  219. suite: [],
  220. time: skipped ? 0 : new _Date().getTime() - startTimeCurrentSpec,
  221. executedExpectationsCount: specResult.failedExpectations.length + specResult.passedExpectations.length,
  222. passedExpectations: specResult.passedExpectations,
  223. properties: specResult.properties
  224. }
  225. // generate ordered list of (nested) suite names
  226. var suitePointer = currentSuite
  227. while (suitePointer.parent) {
  228. result.suite.unshift(suitePointer.name)
  229. suitePointer = suitePointer.parent
  230. }
  231. if (!result.success) {
  232. var steps = specResult.failedExpectations
  233. for (var i = 0, l = steps.length; i < l; i++) {
  234. result.log.push(formatFailedStep(steps[i]))
  235. }
  236. if (typeof window !== 'undefined' && window.location && window.location.origin) {
  237. // Report the name of fhe failing spec so the reporter can emit a debug url.
  238. result.debug_url = debugUrl(specResult.fullName)
  239. }
  240. }
  241. // When failSpecWithNoExpectations is true, Jasmine will report specs without expectations as failed
  242. if (result.executedExpectationsCount === 0 && specResult.status === 'failed') {
  243. result.success = false
  244. result.log.push('Spec has no expectations')
  245. }
  246. tc.result(result)
  247. delete specResult.startTime
  248. }
  249. }
  250. /**
  251. * Extract grep option from karma config
  252. * @param {[Array|string]} clientArguments The karma client arguments
  253. * @return {string} The value of grep option by default empty string
  254. */
  255. var getGrepOption = function (clientArguments) {
  256. var grepRegex = /^--grep=(.*)$/
  257. if (Object.prototype.toString.call(clientArguments) === '[object Array]') {
  258. var indexOfGrep = indexOf(clientArguments, '--grep')
  259. if (indexOfGrep !== -1) {
  260. return clientArguments[indexOfGrep + 1]
  261. }
  262. return map(filter(clientArguments, function (arg) {
  263. return grepRegex.test(arg)
  264. }), function (arg) {
  265. return arg.replace(grepRegex, '$1')
  266. })[0] || ''
  267. } else if (typeof clientArguments === 'string') {
  268. var match = /--grep=([^=]+)/.exec(clientArguments)
  269. return match ? match[1] : ''
  270. }
  271. }
  272. var createRegExp = function (filter) {
  273. filter = filter || ''
  274. if (filter === '') {
  275. return new RegExp() // to match all
  276. }
  277. var regExp = /^[/](.*)[/]([gmixXsuUAJD]*)$/ // pattern to check whether the string is RegExp pattern
  278. var parts = regExp.exec(filter)
  279. if (parts === null) {
  280. return new RegExp(filter.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')) // escape functional symbols
  281. }
  282. var patternExpression = parts[1]
  283. var patternSwitches = parts[2]
  284. return new RegExp(patternExpression, patternSwitches)
  285. }
  286. function getGrepSpecsToRun (clientConfig, specs) {
  287. var grepOption = getGrepOption(clientConfig.args)
  288. if (grepOption) {
  289. var regExp = createRegExp(grepOption)
  290. return filter(specs, function specFilter (spec) {
  291. return regExp.test(spec.getFullName())
  292. })
  293. }
  294. }
  295. function parseQueryParams (location) {
  296. var params = {}
  297. if (location && Object.prototype.hasOwnProperty.call(location, 'search')) {
  298. var pairs = location.search.slice(1).split('&')
  299. for (var i = 0; i < pairs.length; i++) {
  300. var keyValue = pairs[i].split('=')
  301. params[decodeURIComponent(keyValue[0])] =
  302. decodeURIComponent(keyValue[1])
  303. }
  304. }
  305. return params
  306. }
  307. function getId (s) {
  308. return s.id
  309. }
  310. function getSpecsByName (specs, name) {
  311. specs = specs.filter(function (s) {
  312. return s.name.indexOf(name) !== -1
  313. })
  314. if (specs.length === 0) {
  315. throw new Error('No spec found with name: "' + name + '"')
  316. }
  317. return specs
  318. }
  319. function getDebugSpecToRun (location, specs) {
  320. var queryParams = parseQueryParams(location)
  321. var spec = queryParams.spec
  322. if (spec) {
  323. // A single spec has been requested by name for debugging.
  324. return getSpecsByName(specs, spec)
  325. }
  326. }
  327. function getSpecsToRunForCurrentShard (specs, shardIndex, totalShards) {
  328. if (specs.length < totalShards) {
  329. throw new Error(
  330. 'More shards (' + totalShards + ') than test specs (' + specs.length +
  331. ')')
  332. }
  333. // Just do a simple sharding strategy of dividing the number of specs
  334. // equally.
  335. var firstSpec = Math.floor(specs.length * shardIndex / totalShards)
  336. var lastSpec = Math.floor(specs.length * (shardIndex + 1) / totalShards)
  337. return specs.slice(firstSpec, lastSpec)
  338. }
  339. function getShardedSpecsToRun (specs, clientConfig) {
  340. var shardIndex = clientConfig.shardIndex
  341. var totalShards = clientConfig.totalShards
  342. if (shardIndex != null && totalShards != null) {
  343. // Sharded mode - Run only the subset of the specs corresponding to the
  344. // current shard.
  345. return getSpecsToRunForCurrentShard(
  346. specs, Number(shardIndex), Number(totalShards))
  347. }
  348. }
  349. /**
  350. * Create jasmine spec filter
  351. * @param {Object} clientConfig karma config
  352. * @param {!Object} jasmineEnv
  353. */
  354. var KarmaSpecFilter = function (clientConfig, jasmineEnv) {
  355. /**
  356. * Walk the test suite tree depth first and collect all test specs
  357. * @param {!Object} jasmineEnv
  358. * @return {!Array<string>} All possible tests.
  359. */
  360. function getAllSpecs (jasmineEnv) {
  361. var specs = []
  362. var stack = [jasmineEnv.topSuite()]
  363. var currentNode
  364. while ((currentNode = stack.pop())) {
  365. if (currentNode.children) {
  366. // jasmine.Suite
  367. stack = stack.concat(currentNode.children)
  368. } else if (currentNode.id) {
  369. // jasmine.Spec
  370. specs.unshift(currentNode)
  371. }
  372. }
  373. return specs
  374. }
  375. /**
  376. * Filter the specs with URL search params and config.
  377. * @param {!Object} location property 'search' from URL.
  378. * @param {!Object} clientConfig karma client config
  379. * @param {!Object} jasmineEnv
  380. * @return {!Array<string>}
  381. */
  382. function getSpecsToRun (location, clientConfig, jasmineEnv) {
  383. var specs = getAllSpecs(jasmineEnv).map(function (spec) {
  384. spec.name = spec.getFullName()
  385. return spec
  386. })
  387. if (!specs || !specs.length) {
  388. return []
  389. }
  390. return getGrepSpecsToRun(clientConfig, specs) ||
  391. getDebugSpecToRun(location, specs) ||
  392. getShardedSpecsToRun(specs, clientConfig) ||
  393. specs
  394. }
  395. this.specIdsToRun = new Set(getSpecsToRun(window.location, clientConfig, jasmineEnv).map(getId))
  396. this.matches = function (spec) {
  397. return this.specIdsToRun.has(spec.id)
  398. }
  399. }
  400. /**
  401. * Configure jasmine specFilter
  402. *
  403. * This function is invoked from the wrapper.
  404. * @see adapter.wrapper
  405. *
  406. * @param {Object} config The karma config
  407. * @param {Object} jasmineEnv jasmine environment object
  408. */
  409. var createSpecFilter = function (config, jasmineEnv) {
  410. var karmaSpecFilter = new KarmaSpecFilter(config, jasmineEnv)
  411. var originalSpecFilter = jasmineEnv.configuration().specFilter
  412. var specFilter = function (spec) {
  413. return originalSpecFilter(spec) && karmaSpecFilter.matches(spec)
  414. }
  415. return specFilter
  416. }
  417. /**
  418. * Karma starter function factory.
  419. *
  420. * This function is invoked from the wrapper.
  421. * @see adapter.wrapper
  422. *
  423. * @param {Object} karma Karma runner instance.
  424. * @param {Object} [jasmineEnv] Optional Jasmine environment for testing.
  425. * @return {Function} Karma starter function.
  426. */
  427. function createStartFn (karma, jasmineEnv) {
  428. // This function will be assigned to `window.__karma__.start`:
  429. return function () {
  430. var clientConfig = karma.config || {}
  431. var jasmineConfig = clientConfig.jasmine || {}
  432. jasmineEnv = jasmineEnv || window.jasmine.getEnv()
  433. jasmineConfig.specFilter = createSpecFilter(clientConfig, jasmineEnv)
  434. jasmineEnv.configure(jasmineConfig)
  435. window.jasmine.DEFAULT_TIMEOUT_INTERVAL = jasmineConfig.timeoutInterval ||
  436. window.jasmine.DEFAULT_TIMEOUT_INTERVAL
  437. jasmineEnv.addReporter(new KarmaReporter(karma, jasmineEnv))
  438. jasmineEnv.execute()
  439. }
  440. }
  441. function indexOf (collection, find, i /* opt */) {
  442. if (collection.indexOf) {
  443. return collection.indexOf(find, i)
  444. }
  445. if (i === undefined) { i = 0 }
  446. if (i < 0) { i += collection.length }
  447. if (i < 0) { i = 0 }
  448. for (var n = collection.length; i < n; i++) {
  449. if (i in collection && collection[i] === find) {
  450. return i
  451. }
  452. }
  453. return -1
  454. }
  455. function filter (collection, filter, that /* opt */) {
  456. if (collection.filter) {
  457. return collection.filter(filter, that)
  458. }
  459. var other = []
  460. var v
  461. for (var i = 0, n = collection.length; i < n; i++) {
  462. if (i in collection && filter.call(that, v = collection[i], i, collection)) {
  463. other.push(v)
  464. }
  465. }
  466. return other
  467. }
  468. function map (collection, mapper, that /* opt */) {
  469. if (collection.map) {
  470. return collection.map(mapper, that)
  471. }
  472. var other = new Array(collection.length)
  473. for (var i = 0, n = collection.length; i < n; i++) {
  474. if (i in collection) {
  475. other[i] = mapper.call(that, collection[i], i, collection)
  476. }
  477. }
  478. return other
  479. }
  480. window.__karma__.start = createStartFn(window.__karma__)
  481. })(typeof window !== 'undefined' ? window : global);