UNPKG

8.4 kBJavaScriptView Raw
1'use strict'
2
3const BrowserResult = require('./browser_result')
4const helper = require('./helper')
5const logger = require('./logger')
6
7const CONNECTED = 'CONNECTED' // The browser is connected but not yet been commanded to execute tests.
8const CONFIGURING = 'CONFIGURING' // The browser has been told to execute tests; it is configuring before tests execution.
9const EXECUTING = 'EXECUTING' // The browser is executing the tests.
10const EXECUTING_DISCONNECTED = 'EXECUTING_DISCONNECTED' // The browser is executing the tests, but temporarily disconnect (waiting for socket reconnecting).
11const DISCONNECTED = 'DISCONNECTED' // The browser got completely disconnected (e.g. browser crash) and can be only restored with a restart of execution.
12
13class Browser {
14 constructor (id, fullName, collection, emitter, socket, timer, disconnectDelay, noActivityTimeout) {
15 this.id = id
16 this.fullName = fullName
17 this.name = helper.browserFullNameToShort(fullName)
18 this.lastResult = new BrowserResult()
19 this.disconnectsCount = 0
20 this.activeSockets = [socket]
21 this.noActivityTimeout = noActivityTimeout
22 this.collection = collection
23 this.emitter = emitter
24 this.socket = socket
25 this.timer = timer
26 this.disconnectDelay = disconnectDelay
27
28 this.log = logger.create(this.name)
29
30 this.noActivityTimeoutId = null
31 this.pendingDisconnect = null
32 this.setState(CONNECTED)
33 }
34
35 init () {
36 this.log.info(`Connected on socket ${this.socket.id} with id ${this.id}`)
37
38 this.bindSocketEvents(this.socket)
39 this.collection.add(this)
40 this.emitter.emit('browser_register', this)
41 }
42
43 setState (toState) {
44 this.log.debug(`${this.state} -> ${toState}`)
45 this.state = toState
46 }
47
48 onKarmaError (error) {
49 if (this.isNotConnected()) {
50 this.lastResult.error = true
51 }
52 this.emitter.emit('browser_error', this, error)
53 this.refreshNoActivityTimeout()
54 }
55
56 onInfo (info) {
57 if (helper.isDefined(info.dump)) {
58 this.emitter.emit('browser_log', this, info.dump, 'dump')
59 }
60
61 if (helper.isDefined(info.log)) {
62 this.emitter.emit('browser_log', this, info.log, info.type)
63 } else if (helper.isDefined(info.total)) {
64 if (this.state === EXECUTING) {
65 this.lastResult.total = info.total
66 }
67 } else if (!helper.isDefined(info.dump)) {
68 this.emitter.emit('browser_info', this, info)
69 }
70
71 this.refreshNoActivityTimeout()
72 }
73
74 onStart (info) {
75 if (info.total === null) {
76 this.log.warn('Adapter did not report total number of specs.')
77 }
78
79 this.lastResult = new BrowserResult(info.total)
80 this.setState(EXECUTING)
81 this.emitter.emit('browser_start', this, info)
82 this.refreshNoActivityTimeout()
83 }
84
85 onComplete (result) {
86 if (this.isNotConnected()) {
87 this.setState(CONNECTED)
88 this.lastResult.totalTimeEnd()
89
90 this.emitter.emit('browsers_change', this.collection)
91 this.emitter.emit('browser_complete', this, result)
92
93 this.clearNoActivityTimeout()
94 }
95 }
96
97 onDisconnect (reason, disconnectedSocket) {
98 helper.arrayRemove(this.activeSockets, disconnectedSocket)
99
100 if (this.activeSockets.length) {
101 this.log.debug(`Disconnected ${disconnectedSocket.id}, still have ${this.getActiveSocketsIds()}`)
102 return
103 }
104
105 if (this.isConnected()) {
106 this.disconnect(`Client disconnected from CONNECTED state (${reason})`)
107 } else if ([CONFIGURING, EXECUTING].includes(this.state)) {
108 this.log.debug(`Disconnected during run, waiting ${this.disconnectDelay}ms for reconnecting.`)
109 this.setState(EXECUTING_DISCONNECTED)
110
111 this.pendingDisconnect = this.timer.setTimeout(() => {
112 this.lastResult.totalTimeEnd()
113 this.lastResult.disconnected = true
114 this.disconnect(`reconnect failed before timeout of ${this.disconnectDelay}ms (${reason})`)
115 this.emitter.emit('browser_complete', this)
116 }, this.disconnectDelay)
117
118 this.clearNoActivityTimeout()
119 }
120 }
121
122 reconnect (newSocket) {
123 if (this.state === EXECUTING_DISCONNECTED) {
124 this.log.debug(`Lost socket connection, but browser continued to execute. Reconnected ` +
125 `on socket ${newSocket.id}.`)
126 this.setState(EXECUTING)
127 } else if ([CONNECTED, CONFIGURING, EXECUTING].includes(this.state)) {
128 this.log.debug(`Rebinding to new socket ${newSocket.id} (already have ` +
129 `${this.getActiveSocketsIds()})`)
130 } else if (this.state === DISCONNECTED) {
131 this.log.info(`Disconnected browser returned on socket ${newSocket.id} with id ${this.id}.`)
132 this.setState(CONNECTED)
133
134 // Since the disconnected browser is already part of the collection and we want to
135 // make sure that the server can properly handle the browser like it's the first time
136 // connecting this browser (as we want a complete new execution), we need to emit the
137 // following events:
138 this.emitter.emit('browsers_change', this.collection)
139 this.emitter.emit('browser_register', this)
140 }
141
142 if (!this.activeSockets.some((s) => s.id === newSocket.id)) {
143 this.activeSockets.push(newSocket)
144 this.bindSocketEvents(newSocket)
145 }
146
147 if (this.pendingDisconnect) {
148 this.timer.clearTimeout(this.pendingDisconnect)
149 }
150
151 this.refreshNoActivityTimeout()
152 }
153
154 onResult (result) {
155 if (result.length) {
156 result.forEach(this.onResult, this)
157 return
158 } else if (this.isNotConnected()) {
159 this.lastResult.add(result)
160 this.emitter.emit('spec_complete', this, result)
161 }
162 this.refreshNoActivityTimeout()
163 }
164
165 execute (config) {
166 this.activeSockets.forEach((socket) => socket.emit('execute', config))
167 this.setState(CONFIGURING)
168 this.refreshNoActivityTimeout()
169 }
170
171 getActiveSocketsIds () {
172 return this.activeSockets.map((s) => s.id).join(', ')
173 }
174
175 disconnect (reason) {
176 this.log.warn(`Disconnected (${this.disconnectsCount} times)${reason || ''}`)
177 this.setState(DISCONNECTED)
178 this.disconnectsCount++
179 this.emitter.emit('browser_error', this, `Disconnected${reason || ''}`)
180 this.collection.remove(this)
181 }
182
183 refreshNoActivityTimeout () {
184 if (this.noActivityTimeout) {
185 this.clearNoActivityTimeout()
186
187 this.noActivityTimeoutId = this.timer.setTimeout(() => {
188 this.lastResult.totalTimeEnd()
189 this.lastResult.disconnected = true
190 this.disconnect(`, because no message in ${this.noActivityTimeout} ms.`)
191 this.emitter.emit('browser_complete', this)
192 }, this.noActivityTimeout)
193 }
194 }
195
196 clearNoActivityTimeout () {
197 if (this.noActivityTimeout && this.noActivityTimeoutId) {
198 this.timer.clearTimeout(this.noActivityTimeoutId)
199 this.noActivityTimeoutId = null
200 }
201 }
202
203 bindSocketEvents (socket) {
204 // TODO: check which of these events are actually emitted by socket
205 socket.on('disconnect', (reason) => this.onDisconnect(reason, socket))
206 socket.on('start', (info) => this.onStart(info))
207 socket.on('karma_error', (error) => this.onKarmaError(error))
208 socket.on('complete', (result) => this.onComplete(result))
209 socket.on('info', (info) => this.onInfo(info))
210 socket.on('result', (result) => this.onResult(result))
211 }
212
213 isConnected () {
214 return this.state === CONNECTED
215 }
216
217 isNotConnected () {
218 return !this.isConnected()
219 }
220
221 serialize () {
222 return {
223 id: this.id,
224 name: this.name,
225 isConnected: this.state === CONNECTED
226 }
227 }
228
229 toString () {
230 return this.name
231 }
232
233 toJSON () {
234 return {
235 id: this.id,
236 fullName: this.fullName,
237 name: this.name,
238 state: this.state,
239 lastResult: this.lastResult,
240 disconnectsCount: this.disconnectsCount,
241 noActivityTimeout: this.noActivityTimeout,
242 disconnectDelay: this.disconnectDelay
243 }
244 }
245}
246
247Browser.factory = function (
248 id, fullName, /* capturedBrowsers */ collection, emitter, socket, timer,
249 /* config.browserDisconnectTimeout */ disconnectDelay,
250 /* config.browserNoActivityTimeout */ noActivityTimeout
251) {
252 return new Browser(id, fullName, collection, emitter, socket, timer, disconnectDelay, noActivityTimeout)
253}
254
255Browser.STATE_CONNECTED = CONNECTED
256Browser.STATE_CONFIGURING = CONFIGURING
257Browser.STATE_EXECUTING = EXECUTING
258Browser.STATE_EXECUTING_DISCONNECTED = EXECUTING_DISCONNECTED
259Browser.STATE_DISCONNECTED = DISCONNECTED
260
261module.exports = Browser