UNPKG

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