browser.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. 'use strict'
  2. const Result = require('./browser_result')
  3. const helper = require('./helper')
  4. const logger = require('./logger')
  5. // The browser is ready to execute tests.
  6. const READY = 1
  7. // The browser is executing the tests.
  8. const EXECUTING = 2
  9. // The browser is not executing, but temporarily disconnected (waiting for reconnecting).
  10. const READY_DISCONNECTED = 3
  11. // The browser is executing the tests, but temporarily disconnect (waiting for reconnecting).
  12. const EXECUTING_DISCONNECTED = 4
  13. // The browser got permanently disconnected (being removed from the collection and destroyed).
  14. const DISCONNECTED = 5
  15. class Browser {
  16. constructor (id, fullName, collection, emitter, socket, timer, disconnectDelay, noActivityTimeout) {
  17. this.id = id
  18. this.fullName = fullName
  19. this.name = helper.browserFullNameToShort(fullName)
  20. this.state = READY
  21. this.lastResult = new Result()
  22. this.disconnectsCount = 0
  23. this.activeSockets = [socket]
  24. this.noActivityTimeout = noActivityTimeout
  25. this.collection = collection
  26. this.emitter = emitter
  27. this.socket = socket
  28. this.timer = timer
  29. this.disconnectDelay = disconnectDelay
  30. this.log = logger.create(this.name)
  31. this.noActivityTimeoutId = null
  32. this.pendingDisconnect = null
  33. }
  34. init () {
  35. this.collection.add(this)
  36. this.bindSocketEvents(this.socket)
  37. this.log.info('Connected on socket %s with id %s', this.socket.id, this.id)
  38. // TODO(vojta): move to collection
  39. this.emitter.emit('browsers_change', this.collection)
  40. this.emitter.emit('browser_register', this)
  41. }
  42. isReady () {
  43. return this.state === READY
  44. }
  45. toString () {
  46. return this.name
  47. }
  48. onKarmaError (error) {
  49. if (this.isReady()) {
  50. return
  51. }
  52. this.lastResult.error = true
  53. this.emitter.emit('browser_error', this, error)
  54. this.refreshNoActivityTimeout()
  55. }
  56. onInfo (info) {
  57. if (this.isReady()) {
  58. return
  59. }
  60. // TODO(vojta): remove
  61. if (helper.isDefined(info.dump)) {
  62. this.emitter.emit('browser_log', this, info.dump, 'dump')
  63. }
  64. if (helper.isDefined(info.log)) {
  65. this.emitter.emit('browser_log', this, info.log, info.type)
  66. }
  67. if (
  68. !helper.isDefined(info.log) &&
  69. !helper.isDefined(info.dump)
  70. ) {
  71. this.emitter.emit('browser_info', this, info)
  72. }
  73. this.refreshNoActivityTimeout()
  74. }
  75. onStart (info) {
  76. this.lastResult = new Result()
  77. this.lastResult.total = info.total
  78. if (info.total === null) {
  79. this.log.warn('Adapter did not report total number of specs.')
  80. }
  81. this.emitter.emit('browser_start', this, info)
  82. this.refreshNoActivityTimeout()
  83. }
  84. onComplete (result) {
  85. if (this.isReady()) {
  86. return
  87. }
  88. this.state = READY
  89. this.lastResult.totalTimeEnd()
  90. if (!this.lastResult.success) {
  91. this.lastResult.error = true
  92. }
  93. this.emitter.emit('browsers_change', this.collection)
  94. this.emitter.emit('browser_complete', this, result)
  95. this.clearNoActivityTimeout()
  96. }
  97. onDisconnect (disconnectedSocket) {
  98. this.activeSockets.splice(this.activeSockets.indexOf(disconnectedSocket), 1)
  99. if (this.activeSockets.length) {
  100. this.log.debug('Disconnected %s, still have %s', disconnectedSocket.id, this.getActiveSocketsIds())
  101. return
  102. }
  103. if (this.state === READY) {
  104. this.disconnect()
  105. } else if (this.state === EXECUTING) {
  106. this.log.debug('Disconnected during run, waiting %sms for reconnecting.', this.disconnectDelay)
  107. this.state = EXECUTING_DISCONNECTED
  108. this.pendingDisconnect = this.timer.setTimeout(() => {
  109. this.lastResult.totalTimeEnd()
  110. this.lastResult.disconnected = true
  111. this.disconnect()
  112. this.emitter.emit('browser_complete', this)
  113. }, this.disconnectDelay)
  114. this.clearNoActivityTimeout()
  115. }
  116. }
  117. reconnect (newSocket) {
  118. if (this.state === EXECUTING_DISCONNECTED) {
  119. this.state = EXECUTING
  120. this.log.debug('Reconnected on %s.', newSocket.id)
  121. } else if (this.state === EXECUTING || this.state === READY) {
  122. this.log.debug('New connection %s (already have %s)', newSocket.id, this.getActiveSocketsIds())
  123. } else if (this.state === DISCONNECTED) {
  124. this.state = READY
  125. this.log.info('Connected on socket %s with id %s', newSocket.id, this.id)
  126. this.collection.add(this)
  127. // TODO(vojta): move to collection
  128. this.emitter.emit('browsers_change', this.collection)
  129. this.emitter.emit('browser_register', this)
  130. }
  131. const exists = this.activeSockets.some((s) => s.id === newSocket.id)
  132. if (!exists) {
  133. this.activeSockets.push(newSocket)
  134. this.bindSocketEvents(newSocket)
  135. }
  136. if (this.pendingDisconnect) {
  137. this.timer.clearTimeout(this.pendingDisconnect)
  138. }
  139. this.refreshNoActivityTimeout()
  140. }
  141. onResult (result) {
  142. if (result.length) {
  143. return result.forEach(this.onResult, this)
  144. }
  145. // ignore - probably results from last run (after server disconnecting)
  146. if (this.isReady()) {
  147. return
  148. }
  149. this.lastResult.add(result)
  150. this.emitter.emit('spec_complete', this, result)
  151. this.refreshNoActivityTimeout()
  152. }
  153. serialize () {
  154. return {
  155. id: this.id,
  156. name: this.name,
  157. isReady: this.state === READY
  158. }
  159. }
  160. execute (config) {
  161. this.activeSockets.forEach((socket) => socket.emit('execute', config))
  162. this.state = EXECUTING
  163. this.refreshNoActivityTimeout()
  164. }
  165. getActiveSocketsIds () {
  166. return this.activeSockets.map((s) => s.id).join(', ')
  167. }
  168. disconnect (reason) {
  169. this.state = DISCONNECTED
  170. this.disconnectsCount++
  171. this.log.warn('Disconnected (%d times)' + (reason || ''), this.disconnectsCount)
  172. this.emitter.emit('browser_error', this, 'Disconnected' + (reason || ''))
  173. this.collection.remove(this)
  174. }
  175. refreshNoActivityTimeout () {
  176. if (this.noActivityTimeout) {
  177. this.clearNoActivityTimeout()
  178. this.noActivityTimeoutId = this.timer.setTimeout(() => {
  179. this.lastResult.totalTimeEnd()
  180. this.lastResult.disconnected = true
  181. this.disconnect(', because no message in ' + this.noActivityTimeout + ' ms.')
  182. this.emitter.emit('browser_complete', this)
  183. }, this.noActivityTimeout)
  184. }
  185. }
  186. clearNoActivityTimeout () {
  187. if (this.noActivityTimeout) {
  188. if (this.noActivityTimeoutId) {
  189. this.timer.clearTimeout(this.noActivityTimeoutId)
  190. this.noActivityTimeoutId = null
  191. }
  192. }
  193. }
  194. bindSocketEvents (socket) {
  195. // TODO: check which of these events are actually emitted by socket
  196. socket.on('disconnect', () => this.onDisconnect(socket))
  197. socket.on('start', (info) => this.onStart(info))
  198. socket.on('karma_error', (error) => this.onKarmaError(error))
  199. socket.on('complete', (result) => this.onComplete(result))
  200. socket.on('info', (info) => this.onInfo(info))
  201. socket.on('result', (result) => this.onResult(result))
  202. }
  203. }
  204. Browser.factory = function (
  205. id, fullName, /* capturedBrowsers */ collection, emitter, socket, timer,
  206. /* config.browserDisconnectTimeout */ disconnectDelay,
  207. /* config.browserNoActivityTimeout */ noActivityTimeout
  208. ) {
  209. return new Browser(id, fullName, collection, emitter, socket, timer, disconnectDelay, noActivityTimeout)
  210. }
  211. Browser.STATE_READY = READY
  212. Browser.STATE_EXECUTING = EXECUTING
  213. Browser.STATE_READY_DISCONNECTED = READY_DISCONNECTED
  214. Browser.STATE_EXECUTING_DISCONNECTED = EXECUTING_DISCONNECTED
  215. Browser.STATE_DISCONNECTED = DISCONNECTED
  216. module.exports = Browser