-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
karma.js
324 lines (286 loc) · 10.4 KB
/
karma.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
var stringify = require('../common/stringify')
var constant = require('./constants')
var util = require('../common/util')
function Karma (updater, socket, iframe, opener, navigator, location, document) {
this.updater = updater
var startEmitted = false
var self = this
var queryParams = util.parseQueryParams(location.search)
var browserId = queryParams.id || util.generateId('manual-')
var displayName = queryParams.displayName
var returnUrl = queryParams['return_url' + ''] || null
var resultsBufferLimit = 50
var resultsBuffer = []
// This is a no-op if not running with a Trusted Types CSP policy, and
// lets tests declare that they trust the way that karma creates and handles
// URLs.
//
// More info about the proposed Trusted Types standard at
// https://github.com/WICG/trusted-types
var policy = {
createURL: function (s) {
return s
},
createScriptURL: function (s) {
return s
}
}
var trustedTypes = window.trustedTypes || window.TrustedTypes
if (trustedTypes) {
policy = trustedTypes.createPolicy('karma', policy)
if (!policy.createURL) {
// Install createURL for newer browsers. Only browsers that implement an
// old version of the spec require createURL.
// Should be safe to delete all reference to createURL by
// February 2020.
// https://github.com/WICG/trusted-types/pull/204
policy.createURL = function (s) { return s }
}
}
// To start we will signal the server that we are not reconnecting. If the socket loses
// connection and was able to reconnect to the Karma server we will get a
// second 'connect' event. There we will pass 'true' and that will be passed to the
// Karma server then, so that Karma can differentiate between a socket client
// econnect and a full browser reconnect.
var socketReconnect = false
this.VERSION = constant.VERSION
this.config = {}
// Expose for testing purposes as there is no global socket.io
// registry anymore.
this.socket = socket
// Set up postMessage bindings for current window
// DEV: These are to allow windows in separate processes execute local tasks
// Electron is one of these environments
if (window.addEventListener) {
window.addEventListener('message', function handleMessage (evt) {
// Resolve the origin of our message
var origin = evt.origin || evt.originalEvent.origin
// If the message isn't from our host, then reject it
if (origin !== window.location.origin) {
return
}
// Take action based on the message type
var method = evt.data.__karmaMethod
if (method) {
if (!self[method]) {
self.error('Received `postMessage` for "' + method + '" but the method doesn\'t exist')
return
}
self[method].apply(self, evt.data.__karmaArguments)
}
}, false)
}
var childWindow = null
function navigateContextTo (url) {
if (self.config.useIframe === false) {
// run in new window
if (self.config.runInParent === false) {
// If there is a window already open, then close it
// DEV: In some environments (e.g. Electron), we don't have setter access for location
if (childWindow !== null && childWindow.closed !== true) {
// The onbeforeunload listener was added by context to catch
// unexpected navigations while running tests.
childWindow.onbeforeunload = undefined
childWindow.close()
}
childWindow = opener(url)
if (childWindow === null) {
self.error('Opening a new tab/window failed, probably because pop-ups are blocked.')
}
// run context on parent element (client_with_context)
// using window.__karma__.scriptUrls to get the html element strings and load them dynamically
} else if (url !== 'about:blank') {
var loadScript = function (idx) {
if (idx < window.__karma__.scriptUrls.length) {
var parser = new DOMParser()
// Revert escaped characters with special roles in HTML before parsing
var string = window.__karma__.scriptUrls[idx]
.replace(/\\x3C/g, '<')
.replace(/\\x3E/g, '>')
var doc = parser.parseFromString(string, 'text/html')
var ele = doc.head.firstChild || doc.body.firstChild
// script elements created by DomParser are marked as unexecutable,
// create a new script element manually and copy necessary properties
// so it is executable
if (ele.tagName && ele.tagName.toLowerCase() === 'script') {
var tmp = ele
ele = document.createElement('script')
ele.src = policy.createScriptURL(tmp.src)
ele.crossOrigin = tmp.crossOrigin
}
ele.onload = function () {
loadScript(idx + 1)
}
document.body.appendChild(ele)
} else {
window.__karma__.loaded()
}
}
loadScript(0)
}
// run in iframe
} else {
// The onbeforeunload listener was added by the context to catch
// unexpected navigations while running tests.
iframe.contentWindow.onbeforeunload = undefined
iframe.src = policy.createURL(url)
}
}
this.log = function (type, args) {
var values = []
for (var i = 0; i < args.length; i++) {
values.push(this.stringify(args[i], 3))
}
this.info({ log: values.join(', '), type: type })
}
this.stringify = stringify
function getLocation (url, lineno, colno) {
var location = ''
if (url !== undefined) {
location += url
}
if (lineno !== undefined) {
location += ':' + lineno
}
if (colno !== undefined) {
location += ':' + colno
}
return location
}
// error during js file loading (most likely syntax error)
// we are not going to execute at all. `window.onerror` callback.
this.error = function (messageOrEvent, source, lineno, colno, error) {
var message
if (typeof messageOrEvent === 'string') {
message = messageOrEvent
var location = getLocation(source, lineno, colno)
if (location !== '') {
message += '\nat ' + location
}
if (error && error.stack) {
message += '\n\n' + error.stack
}
} else {
// create an object with the string representation of the message to
// ensure all its content is properly transferred to the console log
message = { message: messageOrEvent, str: messageOrEvent.toString() }
}
socket.emit('karma_error', message)
self.updater.updateTestStatus('karma_error ' + message)
this.complete()
return false
}
this.result = function (originalResult) {
var convertedResult = {}
// Convert all array-like objects to real arrays.
for (var propertyName in originalResult) {
if (Object.prototype.hasOwnProperty.call(originalResult, propertyName)) {
var propertyValue = originalResult[propertyName]
if (Object.prototype.toString.call(propertyValue) === '[object Array]') {
convertedResult[propertyName] = Array.prototype.slice.call(propertyValue)
} else {
convertedResult[propertyName] = propertyValue
}
}
}
if (!startEmitted) {
socket.emit('start', { total: null })
self.updater.updateTestStatus('start')
startEmitted = true
}
if (resultsBufferLimit === 1) {
self.updater.updateTestStatus('result')
return socket.emit('result', convertedResult)
}
resultsBuffer.push(convertedResult)
if (resultsBuffer.length === resultsBufferLimit) {
socket.emit('result', resultsBuffer)
self.updater.updateTestStatus('result')
resultsBuffer = []
}
}
this.complete = function (result) {
if (resultsBuffer.length) {
socket.emit('result', resultsBuffer)
resultsBuffer = []
}
socket.emit('complete', result || {})
if (this.config.clearContext) {
navigateContextTo('about:blank')
} else {
self.updater.updateTestStatus('complete')
}
if (returnUrl) {
var isReturnUrlAllowed = false
for (var i = 0; i < this.config.allowedReturnUrlPatterns.length; i++) {
var allowedReturnUrlPattern = new RegExp(this.config.allowedReturnUrlPatterns[i])
if (allowedReturnUrlPattern.test(returnUrl)) {
isReturnUrlAllowed = true
break
}
}
if (!isReturnUrlAllowed) {
throw new Error(
'Security: Navigation to '.concat(
returnUrl,
' was blocked to prevent malicious exploits.'
)
)
}
location.href = returnUrl
}
}
this.info = function (info) {
// TODO(vojta): introduce special API for this
if (!startEmitted && util.isDefined(info.total)) {
socket.emit('start', info)
startEmitted = true
} else {
socket.emit('info', info)
}
}
socket.on('execute', function (cfg) {
self.updater.updateTestStatus('execute')
// reset startEmitted and reload the iframe
startEmitted = false
self.config = cfg
navigateContextTo(constant.CONTEXT_URL)
if (self.config.clientDisplayNone) {
[].forEach.call(document.querySelectorAll('#banner, #browsers'), function (el) {
el.style.display = 'none'
})
}
// clear the console before run
// works only on FF (Safari, Chrome do not allow to clear console from js source)
if (window.console && window.console.clear) {
window.console.clear()
}
})
socket.on('stop', function () {
this.complete()
}.bind(this))
// Report the browser name and Id. Note that this event can also fire if the connection has
// been temporarily lost, but the socket reconnected automatically. Read more in the docs:
// https://socket.io/docs/client-api/#Event-%E2%80%98connect%E2%80%99
socket.on('connect', function () {
socket.io.engine.on('upgrade', function () {
resultsBufferLimit = 1
// Flush any results which were buffered before the upgrade to WebSocket protocol.
if (resultsBuffer.length > 0) {
socket.emit('result', resultsBuffer)
resultsBuffer = []
}
})
var info = {
name: navigator.userAgent,
id: browserId,
isSocketReconnect: socketReconnect
}
if (displayName) {
info.displayName = displayName
}
socket.emit('register', info)
socketReconnect = true
})
}
module.exports = Karma