UNPKG

8.39 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 (Array.isArray(result)) {
156 result.forEach(this.onResult, this)
157 } else if (this.isNotConnected()) {
158 this.lastResult.add(result)
159 this.emitter.emit('spec_complete', this, result)
160 }
161 this.refreshNoActivityTimeout()
162 }
163
164 execute (config) {
165 this.activeSockets.forEach((socket) => socket.emit('execute', config))
166 this.setState(CONFIGURING)
167 this.refreshNoActivityTimeout()
168 }
169
170 getActiveSocketsIds () {
171 return this.activeSockets.map((s) => s.id).join(', ')
172 }
173
174 disconnect (reason) {
175 this.log.warn(`Disconnected (${this.disconnectsCount} times)${reason || ''}`)
176 this.setState(DISCONNECTED)
177 this.disconnectsCount++
178 this.emitter.emit('browser_error', this, `Disconnected${reason || ''}`)
179 this.collection.remove(this)
180 }
181
182 refreshNoActivityTimeout () {
183 if (this.noActivityTimeout) {
184 this.clearNoActivityTimeout()
185
186 this.noActivityTimeoutId = this.timer.setTimeout(() => {
187 this.lastResult.totalTimeEnd()
188 this.lastResult.disconnected = true
189 this.disconnect(`, because no message in ${this.noActivityTimeout} ms.`)
190 this.emitter.emit('browser_complete', this)
191 }, this.noActivityTimeout)
192 }
193 }
194
195 clearNoActivityTimeout () {
196 if (this.noActivityTimeout && this.noActivityTimeoutId) {
197 this.timer.clearTimeout(this.noActivityTimeoutId)
198 this.noActivityTimeoutId = null
199 }
200 }
201
202 bindSocketEvents (socket) {
203 // TODO: check which of these events are actually emitted by socket
204 socket.on('disconnect', (reason) => this.onDisconnect(reason, socket))
205 socket.on('start', (info) => this.onStart(info))
206 socket.on('karma_error', (error) => this.onKarmaError(error))
207 socket.on('complete', (result) => this.onComplete(result))
208 socket.on('info', (info) => this.onInfo(info))
209 socket.on('result', (result) => this.onResult(result))
210 }
211
212 isConnected () {
213 return this.state === CONNECTED
214 }
215
216 isNotConnected () {
217 return !this.isConnected()
218 }
219
220 serialize () {
221 return {
222 id: this.id,
223 name: this.name,
224 isConnected: this.state === CONNECTED
225 }
226 }
227
228 toString () {
229 return this.name
230 }
231
232 toJSON () {
233 return {
234 id: this.id,
235 fullName: this.fullName,
236 name: this.name,
237 state: this.state,
238 lastResult: this.lastResult,
239 disconnectsCount: this.disconnectsCount,
240 noActivityTimeout: this.noActivityTimeout,
241 disconnectDelay: this.disconnectDelay
242 }
243 }
244}
245
246Browser.factory = function (
247 id, fullName, /* capturedBrowsers */ collection, emitter, socket, timer,
248 /* config.browserDisconnectTimeout */ disconnectDelay,
249 /* config.browserNoActivityTimeout */ noActivityTimeout
250) {
251 return new Browser(id, fullName, collection, emitter, socket, timer, disconnectDelay, noActivityTimeout)
252}
253
254Browser.STATE_CONNECTED = CONNECTED
255Browser.STATE_CONFIGURING = CONFIGURING
256Browser.STATE_EXECUTING = EXECUTING
257Browser.STATE_EXECUTING_DISCONNECTED = EXECUTING_DISCONNECTED
258Browser.STATE_DISCONNECTED = DISCONNECTED
259
260module.exports = Browser