-
Notifications
You must be signed in to change notification settings - Fork 3.3k
/
Copy pathbackground.js
360 lines (303 loc) · 10.5 KB
/
background.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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
const get = require('lodash/get')
const map = require('lodash/map')
const pick = require('lodash/pick')
const once = require('lodash/once')
const Promise = require('bluebird')
const browser = require('webextension-polyfill')
const { cookieMatches } = require('@packages/server/lib/automation/util')
const client = require('./client')
const util = require('../../lib/util')
const COOKIE_PROPS = ['url', 'name', 'path', 'secure', 'domain']
const GET_ALL_PROPS = COOKIE_PROPS.concat(['session', 'storeId'])
// https://developer.chrome.com/extensions/cookies#method-set
const SET_PROPS = COOKIE_PROPS.concat(['value', 'httpOnly', 'expirationDate', 'sameSite'])
const httpRe = /^http/
// normalize into null when empty array
const firstOrNull = (cookies) => {
return cookies[0] != null ? cookies[0] : null
}
const checkIfFirefox = async () => {
if (!browser || !get(browser, 'runtime.getBrowserInfo')) {
return false
}
const { name } = await browser.runtime.getBrowserInfo()
return name === 'Firefox'
}
const connect = function (host, path, extraOpts) {
const listenToCookieChanges = once(() => {
return browser.cookies.onChanged.addListener((info) => {
if (info.cause !== 'overwrite') {
return ws.emit('automation:push:request', 'change:cookie', info)
}
})
})
const listenToDownloads = once(() => {
browser.downloads.onCreated.addListener((downloadItem) => {
ws.emit('automation:push:request', 'create:download', {
id: `${downloadItem.id}`,
filePath: downloadItem.filename,
mime: downloadItem.mime,
url: downloadItem.url,
})
})
browser.downloads.onChanged.addListener((downloadDelta) => {
const state = (downloadDelta.state || {}).current
if (state === 'complete') {
ws.emit('automation:push:request', 'complete:download', {
id: `${downloadDelta.id}`,
})
}
if (state === 'canceled') {
ws.emit('automation:push:request', 'canceled:download', {
id: `${downloadDelta.id}`,
})
}
})
})
const listenToOnBeforeHeaders = once(() => {
// adds a header to the request to mark it as a request for the AUT frame
// itself, so the proxy can utilize that for injection purposes
browser.webRequest.onBeforeSendHeaders.addListener((details) => {
if (
// parentFrameId: 0 means the parent is the top-level, so if it isn't
// 0, it's nested inside the AUT and can't be the AUT itself
details.parentFrameId !== 0
// is the spec frame, not the AUT
|| details.url.includes('__cypress')
) return
return {
requestHeaders: [
...details.requestHeaders,
{
name: 'X-Cypress-Is-AUT-Frame',
value: 'true',
},
],
}
}, { urls: ['<all_urls>'], types: ['sub_frame'] }, ['blocking', 'requestHeaders'])
})
const fail = (id, err) => {
return ws.emit('automation:response', id, {
__error: err.message,
__stack: err.stack,
__name: err.name,
})
}
const invoke = function (method, id, ...args) {
const respond = (data) => {
return ws.emit('automation:response', id, { response: data })
}
return Promise.try(() => {
return automation[method].apply(automation, args.concat(respond))
}).catch((err) => {
return fail(id, err)
})
}
const ws = client.connect(host, path, extraOpts)
ws.on('automation:request', (id, msg, data) => {
switch (msg) {
case 'get:cookies':
return invoke('getCookies', id, data)
case 'get:cookie':
return invoke('getCookie', id, data)
case 'set:cookie':
return invoke('setCookie', id, data)
case 'set:cookies':
case 'add:cookies':
return invoke('setCookies', id, data)
case 'clear:cookies':
return invoke('clearCookies', id, data)
case 'clear:cookie':
return invoke('clearCookie', id, data)
case 'is:automation:client:connected':
return invoke('verify', id, data)
case 'focus:browser:window':
return invoke('focus', id)
case 'take:screenshot':
return invoke('takeScreenshot', id)
case 'reset:browser:state':
return invoke('resetBrowserState', id)
case 'reset:browser:tabs:for:next:test':
return invoke('resetBrowserTabsForNextTest', id)
default:
return fail(id, { message: `No handler registered for: '${msg}'` })
}
})
ws.on('automation:config', async (config) => {
const isFirefox = await checkIfFirefox()
listenToCookieChanges()
// Non-Firefox browsers use CDP for these instead
if (isFirefox) {
listenToDownloads()
listenToOnBeforeHeaders()
}
})
ws.on('connect', () => {
ws.emit('automation:client:connected')
})
return ws
}
const setOneCookie = (props) => {
// only get the url if its not already set
if (props.url == null) {
props.url = util.getCookieUrl(props)
}
if (props.hostOnly) {
// If the hostOnly prop is available, delete the domain.
// This will wind up setting a hostOnly cookie based on the calculated cookieURL above.
delete props.domain
}
if (props.domain === 'localhost') {
delete props.domain
}
props = pick(props, SET_PROPS)
return Promise.try(() => {
return browser.cookies.set(props)
})
}
const clearOneCookie = (cookie = {}) => {
const url = util.getCookieUrl(cookie)
const props = { url, name: cookie.name }
const throwError = function (err) {
throw (err != null ? err : new Error(`Removing cookie failed for: ${JSON.stringify(props)}`))
}
return Promise.try(() => {
if (!cookie.name) {
throw new Error(`Removing cookie failed for: ${JSON.stringify(cookie)}. Cookie did not include a name`)
}
return browser.cookies.remove(props)
}).then((details) => {
return cookie
}).catch(throwError)
}
const clearAllCookies = (cookies) => {
return Promise.mapSeries(cookies, clearOneCookie)
}
const automation = {
connect,
getAll (filter = {}) {
filter = pick(filter, GET_ALL_PROPS)
// Firefox's filtering doesn't match the behavior we want, so we do it
// ourselves. for example, getting { domain: example.com } cookies will
// return cookies for example.com and all subdomains, whereas we want an
// exact match for only "example.com".
return Promise.try(() => {
return browser.cookies.getAll({ url: filter.url })
.then((cookies) => {
return cookies.filter((cookie) => {
return cookieMatches(cookie, filter)
})
})
})
},
getCookies (filter, fn) {
return this.getAll(filter)
.then(fn)
},
getCookie (filter, fn) {
return this.getAll(filter)
.then(firstOrNull)
.then(fn)
},
setCookie (props = {}, fn) {
return setOneCookie(props)
.then(fn)
},
setCookies (propsArr = [], fn) {
return Promise.mapSeries(propsArr, setOneCookie)
.then(fn)
},
clearCookie (filter, fn) {
return this.getCookie(filter)
.then((cookie) => {
if (!cookie) return null
return clearOneCookie(cookie)
})
.then(fn)
},
clearCookies (cookies, fn) {
return clearAllCookies(cookies)
.then(fn)
},
focus (fn) {
// lets just make this simple and whatever is the current
// window bring that into focus
//
// TODO: if we REALLY want to be nice its possible we can
// figure out the exact window that's running Cypress but
// that's too much work with too little value at the moment
return Promise.try(() => {
return browser.windows.getCurrent()
}).then((window) => {
return browser.windows.update(window.id, { focused: true })
}).then(fn)
},
resetBrowserState (fn) {
// We remove browser data. Firefox goes through this path, while chrome goes through cdp automation
// Note that firefox does not support fileSystems or serverBoundCertificates
// (https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browsingData/DataTypeSet).
return browser.browsingData.remove({}, { cache: true, cookies: true, downloads: true, formData: true, history: true, indexedDB: true, localStorage: true, passwords: true, pluginData: true, serviceWorkers: true }).then(fn)
},
resetBrowserTabsForNextTest (fn) {
return Promise.try(() => {
return browser.windows.getCurrent({ populate: true })
}).then(async (windowInfo) => {
let newTabId = null
try {
// credit to https://stackoverflow.com/questions/7000190/detect-all-firefox-versions-in-js
// eslint-disable-next-line no-undef
const match = window.navigator.userAgent.match(/Firefox\/([0-9]+)\./)
const version = match ? parseInt(match[1]) : 0
// in versions of Firefox 124 and up, firefox no longer creates a new tab for us when we close all tabs in the browser.
// to keep change minimal and backwards compatible, we are creating an 'about:blank' tab here to keep the behavior consistent.
if (version >= 124) {
const newAboutBlankTab = await browser.tabs.create({ url: 'about:blank', active: false })
newTabId = newAboutBlankTab.id
}
// eslint-disable-next-line no-empty
} catch (e) {}
return browser.tabs.remove(windowInfo.tabs.map((tab) => tab.id).filter((tab) => tab.id !== newTabId))
}).then(fn)
},
query (data) {
const code = `var s; (s = document.getElementById('${data.element}')) && s.textContent`
const queryTab = (tab) => {
return Promise.try(() => {
return browser.tabs.executeScript(tab.id, { code })
}).then((results) => {
if (!results || (results[0] !== data.randomString)) {
throw new Error('Executed script did not return result')
}
})
}
return Promise.try(() => {
return browser.tabs.query({ windowType: 'normal' })
}).filter((tab) => {
// the tab's url must begin with
// http or https so that we filter out
// about:blank and chrome:// urls
// which will throw errors!
return httpRe.test(tab.url)
}).then((tabs) => {
// generate array of promises
return map(tabs, queryTab)
}).any()
},
verify (data, fn) {
return this.query(data)
.then(fn)
},
lastFocusedWindow () {
return Promise.try(() => {
return browser.windows.getLastFocused()
})
},
takeScreenshot (fn) {
return this.lastFocusedWindow()
.then((win) => {
return browser.tabs.captureVisibleTab(win.id, { format: 'png' })
})
.then(fn)
},
}
module.exports = automation