karma.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. var stringify = require('../common/stringify')
  2. var constant = require('./constants')
  3. var util = require('../common/util')
  4. var Karma = function (socket, iframe, opener, navigator, location) {
  5. var startEmitted = false
  6. var reloadingContext = false
  7. var self = this
  8. var queryParams = util.parseQueryParams(location.search)
  9. var browserId = queryParams.id || util.generateId('manual-')
  10. var displayName = queryParams.displayName
  11. var returnUrl = queryParams['return_url' + ''] || null
  12. var resultsBufferLimit = 50
  13. var resultsBuffer = []
  14. this.VERSION = constant.VERSION
  15. this.config = {}
  16. // Expose for testing purposes as there is no global socket.io
  17. // registry anymore.
  18. this.socket = socket
  19. // Set up postMessage bindings for current window
  20. // DEV: These are to allow windows in separate processes execute local tasks
  21. // Electron is one of these environments
  22. if (window.addEventListener) {
  23. window.addEventListener('message', function handleMessage (evt) {
  24. // Resolve the origin of our message
  25. var origin = evt.origin || evt.originalEvent.origin
  26. // If the message isn't from our host, then reject it
  27. if (origin !== window.location.origin) {
  28. return
  29. }
  30. // Take action based on the message type
  31. var method = evt.data.__karmaMethod
  32. if (method) {
  33. if (!self[method]) {
  34. self.error('Received `postMessage` for "' + method + '" but the method doesn\'t exist')
  35. return
  36. }
  37. self[method].apply(self, evt.data.__karmaArguments)
  38. }
  39. }, false)
  40. }
  41. var childWindow = null
  42. var navigateContextTo = function (url) {
  43. if (self.config.useIframe === false) {
  44. // run in new window
  45. if (self.config.runInParent === false) {
  46. // If there is a window already open, then close it
  47. // DEV: In some environments (e.g. Electron), we don't have setter access for location
  48. if (childWindow !== null && childWindow.closed !== true) {
  49. childWindow.close()
  50. }
  51. childWindow = opener(url)
  52. // run context on parent element and dynamically loading scripts
  53. } else if (url !== 'about:blank') {
  54. var loadScript = function (idx) {
  55. if (idx < window.__karma__.scriptUrls.length) {
  56. var ele = document.createElement('script')
  57. ele.src = window.__karma__.scriptUrls[idx]
  58. ele.onload = function () {
  59. loadScript(idx + 1)
  60. }
  61. document.body.appendChild(ele)
  62. } else {
  63. window.__karma__.loaded()
  64. }
  65. }
  66. loadScript(0)
  67. }
  68. // run in iframe
  69. } else {
  70. iframe.src = url
  71. }
  72. }
  73. this.onbeforeunload = function () {
  74. if (!reloadingContext) {
  75. // TODO(vojta): show what test (with explanation about jasmine.UPDATE_INTERVAL)
  76. self.error('Some of your tests did a full page reload!')
  77. }
  78. }
  79. this.log = function (type, args) {
  80. var values = []
  81. for (var i = 0; i < args.length; i++) {
  82. values.push(this.stringify(args[i], 3))
  83. }
  84. this.info({log: values.join(', '), type: type})
  85. }
  86. this.stringify = stringify
  87. var clearContext = function () {
  88. reloadingContext = true
  89. navigateContextTo('about:blank')
  90. }
  91. function getLocation (url, lineno, colno) {
  92. var location = ''
  93. if (url !== undefined) {
  94. location += url
  95. }
  96. if (lineno !== undefined) {
  97. location += ':' + lineno
  98. }
  99. if (colno !== undefined) {
  100. location += ':' + colno
  101. }
  102. return location
  103. }
  104. // error during js file loading (most likely syntax error)
  105. // we are not going to execute at all. `window.onerror` callback.
  106. this.error = function (messageOrEvent, source, lineno, colno, error) {
  107. var message = messageOrEvent
  108. var location = getLocation(source, lineno, colno)
  109. if (location !== '') {
  110. message += '\nat ' + location
  111. }
  112. if (error) {
  113. message += '\n\n' + error.stack
  114. }
  115. // create an object with the string representation of the message to ensure all its content is properly
  116. // transferred to the console log
  117. message = {message: message, str: message.toString()}
  118. socket.emit('karma_error', message)
  119. this.complete()
  120. return false
  121. }
  122. this.result = function (originalResult) {
  123. var convertedResult = {}
  124. // Convert all array-like objects to real arrays.
  125. for (var propertyName in originalResult) {
  126. if (originalResult.hasOwnProperty(propertyName)) {
  127. var propertyValue = originalResult[propertyName]
  128. if (Object.prototype.toString.call(propertyValue) === '[object Array]') {
  129. convertedResult[propertyName] = Array.prototype.slice.call(propertyValue)
  130. } else {
  131. convertedResult[propertyName] = propertyValue
  132. }
  133. }
  134. }
  135. if (!startEmitted) {
  136. socket.emit('start', {total: null})
  137. startEmitted = true
  138. }
  139. if (resultsBufferLimit === 1) {
  140. return socket.emit('result', convertedResult)
  141. }
  142. resultsBuffer.push(convertedResult)
  143. if (resultsBuffer.length === resultsBufferLimit) {
  144. socket.emit('result', resultsBuffer)
  145. resultsBuffer = []
  146. }
  147. }
  148. this.complete = function (result) {
  149. if (resultsBuffer.length) {
  150. socket.emit('result', resultsBuffer)
  151. resultsBuffer = []
  152. }
  153. if (self.config.clearContext) {
  154. // give the browser some time to breath, there could be a page reload, but because a bunch of
  155. // tests could run in the same event loop, we wouldn't notice.
  156. setTimeout(function () {
  157. clearContext()
  158. }, 0)
  159. }
  160. socket.emit('complete', result || {}, function () {
  161. if (returnUrl) {
  162. location.href = returnUrl
  163. }
  164. })
  165. }
  166. this.info = function (info) {
  167. // TODO(vojta): introduce special API for this
  168. if (!startEmitted && util.isDefined(info.total)) {
  169. socket.emit('start', info)
  170. startEmitted = true
  171. } else {
  172. socket.emit('info', info)
  173. }
  174. }
  175. socket.on('execute', function (cfg) {
  176. // reset startEmitted and reload the iframe
  177. startEmitted = false
  178. self.config = cfg
  179. // if not clearing context, reloadingContext always true to prevent beforeUnload error
  180. reloadingContext = !self.config.clearContext
  181. navigateContextTo(constant.CONTEXT_URL)
  182. // clear the console before run
  183. // works only on FF (Safari, Chrome do not allow to clear console from js source)
  184. if (window.console && window.console.clear) {
  185. window.console.clear()
  186. }
  187. })
  188. socket.on('stop', function () {
  189. this.complete()
  190. }.bind(this))
  191. // report browser name, id
  192. socket.on('connect', function () {
  193. socket.io.engine.on('upgrade', function () {
  194. resultsBufferLimit = 1
  195. })
  196. var info = {
  197. name: navigator.userAgent,
  198. id: browserId
  199. }
  200. if (displayName) {
  201. info.displayName = displayName
  202. }
  203. socket.emit('register', info)
  204. })
  205. }
  206. module.exports = Karma