misc: replace marionette-client with geckodriver as b2g marionette client is no longer supported (#30250)

* misc: replace marionette-client with geckodriver as b2g marionette client is no longer supported [run ci]

* install pump [run ci]

* refactor to have geckodriver launch the browser and split out webdriver to own class [run ci]

fix other failing tests [run ci]

fix other failing tests [run ci]

pass env variables to firefox

* fix sigkill / treekill issues on windows with firefox binary being a dangling process [run ci]

* fix issue where browser in headed mode was not starting maximized [run ci]

* stub firefox_spec added deps different to get type inference

* add comment to geckodriver patch

* move capabilities to verbose debug statement

* update changelog

* address comments from code review

* add pending for changelog

* update with suggestions from code review

* remove debug enable as the process needs to be bound to stderr and stdout

* add comment on why we need to bind

* add comments from code review

* address comments from code review

* make sure sessionId is set
This commit is contained in:
Bill Glesias
2024-09-30 12:19:03 -04:00
committed by GitHub
parent 80e70b8988
commit af3839d990
29 changed files with 1893 additions and 539 deletions

View File

@@ -1,3 +1,3 @@
# Bump this version to force CI to re-create the cache from scratch.
09-12-24
09-24-24

View File

@@ -30,7 +30,7 @@ mainBuildFilters: &mainBuildFilters
- /^release\/\d+\.\d+\.\d+$/
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- 'update-v8-snapshot-cache-on-develop'
- 'ryanm/chore/fix-full-snapshot'
- 'misc/remove_marionette_for_geckodriver'
- 'publish-binary'
# usually we don't build Mac app - it takes a long time
@@ -42,7 +42,7 @@ macWorkflowFilters: &darwin-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'ryanm/chore/fix-full-snapshot', << pipeline.git.branch >> ]
- equal: [ 'misc/remove_marionette_for_geckodriver', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -53,7 +53,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'ryanm/chore/fix-full-snapshot', << pipeline.git.branch >> ]
- equal: [ 'misc/remove_marionette_for_geckodriver', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -76,7 +76,7 @@ windowsWorkflowFilters: &windows-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'ryanm/chore/fix-full-snapshot', << pipeline.git.branch >> ]
- equal: [ 'misc/remove_marionette_for_geckodriver', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -152,7 +152,7 @@ commands:
name: Set environment variable to determine whether or not to persist artifacts
command: |
echo "Setting SHOULD_PERSIST_ARTIFACTS variable"
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "ryanm/chore/fix-full-snapshot" ]]; then
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "misc/remove_marionette_for_geckodriver" ]]; then
export SHOULD_PERSIST_ARTIFACTS=true
fi' >> "$BASH_ENV"
# You must run `setup_should_persist_artifacts` command and be using bash before running this command

View File

@@ -1,4 +1,12 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 13.15.1
_Released 10/1/2024 (PENDING)_
**Misc:**
- Cypress now consumes [geckodriver](https://firefox-source-docs.mozilla.org/testing/geckodriver/index.html) to help automate the Firefox browser instead of [marionette-client](https://github.com/cypress-io/marionette-client). Addresses [#30217](https://github.com/cypress-io/cypress/issues/30217).
## 13.15.0
_Released 9/25/2024_

View File

@@ -11,6 +11,7 @@ const state = require('../tasks/state')
const xvfb = require('./xvfb')
const verify = require('../tasks/verify')
const errors = require('../errors')
const readline = require('readline')
const isXlibOrLibudevRe = /^(?:Xlib|libudev)/
const isHighSierraWarningRe = /\*\*\* WARNING/
@@ -236,6 +237,21 @@ module.exports = {
child.on('exit', resolveOn('exit'))
child.on('error', reject)
if (isPlatform('win32')) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
// on windows, SIGINT does not propagate to the child process when ctrl+c is pressed
// this makes sure all nested processes are closed(ex: firefox inside the server)
rl.on('SIGINT', function () {
let kill = require('tree-kill')
kill(child.pid, 'SIGINT')
})
}
// if stdio options is set to 'pipe', then
// we should set up pipes:
// process STDIN (read stream) => child STDIN (writeable)

View File

@@ -60,6 +60,7 @@
"semver": "^7.5.3",
"supports-color": "^8.1.1",
"tmp": "~0.2.3",
"tree-kill": "1.2.2",
"untildify": "^4.0.0",
"yauzl": "^2.10.0"
},

View File

@@ -7,6 +7,9 @@ const tty = require('tty')
const path = require('path')
const EE = require('events')
const mockedEnv = require('mocked-env')
const readline = require('readline')
const proxyquire = require('proxyquire')
const debug = require('debug')('test')
const state = require(`${lib}/tasks/state`)
@@ -22,6 +25,7 @@ const execPath = process.execPath
const nodeVersion = process.versions.node
const defaultBinaryDir = '/default/binary/dir'
let mockReadlineEE
describe('lib/exec/spawn', function () {
beforeEach(function () {
@@ -49,8 +53,11 @@ describe('lib/exec/spawn', function () {
// process.stdin is both an event emitter and a readable stream
this.processStdin = new EE()
mockReadlineEE = new EE()
this.processStdin.pipe = sinon.stub().returns(undefined)
sinon.stub(process, 'stdin').value(this.processStdin)
sinon.stub(readline, 'createInterface').returns(mockReadlineEE)
sinon.stub(cp, 'spawn').returns(this.spawnedProcess)
sinon.stub(xvfb, 'start').resolves()
sinon.stub(xvfb, 'stop').resolves()
@@ -387,6 +394,22 @@ describe('lib/exec/spawn', function () {
})
})
it('propagates treeKill if SIGINT is detected in windows console', async function () {
this.spawnedProcess.pid = 7
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
os.platform.returns('win32')
const treeKillMock = sinon.stub().returns(0)
const spawn = proxyquire(`${lib}/exec/spawn`, { 'tree-kill': treeKillMock })
await spawn.start([], { env: {} })
mockReadlineEE.emit('SIGINT')
expect(treeKillMock).to.have.been.calledWith(7, 'SIGINT')
})
it('does not set windowsHide property when in darwin', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)

View File

@@ -36,11 +36,11 @@
</head>
<body><pre><span style="color:#e05561">Cypress could not connect to Firefox.<span style="color:#e6e6e6">
<span style="color:#e05561"><span style="color:#e6e6e6">
<span style="color:#e05561">An unexpected error was received from Marionette: <span style="color:#de73ff">connection<span style="color:#e05561"><span style="color:#e6e6e6">
<span style="color:#e05561">An unexpected error was received from GeckoDriver: <span style="color:#de73ff">connection<span style="color:#e05561"><span style="color:#e6e6e6">
<span style="color:#e05561"><span style="color:#e6e6e6">
<span style="color:#e05561">To avoid this error, ensure that there are no other instances of Firefox launched by Cypress running.<span style="color:#e6e6e6">
<span style="color:#c062de"><span style="color:#e6e6e6">
<span style="color:#c062de">Error: fail whale<span style="color:#e6e6e6">
<span style="color:#c062de"> at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)<span style="color:#e6e6e6">
<span style="color:#c062de"> at FIREFOX_MARIONETTE_FAILURE (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)<span style="color:#e6e6e6"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
<span style="color:#c062de"> at FIREFOX_GECKODRIVER_FAILURE (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)<span style="color:#e6e6e6"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
</pre></body></html>

View File

@@ -1218,11 +1218,11 @@ export const AllCypressErrors = {
The error was: ${fmt.highlightSecondary(errMsg)}`
},
FIREFOX_MARIONETTE_FAILURE: (origin: string, err: Error) => {
FIREFOX_GECKODRIVER_FAILURE: (origin: string, err: Error) => {
return errTemplate`\
Cypress could not connect to Firefox.
An unexpected error was received from Marionette: ${fmt.highlightSecondary(origin)}
An unexpected error was received from GeckoDriver: ${fmt.highlightSecondary(origin)}
To avoid this error, ensure that there are no other instances of Firefox launched by Cypress running.

View File

@@ -1134,7 +1134,7 @@ describe('visual error templates', () => {
default: ['spec', '1', 'spec must be a string or comma-separated list'],
}
},
FIREFOX_MARIONETTE_FAILURE: () => {
FIREFOX_GECKODRIVER_FAILURE: () => {
const err = makeErr()
return {

View File

@@ -1203,7 +1203,7 @@ enum ErrorTypeEnum {
EXTENSION_NOT_LOADED
FIREFOX_COULD_NOT_CONNECT
FIREFOX_GC_INTERVAL_REMOVED
FIREFOX_MARIONETTE_FAILURE
FIREFOX_GECKODRIVER_FAILURE
FIXTURE_NOT_FOUND
FOLDER_NOT_WRITABLE
FREE_PLAN_EXCEEDS_MONTHLY_PRIVATE_TESTS

View File

@@ -9,6 +9,7 @@ export const debug = Debug('cypress:launcher:browsers')
/** starts a found browser and opens URL if given one */
export type LaunchedBrowser = cp.ChildProcessByStdio<null, Readable, Readable>
// NOTE: For Firefox, geckodriver is used to launch the browser
export function launch (
browser: FoundBrowser,
url: string,
@@ -28,7 +29,7 @@ export function launch (
const spawnOpts: cp.SpawnOptionsWithStdioTuple<cp.StdioNull, cp.StdioPipe, cp.StdioPipe> = {
stdio: ['ignore', 'pipe', 'pipe'],
// allow setting default env vars such as MOZ_HEADLESS_WIDTH
// allow setting default env vars
// but only if it's not already set by the environment
env: { ...browserEnv, ...process.env },
}

View File

@@ -216,7 +216,7 @@ export class BrowserCriClient {
*
* @param {BrowserCriClientCreateOptions} options the options for creating the browser cri client
* @param options.browserName the display name of the browser being launched
* @param options.fullyManageTabs whether or not to fully manage tabs. This is useful for firefox where some work is done with marionette and some with CDP. We don't want to handle disconnections in this class in those scenarios
* @param options.fullyManageTabs whether or not to fully manage tabs. This is useful for firefox where some work is done with GeckoDriver and some with CDP. We don't want to handle disconnections in this class in those scenarios
* @param options.hosts the hosts to which to attempt to connect
* @param options.onAsynchronousError callback for any cdp fatal errors
* @param options.onReconnect callback for when the browser cri client reconnects to the browser

View File

@@ -1,16 +1,14 @@
import Bluebird from 'bluebird'
import Debug from 'debug'
import _ from 'lodash'
import Marionette from 'marionette-client'
import { Command } from 'marionette-client/lib/marionette/message.js'
import util from 'util'
import Foxdriver from '@benmalka/foxdriver'
import * as protocol from './protocol'
import { CdpAutomation } from './cdp_automation'
import { BrowserCriClient } from './browser-cri-client'
import type { Automation } from '../automation'
const errors = require('../errors')
import type { CypressError } from '@packages/errors'
import type { WebDriverClassic } from './webdriver-classic'
const debug = Debug('cypress:server:browsers:firefox-util')
@@ -22,11 +20,7 @@ let timings = {
collections: [] as any[],
}
let driver
const sendMarionette = (data) => {
return driver.send(new Command(data))
}
let webDriverClassic: WebDriverClassic
const getTabId = (tab) => {
return _.get(tab, 'browsingContextID')
@@ -104,43 +98,40 @@ const attachToTabMemory = Bluebird.method((tab) => {
})
})
async function connectMarionetteToNewTab () {
async function connectToNewTabClassic () {
// Firefox keeps a blank tab open in versions of Firefox 123 and lower when the last tab is closed.
// For versions 124 and above, a new tab is not created, so @packages/extension creates one for us.
// Since the tab is always available on our behalf,
// we can connect to it here and navigate it to about:blank to set it up for CDP connection
const handles = await sendMarionette({
name: 'WebDriver:GetWindowHandles',
})
const handles = await webDriverClassic.getWindowHandles()
await sendMarionette({
name: 'WebDriver:SwitchToWindow',
parameters: { handle: handles[0] },
})
await webDriverClassic.switchToWindow(handles[0])
await navigateToUrl('about:blank')
await webDriverClassic.navigate('about:blank')
}
async function connectToNewSpec (options, automation: Automation, browserCriClient: BrowserCriClient) {
debug('firefox: reconnecting to blank tab')
await connectMarionetteToNewTab()
await connectToNewTabClassic()
debug('firefox: reconnecting CDP')
await browserCriClient.currentlyAttachedTarget?.close().catch(() => {})
const pageCriClient = await browserCriClient.attachToTargetUrl('about:blank')
if (browserCriClient) {
await browserCriClient.currentlyAttachedTarget?.close().catch(() => {})
const pageCriClient = await browserCriClient.attachToTargetUrl('about:blank')
await CdpAutomation.create(pageCriClient.send, pageCriClient.on, pageCriClient.off, browserCriClient.resetBrowserTargets, automation)
await CdpAutomation.create(pageCriClient.send, pageCriClient.on, pageCriClient.off, browserCriClient.resetBrowserTargets, automation)
}
await options.onInitializeNewBrowserTab()
debug(`firefox: navigating to ${options.url}`)
await navigateToUrl(options.url)
await navigateToUrlClassic(options.url)
}
async function setupRemote (remotePort, automation, onError): Promise<BrowserCriClient> {
const browserCriClient = await BrowserCriClient.create({ hosts: ['127.0.0.1', '::1'], port: remotePort, browserName: 'Firefox', onAsynchronousError: onError, onServiceWorkerClientEvent: automation.onServiceWorkerClientEvent })
async function setupCDP (remotePort: number, automation: Automation, onError?: (err: Error) => void): Promise<BrowserCriClient> {
const browserCriClient = await BrowserCriClient.create({ hosts: ['127.0.0.1', '::1'], port: remotePort, browserName: 'Firefox', onAsynchronousError: onError as (err: CypressError) => void, onServiceWorkerClientEvent: automation.onServiceWorkerClientEvent })
const pageCriClient = await browserCriClient.attachToTargetUrl('about:blank')
await CdpAutomation.create(pageCriClient.send, pageCriClient.on, pageCriClient.off, browserCriClient.resetBrowserTargets, automation)
@@ -148,11 +139,8 @@ async function setupRemote (remotePort, automation, onError): Promise<BrowserCri
return browserCriClient
}
async function navigateToUrl (url) {
await sendMarionette({
name: 'WebDriver:Navigate',
parameters: { url },
})
async function navigateToUrlClassic (url: string) {
await webDriverClassic.navigate(url)
}
const logGcDetails = () => {
@@ -219,28 +207,40 @@ export default {
return forceGcCc()
},
setup ({
async setup ({
automation,
extensions,
onError,
url,
marionettePort,
foxdriverPort,
remotePort,
}): Bluebird<BrowserCriClient> {
return Bluebird.all([
webDriverClassic: wdcInstance,
}: {
automation: Automation
onError?: (err: Error) => void
url: string
foxdriverPort: number
remotePort: number
webDriverClassic: WebDriverClassic
}): Promise<BrowserCriClient> {
// set the WebDriver classic instance instantiated from geckodriver
webDriverClassic = wdcInstance
const [, browserCriClient] = await Promise.all([
this.setupFoxdriver(foxdriverPort),
this.setupMarionette(extensions, url, marionettePort),
remotePort && setupRemote(remotePort, automation, onError),
]).then(([,, browserCriClient]) => navigateToUrl(url).then(() => browserCriClient))
setupCDP(remotePort, automation, onError),
])
await navigateToUrlClassic(url)
return browserCriClient
},
connectToNewSpec,
navigateToUrl,
navigateToUrlClassic,
setupRemote,
setupCDP,
// NOTE: this is going to be removed in Cypress 14. @see https://github.com/cypress-io/cypress/issues/30222
async setupFoxdriver (port) {
await protocol._connectAsync({
host: '127.0.0.1',
@@ -305,63 +305,4 @@ export default {
})
}
},
async setupMarionette (extensions, url, port) {
await protocol._connectAsync({
host: '127.0.0.1',
port,
getDelayMsForRetry,
})
driver = new Marionette.Drivers.Promises({
port,
tries: 1, // marionette-client has its own retry logic which we want to avoid
})
debug('firefox: navigating page with webdriver')
const onError = (from, reject?) => {
if (!reject) {
reject = (err) => {
throw err
}
}
return (err) => {
debug('error in marionette %o', { from, err })
reject(errors.get('FIREFOX_MARIONETTE_FAILURE', from, err))
}
}
await driver.connect()
.catch(onError('connection'))
await new Bluebird((resolve, reject) => {
const _onError = (from) => {
return onError(from, reject)
}
const { tcp } = driver
tcp.socket.on('error', _onError('Socket'))
tcp.client.on('error', _onError('CommandStream'))
sendMarionette({
name: 'WebDriver:NewSession',
parameters: { acceptInsecureCerts: true },
}).then(() => {
return Bluebird.all(_.map(extensions, (path) => {
return sendMarionette({
name: 'Addon:Install',
parameters: { path, temporary: true },
})
}))
})
.then(resolve)
.catch(_onError('commands'))
})
// even though Marionette is not used past this point, we have to keep the session open
// or else `acceptInsecureCerts` will cease to apply and SSL validation prompts will appear.
},
}

View File

@@ -4,25 +4,25 @@ import Debug from 'debug'
import getPort from 'get-port'
import path from 'path'
import urlUtil from 'url'
import { debug as launcherDebug, launch } from '@packages/launcher/lib/browsers'
import { debug as launcherDebug } from '@packages/launcher/lib/browsers'
import { doubleEscape } from '@packages/launcher/lib/windows'
import FirefoxProfile from 'firefox-profile'
import * as errors from '../errors'
import firefoxUtil from './firefox-util'
import utils from './utils'
import type { Browser, BrowserInstance, GracefulShutdownOptions } from './types'
import { EventEmitter } from 'events'
import os from 'os'
import treeKill from 'tree-kill'
import mimeDb from 'mime-db'
import { getRemoteDebuggingPort } from './protocol'
import type { BrowserCriClient } from './browser-cri-client'
import type { Automation } from '../automation'
import { getCtx } from '@packages/data-context'
import { getError } from '@packages/errors'
import type { BrowserLaunchOpts, BrowserNewTabOpts, RunModeVideoApi } from '@packages/types'
import { GeckoDriver } from './geckodriver'
import { WebDriverClassic } from './webdriver-classic'
const debug = Debug('cypress:server:browsers:firefox')
const debugVerbose = Debug('cypress-verbose:server:browsers:firefox')
// used to prevent the download prompt for the specified file types.
// this should cover most/all file types, but if it's necessary to
@@ -355,33 +355,14 @@ toolbar {
let browserCriClient: BrowserCriClient | undefined
export function _createDetachedInstance (browserInstance: BrowserInstance, browserCriClient?: BrowserCriClient): BrowserInstance {
const detachedInstance: BrowserInstance = new EventEmitter() as BrowserInstance
detachedInstance.pid = browserInstance.pid
// kill the entire process tree, from the spawned instance up
detachedInstance.kill = (): void => {
// Close browser cri client socket. Do nothing on failure here since we're shutting down anyway
if (browserCriClient) {
clearInstanceState({ gracefulShutdown: true })
}
treeKill(browserInstance.pid as number, (err?, result?) => {
debug('force-exit of process tree complete %o', { err, result })
detachedInstance.emit('exit')
})
}
return detachedInstance
}
/**
* Clear instance state for the chrome instance, this is normally called in on kill or on exit.
*/
export function clearInstanceState (options: GracefulShutdownOptions = {}) {
debug('closing remote interface client')
debug('clearing instance state')
if (browserCriClient) {
debug('closing remote interface client')
browserCriClient.close(options.gracefulShutdown).catch(() => {})
browserCriClient = undefined
}
@@ -406,15 +387,11 @@ async function recordVideo (videoApi: RunModeVideoApi) {
}
export async function open (browser: Browser, url: string, options: BrowserLaunchOpts, automation: Automation): Promise<BrowserInstance> {
// see revision comment here https://wiki.mozilla.org/index.php?title=WebDriver/RemoteProtocol&oldid=1234946
const hasCdp = browser.majorVersion >= 86
const defaultLaunchOptions = utils.getDefaultLaunchOptions({
extensions: [] as string[],
preferences: _.extend({}, defaultPreferences),
args: [
'-marionette',
'-new-instance',
'-foreground',
// if testing against older versions of Firefox to determine when a regression may have been introduced, uncomment the '-allow-downgrade' flag.
// '-allow-downgrade',
'-start-debugger-server', // uses the port+host defined in devtools.debugger.remote
@@ -422,20 +399,17 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
],
})
let remotePort
if (hasCdp) {
remotePort = await getRemoteDebuggingPort()
defaultLaunchOptions.args.push(`--remote-debugging-port=${remotePort}`)
}
if (browser.isHeadless) {
defaultLaunchOptions.args.push('-headless')
// we don't need to specify width/height since MOZ_HEADLESS_ env vars will be set
// and the browser will spawn maximized. The user may still supply these args to override
// defaultLaunchOptions.args.push('--width=1920')
// defaultLaunchOptions.args.push('--height=1081')
// defaultLaunchOptions.args.push('-width=1920')
// defaultLaunchOptions.args.push('-height=1081')
} else if (os.platform() === 'win32' || os.platform() === 'darwin') {
// lets the browser come into focus. Only works on Windows or Mac
// this argument is added automatically to the linux geckodriver,
// so adding it is unnecessary and actually causes the browser to fail to launch.
defaultLaunchOptions.args.push('-foreground')
}
debug('firefox open %o', options)
@@ -469,12 +443,16 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
const [
foxdriverPort,
marionettePort,
] = await Promise.all([getPort(), getPort()])
geckoDriverPort,
webDriverBiDiPort,
] = await Promise.all([getPort(), getPort(), getPort(), getPort()])
defaultLaunchOptions.preferences['devtools.debugger.remote-port'] = foxdriverPort
defaultLaunchOptions.preferences['marionette.port'] = marionettePort
debug('available ports: %o', { foxdriverPort, marionettePort })
// NOTE: we get the BiDi port and set it inside of geckodriver, but BiDi is not currently enabled (see remote.active-protocols above).
// this is so the BiDi websocket port does not get set to 0, which is the default for the geckodriver package.
debug('available ports: %o', { foxdriverPort, marionettePort, geckoDriverPort, webDriverBiDiPort })
const [
cacheDir,
@@ -501,18 +479,6 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
debug('firefox directories %o', { path: profile.path(), cacheDir, extensionDest })
const xulStorePath = path.join(profile.path(), 'xulstore.json')
// if user has set custom window.sizemode pref or it's the first time launching on this profile, write to xulStore.
if (!await fs.pathExists(xulStorePath)) {
// this causes the browser to launch maximized, which chrome does by default
// otherwise an arbitrary size will be picked for the window size
// this will not have an effect after first launch in 'interactive' mode
const sizemode = 'maximized'
await fs.writeJSON(xulStorePath, { 'chrome://browser/content/browser.xhtml': { 'main-window': { 'width': 1280, 'height': 1024, sizemode } } })
}
launchOptions.preferences['browser.cache.disk.parent_directory'] = cacheDir
for (const pref in launchOptions.preferences) {
const value = launchOptions.preferences[pref]
@@ -546,40 +512,115 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
await fs.writeFile(path.join(profileDir, 'chrome', 'userChrome.css'), userCss)
}
launchOptions.args = launchOptions.args.concat([
'-profile',
profile.path(),
])
// resolution of exactly 1280x720
// (height must account for firefox url bar, which we can only shrink to 1px ,
// and the total size of the window url and tab bar, which is 85 pixels for a total offset of 86 pixels)
const BROWSER_ENVS = {
MOZ_REMOTE_SETTINGS_DEVTOOLS: '1',
MOZ_HEADLESS_WIDTH: '1280',
MOZ_HEADLESS_HEIGHT: '806',
...launchOptions.env,
}
debug('launching geckodriver with browser envs %o', BROWSER_ENVS)
// create the geckodriver process, which we will use WebDriver Classic to open the browser
const geckoDriverInstance = await GeckoDriver.create({
host: '127.0.0.1',
port: geckoDriverPort,
marionetteHost: '127.0.0.1',
marionettePort,
webdriverBidiPort: webDriverBiDiPort,
profilePath: profile.path(),
binaryPath: browser.path,
// To pass env variables into the firefox process, we CANNOT do it through capabilities when starting the browser.
// Since geckodriver spawns the firefox process, we can pass the env variables directly to geckodriver, which in turn will
// pass them to the firefox process
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=1604723#c20 for more details
spawnOpts: {
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...BROWSER_ENVS,
...process.env,
},
},
})
const wdcInstance = new WebDriverClassic('127.0.0.1', geckoDriverPort)
debug('launch in firefox', { url, args: launchOptions.args })
const browserInstance = launch(browser, 'about:blank', remotePort, launchOptions.args, {
// sets headless resolution to 1280x720 by default
// user can overwrite this default with these env vars or --height, --width arguments
MOZ_HEADLESS_WIDTH: '1280',
MOZ_HEADLESS_HEIGHT: '721',
...launchOptions.env,
})
const capabilitiesToSend = {
capabilities: {
alwaysMatch: {
acceptInsecureCerts: true,
// @see https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions
'moz:firefoxOptions': {
binary: browser.path,
args: launchOptions.args,
prefs: launchOptions.preferences,
},
// @see https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html#moz-debuggeraddress
// we specify the debugger address option for Webdriver, which will return us the CDP address when the capability is returned.
'moz:debuggerAddress': true,
},
},
}
try {
browserCriClient = await firefoxUtil.setup({ automation, extensions: launchOptions.extensions, url, foxdriverPort, marionettePort, remotePort, onError: options.onError })
debugVerbose(`creating session with capabilities %s`, JSON.stringify(capabilitiesToSend.capabilities))
if (os.platform() === 'win32') {
// override the .kill method for Windows so that the detached Firefox process closes between specs
// @see https://github.com/cypress-io/cypress/issues/6392
return _createDetachedInstance(browserInstance, browserCriClient)
// this command starts the webdriver session and actually opens the browser
const { capabilities } = await wdcInstance.createSession(capabilitiesToSend)
debugVerbose(`received capabilities %o`, capabilities)
const cdpPort = parseInt(new URL(`ws://${capabilities['moz:debuggerAddress']}`).port)
debug(`CDP running on port ${cdpPort}`)
const browserPID = capabilities['moz:processID']
debug(`firefox running on pid: ${browserPID}`)
// makes it so get getRemoteDebuggingPort() is calculated correctly
process.env.CYPRESS_REMOTE_DEBUGGING_PORT = cdpPort.toString()
// maximize the window if running headful and no width or height args are provided.
// NOTE: We used to do this with xulstore.json, but this is no longer possible with geckodriver
// as firefox will create the profile under the profile root that we cannot control and we cannot consistently provide
// a base 64 encoded profile.
if (!browser.isHeadless && (!launchOptions.args.includes('-width') || !launchOptions.args.includes('-height'))) {
await wdcInstance.maximizeWindow()
}
// monkey-patch the .kill method to that the CDP connection is closed
const originalBrowserKill = browserInstance.kill
// install the browser extensions
await Promise.all(_.map(launchOptions.extensions, (path) => {
debug(`installing extension at path: ${path}`)
browserInstance.kill = (...args) => {
return wdcInstance!.installAddOn({
path,
temporary: true,
})
}))
debug('setting up firefox utils')
browserCriClient = await firefoxUtil.setup({ automation, url, foxdriverPort, webDriverClassic: wdcInstance, remotePort: cdpPort, onError: options.onError })
// monkey-patch the .kill method to that the CDP connection is closed
const originalGeckoDriverKill = geckoDriverInstance.kill
geckoDriverInstance.kill = (...args) => {
// Do nothing on failure here since we're shutting down anyway
clearInstanceState({ gracefulShutdown: true })
debug('closing firefox')
return originalBrowserKill.apply(browserInstance, args)
process.kill(browserPID)
debug('closing geckodriver')
return originalGeckoDriverKill.apply(geckoDriverInstance, args)
}
await utils.executeAfterBrowserLaunch(browser, {
@@ -589,7 +630,7 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
errors.throwErr('FIREFOX_COULD_NOT_CONNECT', err)
}
return browserInstance
return geckoDriverInstance
}
export async function closeExtraTargets () {

View File

@@ -0,0 +1,35 @@
# GeckoDriver
## Purpose
Cypress uses [GeckoDriver](https://firefox-source-docs.mozilla.org/testing/geckodriver/index.html) to drive [classic WebDriver](https://w3c.github.io/webdriver.) methods, as well as interface with Firefox's [Marionette Protocol](https://firefox-source-docs.mozilla.org/testing/marionette/Intro.html). This is necessary to automate the Firefox browser in the following cases:
* Navigating to the current/next spec URL via [WebDriver Classic](https://w3c.github.io/webdriver.).
* Installing the [Cypress web extension](https://github.com/cypress-io/cypress/tree/develop/packages/extension) via the [Marionette Protocol](https://firefox-source-docs.mozilla.org/testing/marionette/Intro.html), which is critical to automating Firefox.
Currently, [Chrome Devtools Protocol](https://chromedevtools.github.io/devtools-protocol/) automates most of our browser interactions with Firefox. However, [CDP will be removed towards the end of 2024](https://fxdx.dev/deprecating-cdp-support-in-firefox-embracing-the-future-with-webdriver-bidi/) now that [WebDriver BiDi](https://w3c.github.io/webdriver-bidi/) is fully supported in Firefox 130 and up. [GeckoDriver](https://firefox-source-docs.mozilla.org/testing/geckodriver/index.html) will be the entry point in which Cypress implements [WebDriver BiDi](https://w3c.github.io/webdriver-bidi/) for Firefox.
## Historical Context
Previously, Cypress was using an older package called the [marionette-client](https://github.com/cypress-io/marionette-client), which is near identical to the [mozilla b2g marionette client](https://github.com/mozilla-b2g/gaia/tree/master/tests/jsmarionette/client/marionette-client/lib/marionette). The b2g client hasn't had active development since 2014 and there have been changes to Marionette's server implementation since then. This means the [marionette-client](https://github.com/cypress-io/marionette-client) could break at any time, hence why we have migrated away from it. See [Cypress' migration to WebDriver BiDi within Firefox](https://bugzilla.mozilla.org/show_bug.cgi?id=1604723) bugzilla ticket for more details.
## Implementation
To consume [`GeckoDriver`](https://firefox-source-docs.mozilla.org/testing/geckodriver/index.html), Cypress installs the [`geckodriver`](https://github.com/webdriverio-community/node-geckodriver#readme) package, a lightweight wrapper around the [geckodriver binary](https://github.com/mozilla/geckodriver), to connect to the Firefox browser. Once connected, `GeckoDriver` is able to send `WebDriver` commands, as well as `Marionette` commands, to the Firefox browser. It is also capable of creating a `WebDriver BiDi` session to send `WebDriver BiDi` commands and receive `WebDriver BiDi` events.
It is worth noting that Cypress patches the [`geckodriver`](https://github.com/webdriverio-community/node-geckodriver#readme) package for a few reasons:
* To coincide with our debug logs in order to not print extraneous messages to the console that could disrupt end user experience as well as impact our system tests.
* Patch top-level awaits to correctly build the app.
* Pass process spawning arguments to the geckodriver process (which is a child process of the main process) in order to set `MOZ_` prefixed environment variables in the browser. These cannot be set through the [env capability](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions#env_object) and must be done through `geckodriver` as `geckodriver` spawns the `firefox` process. Please see [this comment](https://bugzilla.mozilla.org/show_bug.cgi?id=1604723#c20) for more details.
Currently, since the use of WebDriver Classic is so miniscule, the methods are implemented using direct fetch calls inside the [WebDriver Classic Class](../webdriver-classic/index.ts). It's important to note that, unlike Chrome, Firefox is launched via the WebDriver [newSession command](https://w3c.github.io/webdriver.#new-session) As BiDi development occurs and to reduce maintenance cost, using an already implemented client like [`webdriver`](https://www.npmjs.com/package/webdriver) will be explored.
Since we patch the [`geckodriver`](https://github.com/webdriverio-community/node-geckodriver#readme) package, we [`nohoist`](https://classic.yarnpkg.com/blog/2018/02/15/nohoist/) the dependency. This mostly works with sub-dependencies, but one of the dependencies, `pump@^3.0.0` (from `geckodriver` -> `tar-fs` -> `pump`) is missing from the binary. To workaround this, we install `pump` in `@packages/server` and `nohoist` the dependency so it is available in the binary as a production dependency.
## Debugging
To help debug `geckodriver` and `firefox`, the `DEBUG=cypress-verbose:server:browsers:geckodriver` can be used when launching `cypress`. This will
* Enable full `stdout` and `stderr` for `geckodriver` (including startup time) and the `firefox` process. The logs will **NOT** be truncated so they may get quite long.
* Enables the debugger terminal, which will additionally print browser information to the debugger terminal process.
If you are having trouble running firefox, turning on this debug option can be quite useful.

View File

@@ -0,0 +1,122 @@
import Bluebird from 'bluebird'
import debugModule from 'debug'
import errors from '@packages/errors'
import type { ChildProcess } from 'child_process'
const geckoDriverPackageName = 'geckodriver'
const GECKODRIVER_DEBUG_NAMESPACE = 'cypress:server:browsers:geckodriver'
const GECKODRIVER_DEBUG_NAMESPACE_VERBOSE = 'cypress-verbose:server:browsers:geckodriver'
const debug = debugModule(GECKODRIVER_DEBUG_NAMESPACE)
const debugVerbose = debugModule(GECKODRIVER_DEBUG_NAMESPACE_VERBOSE)
type SpawnOpt = { [key: string]: string | string[] | SpawnOpt }
export type StartGeckoDriverArgs = {
host: string
port: number
marionetteHost: string
marionettePort: number
webdriverBidiPort: number
profilePath: string
binaryPath: string
spawnOpts?: SpawnOpt
}
/**
* Class with static methods that serve as a wrapper around GeckoDriver
*/
export class GeckoDriver {
// We resolve this package in such a way that packherd can discover it, meaning we are re-declaring the types here to get typings support =(
// the only reason a static method is used here is so we can stub the class method more easily while under unit-test
private static getGeckoDriverPackage: () => {
start: (args: {
allowHosts?: string[]
allowOrigins?: string[]
binary?: string
connectExisting?: boolean
host?: string
jsdebugger?: boolean
log?: 'fatal' | 'error' | 'warn' | 'info' | 'config' | 'debug' | 'trace'
logNoTruncate?: boolean
marionetteHost?: string
marionettePort?: number
port?: number
websocketPort?: number
profileRoot?: string
geckoDriverVersion?: string
customGeckoDriverPath?: string
cacheDir?: string
spawnOpts: SpawnOpt
}) => Promise<ChildProcess>
download: (geckodriverVersion?: string, cacheDir?: string) => Promise<string>
} = () => {
/**
* NOTE: geckodriver is an ESM package and does not play well with mksnapshot.
* Requiring the package in this way, dynamically, will
* make it undiscoverable by mksnapshot
*/
return require(require.resolve(geckoDriverPackageName, { paths: [__dirname] }))
}
/**
* creates an instance of the GeckoDriver server. This is needed to start WebDriver
* @param {StartGeckoDriverArgs} opts - arguments needed to start GeckoDriver
* @returns {ChildProcess} - the child process in which the geckodriver is running
*/
static async create (opts: StartGeckoDriverArgs, timeout: number = 5000): Promise<ChildProcess> {
debug('no geckodriver instance exists. starting geckodriver...')
let geckoDriverChildProcess: ChildProcess | null = null
try {
const { start: startGeckoDriver } = GeckoDriver.getGeckoDriverPackage()
geckoDriverChildProcess = await startGeckoDriver({
host: opts.host,
port: opts.port,
marionetteHost: opts.marionetteHost,
marionettePort: opts.marionettePort,
websocketPort: opts.webdriverBidiPort,
profileRoot: opts.profilePath,
binary: opts.binaryPath,
jsdebugger: debugModule.enabled(GECKODRIVER_DEBUG_NAMESPACE_VERBOSE) || false,
log: debugModule.enabled(GECKODRIVER_DEBUG_NAMESPACE_VERBOSE) ? 'debug' : 'error',
logNoTruncate: debugModule.enabled(GECKODRIVER_DEBUG_NAMESPACE_VERBOSE),
spawnOpts: opts.spawnOpts || {},
})
// using a require statement to make this easier to test with mocha/mockery
const waitPort = require('wait-port')
await Bluebird.resolve(waitPort({
port: opts.port,
// add 1 second to the timeout so the timeout throws first if the limit is reached
timeout: timeout + 1000,
output: debugModule.enabled(GECKODRIVER_DEBUG_NAMESPACE_VERBOSE) ? 'dots' : 'silent',
})).timeout(timeout)
debug('geckodriver started!')
// For whatever reason, we NEED to bind to stderr/stdout in order
// for the geckodriver process not to hang, even though the event effectively
// isn't doing anything without debug logs enabled.
geckoDriverChildProcess.stdout?.on('data', (buf) => {
debugVerbose('firefox stdout: %s', String(buf).trim())
})
geckoDriverChildProcess.stderr?.on('data', (buf) => {
debugVerbose('firefox stderr: %s', String(buf).trim())
})
geckoDriverChildProcess.on('exit', (code, signal) => {
debugVerbose('firefox exited: %o', { code, signal })
})
return geckoDriverChildProcess
} catch (err) {
geckoDriverChildProcess?.kill()
debug(`geckodriver failed to start from 'geckodriver:start' for reason: ${err}`)
throw errors.get('FIREFOX_GECKODRIVER_FAILURE', 'geckodriver:start', err)
}
}
}

View File

@@ -0,0 +1,263 @@
import debugModule from 'debug'
// using cross fetch to make unit testing easier to mock
import crossFetch from 'cross-fetch'
const debug = debugModule('cypress:server:browsers:webdriver')
type InstallAddOnArgs = {
path: string
temporary: boolean
}
namespace WebDriver {
export namespace Session {
export type NewResult = {
capabilities: {
acceptInsecureCerts: boolean
browserName: string
browserVersion: string
platformName: string
pageLoadStrategy: 'normal'
strictFileInteractability: boolean
timeouts: {
implicit: number
pageLoad: number
script: number
}
'moz:accessibilityChecks': boolean
'moz:buildID': string
'moz:geckodriverVersion': string
'moz:debuggerAddress': string
'moz:headless': boolean
'moz:platformVersion': string
'moz:processID': number
'moz:profile': string
'moz:shutdownTimeout': number
'moz:webdriverClick': boolean
'moz:windowless': boolean
unhandledPromptBehavior: string
userAgent: string
sessionId: string
}
}
}
}
export class WebDriverClassic {
#host: string
#port: number
private sessionId: string = ''
constructor (host: string, port: number) {
this.#host = host
this.#port = port
}
/**
* Creates a new WebDriver Session through GeckoDriver. Capabilities are predetermined
* @see https://w3c.github.io/webdriver.#new-session
* @returns {Promise<WebDriver.Session.NewResult>} - the results of the Webdriver Session (enabled through remote.active-protocols)
*/
async createSession (args: {
capabilities: {[key: string]: any}
}): Promise<WebDriver.Session.NewResult> {
const getSessionUrl = `http://${this.#host}:${this.#port}/session`
const body = {
capabilities: args.capabilities,
}
try {
const createSessionResp = await crossFetch(getSessionUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!createSessionResp.ok) {
const error = new Error(`${createSessionResp.status}: ${createSessionResp.statusText}.`)
try {
const resp = await createSessionResp.json()
error.message = `${error.message } ${resp.value.error}. ${resp.value.message}.`
} finally {
// if for some reason we can't parse the response, continue to throw with some information.
throw error
}
}
const createSessionRespBody = await createSessionResp.json()
this.sessionId = createSessionRespBody.value.sessionId
return createSessionRespBody.value
} catch (e) {
debug(`unable to create new Webdriver session: ${e}`)
throw e
}
}
/**
* Gets available windows handles in the browser. The order in which the window handles are returned is arbitrary.
* @see https://w3c.github.io/webdriver.#get-window-handles
*
* @returns {Promise<string[]>} All the available top-level contexts/handles
*/
async getWindowHandles (): Promise<string[]> {
const getWindowHandles = `http://${this.#host}:${this.#port}/session/${this.sessionId}/window/handles`
try {
const getWindowHandlesResp = await crossFetch(getWindowHandles)
if (!getWindowHandlesResp.ok) {
throw new Error(`${getWindowHandlesResp.status}: ${getWindowHandlesResp.statusText}`)
}
const getWindowHandlesRespBody = await getWindowHandlesResp.json()
return getWindowHandlesRespBody.value
} catch (e) {
debug(`unable to get classic webdriver window handles: ${e}`)
throw e
}
}
/**
* Switching windows will select the session's current top-level browsing context as the target for all subsequent commands.
* In a tabbed browser, this will typically make the tab containing the browsing context the selected tab.
* @see https://w3c.github.io/webdriver.#dfn-switch-to-window
*
* @param {string} handle - the context ID of the window handle
* @returns {Promise<null>}
*/
async switchToWindow (handle: string): Promise<null> {
const switchToWindowUrl = `http://${this.#host}:${this.#port}/session/${this.sessionId}/window`
const body = {
handle,
}
try {
const switchToWindowResp = await crossFetch(switchToWindowUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!switchToWindowResp.ok) {
throw new Error(`${switchToWindowResp.status}: ${switchToWindowResp.statusText}`)
}
const switchToWindowRespBody = await switchToWindowResp.json()
return switchToWindowRespBody.value
} catch (e) {
debug(`unable to switch to window via classic webdriver : ${e}`)
throw e
}
}
/**
* maximizes the current window
* @see https://w3c.github.io/webdriver.#maximize-window
*
* @returns {Promise<null>}
*/
async maximizeWindow (): Promise<null> {
const maximizeWindowUrl = `http://${this.#host}:${this.#port}/session/${this.sessionId}/window/maximize`
try {
const maximizeWindowResp = await crossFetch(maximizeWindowUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
})
if (!maximizeWindowResp.ok) {
throw new Error(`${maximizeWindowResp.status}: ${maximizeWindowResp.statusText}`)
}
const maximizeWindowRespBody = await maximizeWindowResp.json()
return maximizeWindowRespBody.value
} catch (e) {
debug(`unable to maximize window via classic webdriver : ${e}`)
throw e
}
}
/**
* causes the user agent to navigate the session's current top-level browsing context to a new location.
* @see https://w3c.github.io/webdriver.#navigate-to
*
* @param url - the url of where the context handle is navigating to
* @returns {Promise<null>}
*/
async navigate (url: string): Promise<null> {
const navigateUrl = `http://${this.#host}:${this.#port}/session/${this.sessionId}/url`
const body = {
url,
}
try {
const navigateResp = await crossFetch(navigateUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!navigateResp.ok) {
throw new Error(`${navigateResp.status}: ${navigateResp.statusText}`)
}
const navigateRespBody = await navigateResp.json()
return navigateRespBody.value
} catch (e) {
debug(`unable to navigate via classic webdriver : ${e}`)
throw e
}
}
/**
* Installs a web extension on the given WebDriver session
* @see https://searchfox.org/mozilla-central/rev/cc01f11adfacca9cd44a75fd140d2fdd8f9a48d4/testing/geckodriver/src/command.rs#33-36
* @param {InstallAddOnArgs} opts - options needed to install a web extension.
*/
async installAddOn (opts: InstallAddOnArgs) {
const body = {
path: opts.path,
temporary: opts.temporary,
}
// If the webdriver session is created, we can now install our extension through geckodriver
const url = `http://${this.#host}:${this.#port}/session/${this.sessionId}/moz/addon/install`
try {
const resp = await crossFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!resp.ok) {
throw new Error(`${resp.status}: ${resp.statusText}`)
}
} catch (e) {
debug(`unable to install extension: ${e}`)
throw e
}
}
}

View File

@@ -71,6 +71,7 @@
"firefox-profile": "4.6.0",
"fluent-ffmpeg": "2.1.2",
"fs-extra": "9.1.0",
"geckodriver": "4.4.2",
"get-port": "5.1.1",
"getos": "3.2.1",
"glob": "7.1.3",
@@ -89,7 +90,6 @@
"lockfile": "1.0.4",
"lodash": "^4.17.21",
"log-symbols": "2.2.0",
"marionette-client": "https://github.com/cypress-io/marionette-client.git#5fc10cdf6c02627e9a2add98ca52de4d0c2fe74d",
"md5": "2.3.0",
"memfs": "3.5.3",
"mime": "2.6.0",
@@ -107,6 +107,7 @@
"pidusage": "3.0.2",
"pluralize": "8.0.0",
"pretty-bytes": "^5.6.0",
"pump": "^3.0.2",
"randomstring": "1.3.0",
"recast": "0.20.4",
"resolve": "1.17.0",
@@ -124,12 +125,12 @@
"through": "2.3.8",
"tough-cookie": "4.1.3",
"trash": "5.2.0",
"tree-kill": "1.2.2",
"ts-node": "^10.9.2",
"tslib": "2.3.1",
"underscore.string": "3.3.6",
"url-parse": "1.5.10",
"uuid": "8.3.2",
"wait-port": "1.1.0",
"webpack-virtual-modules": "0.5.0",
"widest-line": "3.1.0"
},
@@ -216,7 +217,9 @@
"nohoist": [
"@benmalka/foxdriver",
"devtools-protocol",
"geckodriver",
"http-proxy",
"pump",
"tsconfig-paths"
]
},

View File

@@ -0,0 +1,311 @@
diff --git a/node_modules/geckodriver/README.md b/node_modules/geckodriver/README.md
deleted file mode 100644
index 8634875..0000000
--- a/node_modules/geckodriver/README.md
+++ /dev/null
@@ -1,230 +0,0 @@
-Geckodriver [![CI](https://github.com/webdriverio-community/node-geckodriver/actions/workflows/ci.yml/badge.svg)](https://github.com/webdriverio-community/node-geckodriver/actions/workflows/ci.yml) [![Audit](https://github.com/webdriverio-community/node-geckodriver/actions/workflows/audit.yml/badge.svg)](https://github.com/webdriverio-community/node-geckodriver/actions/workflows/audit.yml)
-==========
-
-An NPM wrapper for Mozilla's [Geckodriver](https://github.com/mozilla/geckodriver). It manages to download various (or the latest) Geckodriver versions and provides a programmatic interface to start and stop it within Node.js. __Note:__ this is a wrapper module. If you discover any bugs with Geckodriver, please report them in the [official repository](https://github.com/mozilla/geckodriver).
-
-# Installing
-
-You can install this package via:
-
-```sh
-npm install geckodriver
-```
-
-Or install it globally:
-
-```sh
-npm install -g geckodriver
-```
-
-__Note:__ This installs a `geckodriver` shell script that runs the executable, but on Windows, [`selenium-webdriver`](https://www.npmjs.com/package/selenium-webdriver) looks for `geckodriver.exe`. To use a global installation of this package with [`selenium-webdriver`](https://www.npmjs.com/package/selenium-webdriver) on Windows, copy or link `geckodriver.exe` to a location on your `PATH` (such as the NPM bin directory) after installing this package:
-
-```sh
-mklink %USERPROFILE%\AppData\Roaming\npm\geckodriver.exe %USERPROFILE%\AppData\Roaming\npm\node_modules\geckodriver\geckodriver.exe
-```
-
-Once installed you can start Geckodriver via:
-
-```sh
-npx geckodriver --port=4444
-```
-
-By default, this package downloads Geckodriver when used for the first time through the CLI or the programmatic interface. If you like to download it as part of the NPM install process, set the `GECKODRIVER_AUTO_INSTALL` environment flag, e.g.:
-
-```sh
-GECKODRIVER_AUTO_INSTALL=1 npm i
-```
-
-To get a list of available CLI options run `npx geckodriver --help`. By default, this package downloads the latest version of the driver. If you prefer to have it install a custom Geckodriver version you can define the environment variable `GECKODRIVER_VERSION` when running in CLI, e.g.:
-
-```sh
-$ npm i geckodriver
-$ GECKODRIVER_VERSION="0.31.0" npx geckodriver --version
-geckodriver 0.31.0 (b617178ef491 2022-04-06 11:57 +0000)
-
-The source code of this program is available from
-testing/geckodriver in https://hg.mozilla.org/mozilla-central.
-
-This program is subject to the terms of the Mozilla Public License 2.0.
-You can obtain a copy of the license at https://mozilla.org/MPL/2.0/.
-```
-
-## Setting a CDN URL for binary download
-
-To set an alternate CDN location for Geckodriver binaries, set the `GECKODRIVER_CDNURL` like this:
-
-```sh
-GECKODRIVER_CDNURL=https://INTERNAL_CDN/geckodriver/download
-```
-
-Binaries on your CDN should be located in a subdirectory of the above base URL. For example, `/vxx.xx.xx/*.tar.gz` should be located under `/geckodriver/download` above.
-
-Alternatively, you can add the same property to your .npmrc file.
-
-The default location is set to https://github.com/mozilla/geckodriver/releases/download
-
-## Setting a PROXY URL
-
-Use `HTTPS_PROXY` or `HTTP_PROXY` to set your proxy URL.
-
-# Programmatic Interface
-
-You can import this package with Node.js and start the driver as part of your script and use it e.g. with [WebdriverIO](https://webdriver.io).
-
-## Exported Methods
-
-The package exports a `start` and `download` method.
-
-### `start`
-
-Starts a Geckodriver instance and returns a [`ChildProcess`](https://nodejs.org/api/child_process.html#class-childprocess). If Geckodriver is not downloaded it will download it for you.
-
-__Params:__ `GeckodriverParameters` - options to pass into Geckodriver (see below)
-
-__Example:__
-
-```js
-import { start } from 'geckodriver';
-import { remote } from 'webdriverio';
-import waitPort from 'wait-port';
-
-/**
- * first start Geckodriver
- */
-const cp = await start({ port: 4444 });
-
-/**
- * wait for Geckodriver to be up
- */
-await waitPort({ port: 4444 });
-
-/**
- * then start WebdriverIO session
- */
-const browser = await remote({ capabilities: { browserName: 'firefox' } });
-await browser.url('https://webdriver.io');
-console.log(await browser.getTitle()); // prints "WebdriverIO · Next-gen browser and mobile automation test framework for Node.js | WebdriverIO"
-
-/**
- * kill Geckodriver process
- */
-cp.kill();
-```
-
-__Note:__ as you can see in the example above this package does not wait for the driver to be up, you have to manage this yourself through packages like [`wait-on`](https://github.com/jeffbski/wait-on).
-
-### `download`
-
-Method to download a Geckodriver with a particular version. If a version parameter is omitted it tries to download the latest available version of the driver.
-
-__Params:__ `string` - version of Geckodriver to download (optional)
-
-## CJS Support
-
-In case your module uses CJS you can use this package as follows:
-
-```js
-const { start } = require('geckodriver')
-// see example above
-```
-
-## Options
-
-The `start` method offers the following options to be passed on to the actual Geckodriver CLI.
-
-### `allowHosts`
-
-List of host names to allow. By default, the value of --host is allowed, and in addition, if that's a well-known local address, other variations on well-known local addresses are allowed. If --allow-hosts is provided only exactly those hosts are allowed.
-
-Type: `string[]`<br />
-Default: `[]`
-
-### `allowOrigins`
-List of request origins to allow. These must be formatted as `scheme://host:port`. By default, any request with an origin header is rejected. If `--allow-origins` is provided then only exactly those origins are allowed.
-
-Type: `string[]`<br />
-Default: `[]`
-
-### `binary`
-Path to the Firefox binary.
-
-Type: `string`
-
-### `connectExisting`
-Connect to an existing Firefox instance.
-
-Type: `boolean`<br />
-Default: `false`
-
-### `host`
-Host IP to use for WebDriver server.
-
-Type: `string`<br />
-Default: `0.0.0.0`
-
-### `jsdebugger`
-Attach browser toolbox debugger for Firefox.
-
-Type: `boolean`<br />
-Default: `false`
-
-### `log`
-Set Gecko log level [possible values: `fatal`, `error`, `warn`, `info`, `config`, `debug`, `trace`].
-
-Type: `string`
-
-### `logNoTruncated`
-Write server log to file instead of stderr, increases log level to `INFO`.
-
-Type: `boolean`
-
-### `marionetteHost`
-Host to use to connect to Gecko.
-
-Type: `boolean`<br />
-Default: `127.0.0.1`
-
-### `marionettePort`
-Port to use to connect to Gecko.
-
-Type: `number`<br />
-Default: `0`
-
-### `port`
-Port to listen on.
-
-Type: `number`
-
-### `profileRoot`
-Directory in which to create profiles. Defaults to the system temporary directory.
-
-Type: `string`
-
-### `geckoDriverVersion`
-A version of Geckodriver to start. See https://github.com/mozilla/geckodriver/releases for all available versions, platforms and architecture.
-
-Type: `string`
-
-### `customGeckoDriverPath`
-Don't download Geckodriver, instead use a custom path to it, e.g. a cached binary.
-
-Type: `string`<br />
-Default: `process.env.GECKODRIVER_PATH`
-
-### `cacheDir`
-The path to the root of the cache directory.
-
-Type: `string`<br />
-Default: `process.env.GECKODRIVER_CACHE_DIR || os.tmpdir()`
-
-# Other Browser Driver
-
-If you also look for other browser driver NPM wrappers, you can find them here:
-
-- Chrome: [giggio/node-chromedriver](https://github.com/giggio/node-chromedriver)
-- Microsoft Edge: [webdriverio-community/node-edgedriver](https://github.com/webdriverio-community/node-edgedriver)
-- Safari: [webdriverio-community/node-safaridriver](https://github.com/webdriverio-community/node-safaridriver)
-
----
-
-For more information on WebdriverIO see the [homepage](https://webdriver.io).
diff --git a/node_modules/geckodriver/dist/index.js b/node_modules/geckodriver/dist/index.js
index 2367e0b..18be0e1 100644
--- a/node_modules/geckodriver/dist/index.js
+++ b/node_modules/geckodriver/dist/index.js
@@ -1,11 +1,11 @@
import cp from 'node:child_process';
-import logger from '@wdio/logger';
+import debugModule from 'debug';
import { download as downloadDriver } from './install.js';
import { hasAccess, parseParams } from './utils.js';
import { DEFAULT_HOSTNAME } from './constants.js';
-const log = logger('geckodriver');
+const debug = debugModule('cypress-verbose:server:browsers:geckodriver');
export async function start(params) {
- const { cacheDir, customGeckoDriverPath, ...startArgs } = params;
+ const { cacheDir, customGeckoDriverPath, spawnOpts, ...startArgs } = params;
let geckoDriverPath = (customGeckoDriverPath ||
process.env.GECKODRIVER_PATH ||
// deprecated
@@ -23,8 +23,8 @@ export async function start(params) {
// Otherwise all instances try to connect to the default port and fail
startArgs.websocketPort = startArgs.websocketPort ?? 0;
const args = parseParams(startArgs);
- log.info(`Starting Geckodriver at ${geckoDriverPath} with params: ${args.join(' ')}`);
- return cp.spawn(geckoDriverPath, args);
+ debug(`Starting Geckodriver at ${geckoDriverPath} with params: ${args.join(' ')}`);
+ return cp.spawn(geckoDriverPath, args, spawnOpts);
}
export const download = downloadDriver;
export * from './types.js';
diff --git a/node_modules/geckodriver/dist/install.js b/node_modules/geckodriver/dist/install.js
index c27c805..d230983 100644
--- a/node_modules/geckodriver/dist/install.js
+++ b/node_modules/geckodriver/dist/install.js
@@ -4,14 +4,14 @@ import util from 'node:util';
import stream from 'node:stream';
import fsp, { writeFile } from 'node:fs/promises';
import zlib from 'node:zlib';
-import logger from '@wdio/logger';
+import debugModule from 'debug';
import tar from 'tar-fs';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { HttpProxyAgent } from 'http-proxy-agent';
import { BINARY_FILE, GECKODRIVER_CARGO_YAML } from './constants.js';
import { hasAccess, getDownloadUrl, retryFetch } from './utils.js';
import { BlobReader, BlobWriter, ZipReader } from '@zip.js/zip.js';
-const log = logger('geckodriver');
+const debug = debugModule('cypress-verbose:server:browsers:geckodriver');
const streamPipeline = util.promisify(stream.pipeline);
const fetchOpts = {};
if (process.env.HTTPS_PROXY) {
@@ -36,10 +36,10 @@ export async function download(geckodriverVersion = process.env.GECKODRIVER_VERS
throw new Error(`Couldn't find version property in Cargo.toml file: ${JSON.stringify(toml)}`);
}
geckodriverVersion = version.split(' = ').pop().slice(1, -1);
- log.info(`Detected Geckodriver v${geckodriverVersion} to be latest`);
+ debug(`Detected Geckodriver v${geckodriverVersion} to be latest`);
}
const url = getDownloadUrl(geckodriverVersion);
- log.info(`Downloading Geckodriver from ${url}`);
+ debug(`Downloading Geckodriver from ${url}`);
const res = await retryFetch(url, fetchOpts);
if (res.status !== 200) {
throw new Error(`Failed to download binary (statusCode ${res.status}): ${res.statusText}`);
@@ -70,6 +70,8 @@ async function downloadZip(res, cacheDir) {
* download on install
*/
if (process.argv[1] && process.argv[1].endsWith('/dist/install.js') && process.env.GECKODRIVER_AUTO_INSTALL) {
- await download().then(() => log.info('Success!'), (err) => log.error(`Failed to install Geckodriver: ${err.stack}`));
+ // removing the await here as packherd cannot bundle with a top-level await.
+ // This only has an impact if invoking from a CLI context, which cypress is not.
+ download().then(() => debug('Success!'), (err) => debug(`Failed to install Geckodriver: ${err.stack}`));
}
//# sourceMappingURL=install.js.map
\ No newline at end of file

View File

@@ -1,18 +1,16 @@
require('../../spec_helper')
import 'chai-as-promised'
import { expect } from 'chai'
import { EventEmitter } from 'events'
import Marionette from 'marionette-client'
import os from 'os'
import sinon from 'sinon'
import stripAnsi from 'strip-ansi'
import Foxdriver from '@benmalka/foxdriver'
import * as firefox from '../../../lib/browsers/firefox'
import firefoxUtil from '../../../lib/browsers/firefox-util'
import { CdpAutomation } from '../../../lib/browsers/cdp_automation'
import { BrowserCriClient } from '../../../lib/browsers/browser-cri-client'
import { ICriClient } from '../../../lib/browsers/cri-client'
import { GeckoDriver } from '../../../lib/browsers/geckodriver'
import * as webDriverClassicImport from '../../../lib/browsers/webdriver-classic'
const path = require('path')
const _ = require('lodash')
@@ -26,39 +24,9 @@ const specUtil = require('../../specUtils')
describe('lib/browsers/firefox', () => {
const port = 3333
let marionetteDriver: any
let marionetteSendCb: any
let foxdriver: any
let foxdriverTab: any
const stubMarionette = () => {
marionetteSendCb = null
const connect = sinon.stub()
connect.resolves()
const send = sinon.stub().callsFake((opts) => {
if (marionetteSendCb) {
return marionetteSendCb(opts)
}
return Promise.resolve()
})
const close = sinon.stub()
const socket = new EventEmitter()
const client = new EventEmitter()
const tcp = { socket, client }
marionetteDriver = {
tcp, connect, send, close,
}
sinon.stub(Marionette.Drivers, 'Promises').returns(marionetteDriver)
}
let wdcInstance: sinon.SinonStubbedInstance<webDriverClassicImport.WebDriverClassic>
const stubFoxdriver = () => {
foxdriverTab = {
@@ -90,7 +58,7 @@ describe('lib/browsers/firefox', () => {
return mockfs.restore()
})
beforeEach(() => {
beforeEach(function () {
sinon.stub(utils, 'getProfileDir').returns('/path/to/appData/firefox-stable/interactive')
mockfs({
@@ -99,31 +67,50 @@ describe('lib/browsers/firefox', () => {
sinon.stub(protocol, '_connectAsync').resolves(null)
stubMarionette()
this.browserInstance = {
// should be high enough to not kill any real PIDs
pid: Number.MAX_SAFE_INTEGER,
}
sinon.stub(GeckoDriver, 'create').resolves(this.browserInstance)
wdcInstance = sinon.createStubInstance(webDriverClassicImport.WebDriverClassic)
wdcInstance.createSession.resolves({
capabilities: {
'moz:debuggerAddress': '127.0.0.1:12345',
acceptInsecureCerts: false,
browserName: '',
browserVersion: '',
platformName: '',
pageLoadStrategy: 'normal',
strictFileInteractability: false,
timeouts: {
implicit: 0,
pageLoad: 0,
script: 0,
},
'moz:accessibilityChecks': false,
'moz:buildID': '',
'moz:geckodriverVersion': '',
'moz:headless': false,
'moz:platformVersion': '',
'moz:processID': 0,
'moz:profile': '',
'moz:shutdownTimeout': 0,
'moz:webdriverClick': false,
'moz:windowless': false,
unhandledPromptBehavior: '',
userAgent: '',
sessionId: '',
},
})
sinon.stub(webDriverClassicImport, 'WebDriverClassic').callsFake(() => wdcInstance)
stubFoxdriver()
})
context('#connectToNewSpec', () => {
beforeEach(function () {
this.browser = { name: 'firefox', channel: 'stable' }
this.automation = {
use: sinon.stub().returns({}),
}
this.options = {
onError: () => {},
}
})
it('calls connectToNewSpec in firefoxUtil', function () {
sinon.stub(firefoxUtil, 'connectToNewSpec').withArgs(50505, this.options, this.automation).resolves()
firefox.connectToNewSpec(this.browser, this.options, this.automation)
expect(firefoxUtil.connectToNewSpec).to.be.called
})
})
context('#open', () => {
beforeEach(function () {
// majorVersion >= 86 indicates CDP support for Firefox, which provides
@@ -139,11 +126,6 @@ describe('lib/browsers/firefox', () => {
browser: this.browser,
}
this.browserInstance = {
// should be high enough to not kill any real PIDs
pid: Number.MAX_SAFE_INTEGER,
}
sinon.stub(process, 'pid').value(1111)
protocol.foo = 'bar'
@@ -167,6 +149,30 @@ describe('lib/browsers/firefox', () => {
sinon.stub(CdpAutomation, 'create').resolves()
})
context('#connectToNewSpec', () => {
beforeEach(function () {
this.options.onError = () => {}
this.options.onInitializeNewBrowserTab = sinon.stub()
})
it('calls connectToNewSpec in firefoxUtil', async function () {
wdcInstance.getWindowHandles.resolves(['mock-context-id'])
await firefox.open(this.browser, 'http://', this.options, this.automation)
this.options.url = 'next-spec-url'
await firefox.connectToNewSpec(this.browser, this.options, this.automation)
expect(this.options.onInitializeNewBrowserTab).to.have.been.called
expect(wdcInstance.getWindowHandles).to.have.been.called
expect(wdcInstance.switchToWindow).to.have.been.calledWith('mock-context-id')
// first time when connecting a new tab
expect(wdcInstance.navigate).to.have.been.calledWith('about:blank')
// second time when navigating to the spec
expect(wdcInstance.navigate).to.have.been.calledWith('next-spec-url')
})
})
it('executes before:browser:launch if registered', function () {
plugins.has.withArgs('before:browser:launch').returns(true)
plugins.execute.resolves(null)
@@ -215,29 +221,83 @@ describe('lib/browsers/firefox', () => {
})
})
it('adds extensions returned by before:browser:launch, along with cypress extension', function () {
plugins.has.withArgs('before:browser:launch').returns(true)
plugins.execute.resolves({
extensions: ['/path/to/user/ext'],
})
it('creates the geckodriver, the creation of the WebDriver session, installs the extension, and passes the correct port to CDP', function () {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(marionetteDriver.send).calledWithMatch({ name: 'Addon:Install', params: { path: '/path/to/ext' } })
expect(GeckoDriver.create).to.have.been.calledWith({
host: '127.0.0.1',
port: sinon.match(Number),
marionetteHost: '127.0.0.1',
marionettePort: sinon.match(Number),
webdriverBidiPort: sinon.match(Number),
profilePath: '/path/to/appData/firefox-stable/interactive',
binaryPath: undefined,
spawnOpts: sinon.match({
stdio: ['ignore', 'pipe', 'pipe'],
env: {
MOZ_REMOTE_SETTINGS_DEVTOOLS: '1',
MOZ_HEADLESS_WIDTH: '1280',
MOZ_HEADLESS_HEIGHT: '806',
},
}),
})
expect(marionetteDriver.send).calledWithMatch({ name: 'Addon:Install', params: { path: '/path/to/user/ext' } })
expect(wdcInstance.createSession).to.have.been.calledWith(sinon.match(
{
capabilities: {
alwaysMatch: {
acceptInsecureCerts: true,
'moz:firefoxOptions': {
args: [
'-new-instance',
'-start-debugger-server',
'-no-remote',
...(os.platform() !== 'linux' ? ['-foreground'] : []),
],
},
'moz:debuggerAddress': true,
},
},
},
))
expect(wdcInstance.installAddOn).to.have.been.calledWith(sinon.match({
path: '/path/to/ext',
temporary: true,
}))
expect(wdcInstance.navigate).to.have.been.calledWith('http://')
// make sure CDP gets the expected port
expect(BrowserCriClient.create).to.be.calledWith({ hosts: ['127.0.0.1', '::1'], port: 12345, browserName: 'Firefox', onAsynchronousError: undefined, onServiceWorkerClientEvent: undefined })
})
})
it('adds only cypress extension if before:browser:launch returns object with non-array extensions', function () {
plugins.has.withArgs('before:browser:launch').returns(true)
plugins.execute.resolves({
extensions: 'not-an-array',
it('does not maximize the browser if headless', function () {
this.browser.isHeadless = true
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(wdcInstance.maximizeWindow).not.to.have.been.called
})
})
it('does not maximize the browser if "-width" or "-height" arg is set', function () {
this.browser.isHeadless = false
sinon.stub(utils, 'executeBeforeBrowserLaunch').resolves({
args: ['-width', '1280', '-height', '720'],
extensions: [],
preferences: {},
})
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(marionetteDriver.send).calledWithMatch({ name: 'Addon:Install', params: { path: '/path/to/ext' } })
expect(wdcInstance.maximizeWindow).not.to.have.been.called
})
})
expect(marionetteDriver.send).not.calledWithMatch({ name: 'Addon:Install', params: { path: '/path/to/user/ext' } })
it('maximizes the browser if headed and no "-width" or "-height" arg is set', function () {
this.browser.isHeadless = false
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(wdcInstance.maximizeWindow).to.have.been.called
})
})
@@ -357,21 +417,6 @@ describe('lib/browsers/firefox', () => {
})
})
it('launches with the url and args', function () {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(launch.launch).to.be.calledWith(this.browser, 'about:blank', 1234, [
'-marionette',
'-new-instance',
'-foreground',
'-start-debugger-server',
'-no-remote',
'--remote-debugging-port=1234',
'-profile',
'/path/to/appData/firefox-stable/interactive',
])
})
})
it('resolves the browser instance', function () {
return firefox.open(this.browser, 'http://', this.options, this.automation).then((result) => {
expect(result).to.equal(this.browserInstance)
@@ -395,15 +440,6 @@ describe('lib/browsers/firefox', () => {
})
})
it('creates xulstore.json if not exist', function () {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
// @ts-ignore
expect(specUtil.getFsPath('/path/to/appData/firefox-stable/interactive')).containSubset({
'xulstore.json': '{"chrome://browser/content/browser.xhtml":{"main-window":{"width":1280,"height":1024,"sizemode":"maximized"}}}\n',
})
})
})
it('creates chrome/userChrome.css if not exist', function () {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(specUtil.getFsPath('/path/to/appData/firefox-stable/interactive/chrome/userChrome.css')).ok
@@ -464,21 +500,6 @@ describe('lib/browsers/firefox', () => {
expect(instance).to.eq(this.browserInstance)
})
// @see https://github.com/cypress-io/cypress/issues/6392
it('detached on Windows', async function () {
sinon.stub(os, 'platform').returns('win32')
const instance = await firefox.open(this.browser, 'http://', this.options, this.automation)
expect(instance).to.not.eq(this.browserInstance)
expect(instance.pid).to.eq(this.browserInstance.pid)
await new Promise((resolve) => {
// ensure events are wired as expected
instance.on('exit', resolve)
instance.kill()
})
})
})
})
@@ -489,57 +510,6 @@ describe('lib/browsers/firefox', () => {
})
context('firefox-util', () => {
context('#setupMarionette', () => {
// @see https://github.com/cypress-io/cypress/issues/7159
it('attaches geckodriver after testing connection', async () => {
await firefoxUtil.setupMarionette([], '', port)
expect(marionetteDriver.connect).to.be.calledOnce
expect(protocol._connectAsync).to.be.calledWith({
host: '127.0.0.1',
port,
getDelayMsForRetry: sinon.match.func,
})
})
it('rejects on errors on socket', async () => {
marionetteSendCb = () => {
marionetteDriver.tcp.socket.emit('error', new Error('foo error'))
return Promise.resolve()
}
await expect(firefoxUtil.setupMarionette([], '', port))
.to.be.rejected.then((err) => {
expect(stripAnsi(err.message)).to.include(`An unexpected error was received from Marionette: Socket`)
expect(err.details).to.include('Error: foo error')
expect(err.originalError.message).to.eq('foo error')
})
})
it('rejects on errors from marionette commands', async () => {
marionetteSendCb = () => {
return Promise.reject(new Error('foo error'))
}
await expect(firefoxUtil.setupMarionette([], '', port))
.to.be.rejected.then((err) => {
expect(stripAnsi(err.message)).to.include('An unexpected error was received from Marionette: commands')
expect(err.details).to.include('Error: foo error')
})
})
it('rejects on errors during initial Marionette connection', async () => {
marionetteDriver.connect.rejects(new Error('not connectable'))
await expect(firefoxUtil.setupMarionette([], '', port))
.to.be.rejected.then((err) => {
expect(stripAnsi(err.message)).to.include('An unexpected error was received from Marionette: connection')
expect(err.details).to.include('Error: not connectable')
})
})
})
context('#setupFoxdriver', () => {
it('attaches foxdriver after testing connection', async () => {
await firefoxUtil.setupFoxdriver(port)
@@ -601,7 +571,7 @@ describe('lib/browsers/firefox', () => {
sinon.stub(BrowserCriClient, 'create').resolves(browserCriClient)
sinon.stub(CdpAutomation, 'create').resolves()
const actual = await firefoxUtil.setupRemote(port, automationStub, null)
const actual = await firefoxUtil.setupCDP(port, automationStub, null)
expect(actual).to.equal(browserCriClient)
expect(browserCriClient.attachToTargetUrl).to.be.calledWith('about:blank')

View File

@@ -0,0 +1,242 @@
import Bluebird from 'bluebird'
import debug from 'debug'
import mockery from 'mockery'
import EventEmitter from 'events'
import { expect, sinon } from '../../../spec_helper'
import { GeckoDriver, type StartGeckoDriverArgs } from '../../../../lib/browsers/geckodriver'
import type Sinon from 'sinon'
describe('lib/browsers/geckodriver', () => {
let geckoDriverMockProcess: any
let geckoDriverMockStart: Sinon.SinonStub
let waitPortPackageStub: Sinon.SinonStub
let mockOpts: StartGeckoDriverArgs
beforeEach(() => {
geckoDriverMockProcess = new EventEmitter()
geckoDriverMockStart = sinon.stub()
waitPortPackageStub = sinon.stub()
geckoDriverMockProcess.stdout = new EventEmitter()
geckoDriverMockProcess.stderr = new EventEmitter()
geckoDriverMockProcess.kill = sinon.stub().returns(true)
mockOpts = {
host: '127.0.0.1',
port: 3000,
marionetteHost: '127.0.0.1',
marionettePort: 3001,
webdriverBidiPort: 3002,
profilePath: 'path/to/profile',
binaryPath: 'path/to/binary',
}
mockery.enable()
mockery.warnOnUnregistered(false)
mockery.registerMock('wait-port', waitPortPackageStub)
// we stub the dynamic require on the Class to make this easier to test
// @ts-expect-error
GeckoDriver.getGeckoDriverPackage = () => {
return {
start: geckoDriverMockStart,
}
}
})
afterEach(() => {
mockery.deregisterMock('geckodriver')
mockery.deregisterMock('wait-port')
mockery.disable()
})
describe('GeckoDriver.create', () => {
it('starts the geckodriver', async () => {
geckoDriverMockStart.resolves(geckoDriverMockProcess)
waitPortPackageStub.resolves()
const geckoDriverInstanceWrapper = await GeckoDriver.create(mockOpts)
expect(geckoDriverInstanceWrapper).to.equal(geckoDriverMockProcess)
expect(geckoDriverMockStart).to.have.been.called.once
expect(geckoDriverMockStart).to.have.been.calledWith(sinon.match({
host: '127.0.0.1',
port: 3000,
marionetteHost: '127.0.0.1',
marionettePort: 3001,
websocketPort: 3002,
profileRoot: 'path/to/profile',
binary: 'path/to/binary',
jsdebugger: false,
logNoTruncate: false,
log: 'error',
spawnOpts: {},
}))
expect(waitPortPackageStub).to.have.been.called.once
expect(waitPortPackageStub).to.have.been.calledWith(sinon.match({
port: 3000,
timeout: 6000,
output: 'silent',
}))
})
it('allows overriding of default props when starting', async () => {
geckoDriverMockStart.resolves(geckoDriverMockProcess)
waitPortPackageStub.resolves()
mockOpts.spawnOpts = {
MOZ_FOO: 'BAR',
}
const geckoDriverInstanceWrapper = await GeckoDriver.create(mockOpts, 10000)
expect(geckoDriverInstanceWrapper).to.equal(geckoDriverMockProcess)
expect(geckoDriverMockStart).to.have.been.called.once
expect(geckoDriverMockStart).to.have.been.calledWith(sinon.match({
host: '127.0.0.1',
port: 3000,
marionetteHost: '127.0.0.1',
marionettePort: 3001,
websocketPort: 3002,
profileRoot: 'path/to/profile',
binary: 'path/to/binary',
jsdebugger: false,
logNoTruncate: false,
log: 'error',
spawnOpts: {
MOZ_FOO: 'BAR',
},
}))
expect(waitPortPackageStub).to.have.been.called.once
expect(waitPortPackageStub).to.have.been.calledWith(sinon.match({
port: 3000,
timeout: 11000,
output: 'silent',
}))
})
describe('debugging', () => {
afterEach(() => {
debug.disable()
})
it('sets additional arguments if "DEBUG=cypress-verbose:server:browsers:geckodriver" is set', async () => {
debug.enable('cypress-verbose:server:browsers:geckodriver')
geckoDriverMockStart.resolves(geckoDriverMockProcess)
waitPortPackageStub.resolves()
mockOpts.spawnOpts = {
MOZ_FOO: 'BAR',
}
const geckoDriverInstanceWrapper = await GeckoDriver.create(mockOpts)
expect(geckoDriverInstanceWrapper).to.equal(geckoDriverMockProcess)
expect(geckoDriverMockStart).to.have.been.called.once
expect(geckoDriverMockStart).to.have.been.calledWith(sinon.match({
host: '127.0.0.1',
port: 3000,
marionetteHost: '127.0.0.1',
marionettePort: 3001,
websocketPort: 3002,
profileRoot: 'path/to/profile',
binary: 'path/to/binary',
jsdebugger: true,
logNoTruncate: true,
log: 'debug',
spawnOpts: {
MOZ_FOO: 'BAR',
},
}))
expect(waitPortPackageStub).to.have.been.called.once
expect(waitPortPackageStub).to.have.been.calledWith(sinon.match({
port: 3000,
timeout: 6000,
output: 'dots',
}))
})
})
describe('throws if', () => {
it('geckodriver failed to start', async () => {
geckoDriverMockStart.rejects(new Error('I FAILED TO START'))
try {
await GeckoDriver.create(mockOpts)
} catch (err) {
expect(err.isCypressErr).to.be.true
expect(err.type).to.equal('FIREFOX_GECKODRIVER_FAILURE')
// what the debug logs will show
expect(err.details).to.contain('Error: I FAILED TO START')
// what the user sees
expect(err.messageMarkdown).to.equal('Cypress could not connect to Firefox.\n\nAn unexpected error was received from GeckoDriver: `geckodriver:start`\n\nTo avoid this error, ensure that there are no other instances of Firefox launched by Cypress running.')
return
}
throw 'test did not enter catch as expected'
})
it('geckodriver failed to attach or took to long to register', async () => {
geckoDriverMockStart.resolves(geckoDriverMockProcess)
waitPortPackageStub.rejects(new Error('I DID NOT ATTACH OR TOOK TOO LONG!'))
try {
await GeckoDriver.create(mockOpts)
} catch (err) {
expect(err.isCypressErr).to.be.true
expect(err.type).to.equal('FIREFOX_GECKODRIVER_FAILURE')
// what the debug logs will show
expect(err.details).to.contain('Error: I DID NOT ATTACH OR TOOK TOO LONG!')
// what the user sees
expect(err.messageMarkdown).to.equal('Cypress could not connect to Firefox.\n\nAn unexpected error was received from GeckoDriver: `geckodriver:start`\n\nTo avoid this error, ensure that there are no other instances of Firefox launched by Cypress running.')
expect(geckoDriverMockProcess.kill).to.have.been.called.once
return
}
throw 'test did not enter catch as expected'
})
it('geckodriver times out starting', async () => {
geckoDriverMockStart.resolves(geckoDriverMockProcess)
// return a promise that does not resolve so the timeout is reached
waitPortPackageStub.resolves(new Bluebird(() => {}))
try {
// timeout after 0 seconds
await GeckoDriver.create(mockOpts, 0)
} catch (err) {
expect(err.isCypressErr).to.be.true
expect(err.type).to.equal('FIREFOX_GECKODRIVER_FAILURE')
// what the debug logs will show
expect(err.details).to.contain('TimeoutError: operation timed out')
// what the user sees
expect(err.messageMarkdown).to.equal('Cypress could not connect to Firefox.\n\nAn unexpected error was received from GeckoDriver: `geckodriver:start`\n\nTo avoid this error, ensure that there are no other instances of Firefox launched by Cypress running.')
expect(geckoDriverMockProcess.kill).to.have.been.called.once
return
}
throw 'test did not enter catch as expected'
})
})
})
})

View File

@@ -0,0 +1,316 @@
import nock from 'nock'
import { expect } from '../../../spec_helper'
import { WebDriverClassic } from '../../../../lib/browsers/webdriver-classic'
describe('lib/browsers/webdriver-classic', () => {
let mockSessionId: string
let mockOpts: {
host: string
port: number
}
let nockContext: nock.Scope
beforeEach(() => {
mockSessionId = `123456-abcdef`
mockOpts = {
host: '127.0.0.1',
port: 3000,
}
nockContext = nock(`http://${mockOpts.host}:${mockOpts.port}`)
})
afterEach(() => {
nock.cleanAll()
})
describe('WebDriverClassic.createSession', () => {
it('can create a session', async () => {
const newSessionScope = nockContext.post('/session', {
capabilities: {
alwaysMatch: {
acceptInsecureCerts: true,
binary: '/path/to/binary',
'moz:firefoxOptions': {
args: ['-headless', '-new-instance'],
env: {
foo: 'bar',
},
prefs: {
'remote.active-protocols': 1,
},
},
'moz:debuggerAddress': true,
},
},
}).reply(200, {
value: {
capabilities: {
acceptInsecureCerts: true,
browserName: 'firefox',
browserVersion: '130.0',
'moz:accessibilityChecks': false,
'moz:buildID': '20240829075237',
'moz:geckodriverVersion': '0.35.0',
'moz:headless': false,
'moz:platformVersion': '23.3.0',
'moz:profile': '/path/to/profile',
'moz:processID': 12345,
'moz:shutdownTimeout': 60000,
'moz:windowless': false,
'moz:webdriverClick': true,
'pageLoadStrategy': 'normal',
platformName: 'mac',
proxy: {},
setWindowRect: true,
strictFileInteractability: false,
timeouts: {
implicit: 0,
pageLoad: 300000,
script: 30000,
},
unhandledPromptBehavior: 'dismiss and notify',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:130.0) Gecko/20100101 Firefox/130.0',
'moz:debuggerAddress': '127.0.0.1:3001',
},
sessionId: mockSessionId,
},
})
const wdc = new WebDriverClassic(mockOpts.host, mockOpts.port)
const { capabilities } = await wdc.createSession({
capabilities: {
alwaysMatch: {
acceptInsecureCerts: true,
binary: '/path/to/binary',
'moz:firefoxOptions': {
args: ['-headless', '-new-instance'],
env: {
foo: 'bar',
},
prefs: {
'remote.active-protocols': 1,
},
},
'moz:debuggerAddress': true,
},
},
})
// test a few expected capabilities from the response
expect(capabilities.acceptInsecureCerts).to.be.true
expect(capabilities['moz:debuggerAddress']).to.equal('127.0.0.1:3001')
expect(capabilities.platformName).to.equal('mac')
newSessionScope.done()
})
it('throws if session cannot be created (detailed)', () => {
nockContext.post('/session', {
capabilities: {
alwaysMatch: {
acceptInsecureCerts: true,
},
},
}).reply(500, {
value: {
error: 'session not created',
message: 'failed to set preferences: unknown error',
},
})
const wdc = new WebDriverClassic(mockOpts.host, mockOpts.port)
expect(wdc.createSession({
capabilities: {
alwaysMatch: {
acceptInsecureCerts: true,
},
},
})).to.be.rejectedWith('500: Internal Server Error. session not created. failed to set preferences: unknown error.')
})
it('throws if session cannot be created (generic)', () => {
nockContext.post('/session', {
capabilities: {
alwaysMatch: {
acceptInsecureCerts: true,
},
},
}).reply(500)
const wdc = new WebDriverClassic(mockOpts.host, mockOpts.port)
expect(wdc.createSession({
capabilities: {
alwaysMatch: {
acceptInsecureCerts: true,
},
},
})).to.be.rejectedWith('500: Internal Server Error.')
})
})
describe('WebDriverClassic.installAddOn', () => {
it('can install extensions', async () => {
const installExtensionScope = nockContext.post(`/session/${mockSessionId}/moz/addon/install`, {
path: '/path/to/ext',
temporary: true,
}).reply(200)
const wdc = new WebDriverClassic(mockOpts.host, mockOpts.port)
// @ts-expect-error
wdc.sessionId = mockSessionId
await wdc.installAddOn({
path: '/path/to/ext',
temporary: true,
})
installExtensionScope.done()
})
it('throws if extension cannot be installed', () => {
nockContext.post(`/session/${mockSessionId}/moz/addon/install`, {
path: '/path/to/ext',
temporary: true,
}).reply(500)
const wdc = new WebDriverClassic(mockOpts.host, mockOpts.port)
// @ts-expect-error
wdc.sessionId = mockSessionId
expect(wdc.installAddOn({
path: '/path/to/ext',
temporary: true,
})).to.be.rejectedWith('500: Internal Server Error')
})
})
describe('WebDriverClassic.getWindowHandles', () => {
it('returns the page contexts when the requests succeeds', async () => {
const expectedContexts = ['mock-context-id-1']
nockContext.get(`/session/${mockSessionId}/window/handles`).reply(200, {
value: expectedContexts,
})
const wdc = new WebDriverClassic(mockOpts.host, mockOpts.port)
// @ts-expect-error
wdc.sessionId = mockSessionId
const contexts = await wdc.getWindowHandles()
expect(contexts).to.deep.equal(expectedContexts)
})
it('throws an error if the request fails', async () => {
nockContext.get(`/session/${mockSessionId}/window/handles`).reply(500)
const wdc = new WebDriverClassic(mockOpts.host, mockOpts.port)
// @ts-expect-error
wdc.sessionId = mockSessionId
expect(wdc.getWindowHandles()).to.be.rejectedWith('500: Internal Server Error')
})
})
describe('WebDriverClassic.switchToWindow', () => {
it('returns null when the requests succeeds', async () => {
nockContext.post(`/session/${mockSessionId}/window`, {
handle: 'mock-context-id',
}).reply(200, {
value: null,
})
const wdc = new WebDriverClassic(mockOpts.host, mockOpts.port)
// @ts-expect-error
wdc.sessionId = mockSessionId
const payload = await wdc.switchToWindow('mock-context-id')
expect(payload).to.equal(null)
})
it('throws an error if the request fails', async () => {
nockContext.post(`/session/${mockSessionId}/window`, {
handle: 'mock-context-id',
}).reply(500)
const wdc = new WebDriverClassic(mockOpts.host, mockOpts.port)
// @ts-expect-error
wdc.sessionId = mockSessionId
expect(wdc.switchToWindow('mock-context-id')).to.be.rejectedWith('500: Internal Server Error')
})
})
describe('WebDriverClassic.navigate', () => {
let mockNavigationUrl = 'http://localhost:8080'
it('returns null when the requests succeeds', async () => {
nockContext.post(`/session/${mockSessionId}/url`, {
url: mockNavigationUrl,
}).reply(200, {
value: null,
})
const wdc = new WebDriverClassic(mockOpts.host, mockOpts.port)
// @ts-expect-error
wdc.sessionId = mockSessionId
const payload = await wdc.navigate(mockNavigationUrl)
expect(payload).to.equal(null)
})
it('throws an error if the request fails', async () => {
nockContext.post(`/session/${mockSessionId}/url`, {
url: mockNavigationUrl,
}).reply(500)
const wdc = new WebDriverClassic(mockOpts.host, mockOpts.port)
// @ts-expect-error
wdc.sessionId = mockSessionId
expect(wdc.navigate(mockNavigationUrl)).to.be.rejectedWith('500: Internal Server Error')
})
})
describe('WebDriverClassic.maximizeWindow', () => {
it('returns null when the requests succeeds', async () => {
nockContext.post(`/session/${mockSessionId}/window/maximize`).reply(200, {
value: null,
})
const wdc = new WebDriverClassic(mockOpts.host, mockOpts.port)
// @ts-expect-error
wdc.sessionId = mockSessionId
const payload = await wdc.maximizeWindow()
expect(payload).to.equal(null)
})
it('throws an error if the request fails', async () => {
nockContext.post(`/session/${mockSessionId}/window/maximize`).reply(500)
const wdc = new WebDriverClassic(mockOpts.host, mockOpts.port)
// @ts-expect-error
wdc.sessionId = mockSessionId
expect(wdc.maximizeWindow()).to.be.rejectedWith('500: Internal Server Error')
})
})
})

View File

@@ -170,8 +170,8 @@ const buildEntryPointAndCleanup = async (buildAppDir) => {
await Promise.all(potentiallyRemovedDependencies.map(async (dependency) => {
const typeScriptlessDependency = dependency.replace(/\.ts$/, '.js')
// marionette-client and babel/runtime require all of their dependencies in a very non-standard dynamic way. We will keep anything in marionette-client and babel/runtime
if (!keptDependencies.includes(typeScriptlessDependency.slice(2)) && !typeScriptlessDependency.includes('marionette-client') && !typeScriptlessDependency.includes('@babel/runtime')) {
// babel/runtime requires all of its dependencies in a very non-standard dynamic way. We will keep anything in babel/runtime
if (!keptDependencies.includes(typeScriptlessDependency.slice(2)) && !typeScriptlessDependency.includes('@babel/runtime')) {
await fs.remove(path.join(buildAppDir, typeScriptlessDependency))
}
}))

View File

@@ -5,9 +5,10 @@ module.exports = {
if (browser.family === 'firefox') {
// this is needed to ensure correct error screenshot / video recording
// resolution of exactly 1280x720
// (height must account for firefox url bar, which we can only shrink to 1px)
// (height must account for firefox url bar, which we can only shrink to 1px ,
// and the total size of the window url and tab bar, which is 85 pixels for a total offset of 86 pixels)
options.args.push(
'-width', '1280', '-height', '721',
'-width', '1280', '-height', '806',
)
} else if (browser.name === 'electron') {
options.preferences.width = 1280

View File

@@ -8,5 +8,5 @@ it('asserts on browser args', () => {
return
}
cy.task('assertPsOutput')
cy.task('assertPsOutput', Cypress.browser.name)
})

View File

@@ -8,5 +8,5 @@ it('2 - asserts on browser args', () => {
return
}
cy.task('assertPsOutput')
cy.task('assertPsOutput', Cypress.browser.name)
})

View File

@@ -26,13 +26,22 @@ const getHandlersByType = (type) => {
return {
onBeforeBrowserLaunch (browser, launchOptions) {
// this will emit a warning but only once
launchOptions = launchOptions.concat(['--foo'])
launchOptions.push('--foo=bar')
launchOptions.unshift('--load-extension=/foo/bar/baz.js')
// with firefox & geckodriver, you cannot pipe extraneous arguments to the browser or else the browser will fail to launch
if (browser.name === 'firefox') {
launchOptions = launchOptions.concat(['-height', `768`, '-width', '1366'])
} else {
launchOptions = launchOptions.concat(['--foo'])
launchOptions.push('--foo=bar')
launchOptions.unshift('--load-extension=/foo/bar/baz.js')
}
return launchOptions
},
onTask: { assertPsOutput: assertPsOutput(['--foo', '--foo=bar']) },
onTask: {
assertPsOutput (args) {
return args === 'firefox' ? assertPsOutput(['-height', '-width']) : assertPsOutput(['--foo', '--foo=bar'])
},
},
}
case 'return-new-array-without-mutation':
@@ -50,12 +59,22 @@ const getHandlersByType = (type) => {
return {
onBeforeBrowserLaunch (browser, launchOptions) {
// this will NOT emit a warning
launchOptions.args.push('--foo')
launchOptions.args.unshift('--bar')
// with firefox & geckodriver, you cannot pipe extraneous arguments to the browser or else the browser will fail to launch
if (browser.name === 'firefox') {
launchOptions.args.push('-height', '768')
launchOptions.args.push('-width', '1366')
} else {
launchOptions.args.push('--foo')
launchOptions.args.unshift('--bar')
}
return launchOptions
},
onTask: { assertPsOutput: assertPsOutput(['--foo', '--bar']) },
onTask: {
assertPsOutput (args) {
return args === 'firefox' ? assertPsOutput(['-height', '-width']) : assertPsOutput(['--foo', '--bar'])
},
},
}
case 'return-undefined-mutate-array':

View File

@@ -32,8 +32,7 @@ describe('windowSize', () => {
// availHeight: top.screen.availHeight,
}).deep.eq({
innerWidth: 1280,
// NOTE: Firefox 130.0 with the default sizing is a pixel short, which we are accounting for here
innerHeight: Cypress.browser.name === 'firefox' ? 719 : 720,
innerHeight: 720,
// screenWidth: 1280,
// screenHeight: 720,
// availWidth: 1280,

374
yarn.lock
View File

@@ -8730,6 +8730,16 @@
dependencies:
vue-demi "*"
"@wdio/logger@^8.28.0":
version "8.38.0"
resolved "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz#a96406267e800bef9c58ac95de00f42ab0d3ac5c"
integrity sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==
dependencies:
chalk "^5.1.2"
loglevel "^1.6.0"
loglevel-plugin-prefix "^0.8.4"
strip-ansi "^7.1.0"
"@webassemblyjs/ast@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
@@ -9170,6 +9180,11 @@
resolved "https://registry.yarnpkg.com/@zeit/schemas/-/schemas-2.36.0.tgz#7a1b53f4091e18d0b404873ea3e3c83589c765f2"
integrity sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==
"@zip.js/zip.js@^2.7.44":
version "2.7.52"
resolved "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.52.tgz#bc11de93b41f09e03155bc178e7f9c2e2612671d"
integrity sha512-+5g7FQswvrCHwYKNMd/KFxZSObctLSsQOgqBSi0LzwHo3li9Eh1w5cF5ndjQw9Zbr3ajVnd2+XyiX85gAetx1Q==
"@zkochan/js-yaml@0.0.6":
version "0.0.6"
resolved "https://registry.yarnpkg.com/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz#975f0b306e705e28b8068a07737fa46d3fc04826"
@@ -10111,11 +10126,6 @@ async@^2.1.4, async@^2.6.4:
dependencies:
lodash "^4.17.14"
async@~0.2.9:
version "0.2.10"
resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E=
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -10205,16 +10215,7 @@ axios@0.21.2:
dependencies:
follow-redirects "^1.14.0"
axios@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axios@^1.6.0:
axios@^1.0.0, axios@^1.6.0:
version "1.7.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.4.tgz#4c8ded1b43683c8dd362973c393f3ede24052aa2"
integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==
@@ -10223,10 +10224,10 @@ axios@^1.6.0:
form-data "^4.0.0"
proxy-from-env "^1.1.0"
b4a@^1.6.4:
version "1.6.4"
resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.4.tgz#ef1c1422cae5ce6535ec191baeed7567443f36c9"
integrity sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==
b4a@^1.6.4, b4a@^1.6.6:
version "1.6.6"
resolved "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz#a4cc349a3851987c3c4ac2d7785c18744f6da9ba"
integrity sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==
babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
version "6.26.0"
@@ -10409,6 +10410,40 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
bare-events@^2.0.0, bare-events@^2.2.0:
version "2.4.2"
resolved "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz#3140cca7a0e11d49b3edc5041ab560659fd8e1f8"
integrity sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==
bare-fs@^2.1.1:
version "2.3.4"
resolved "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.4.tgz#339d3a9ee574bf58de3a9c93f45dd6f1c62c92d2"
integrity sha512-7YyxitZEq0ey5loOF5gdo1fZQFF7290GziT+VbAJ+JbYTJYaPZwuEz2r/Nq23sm4fjyTgUf2uJI2gkT3xAuSYA==
dependencies:
bare-events "^2.0.0"
bare-path "^2.0.0"
bare-stream "^2.0.0"
bare-os@^2.1.0:
version "2.4.3"
resolved "https://registry.npmjs.org/bare-os/-/bare-os-2.4.3.tgz#e8b628e48b9f48165619f9238e5eeaf2eedaffef"
integrity sha512-FjkNiU3AwTQNQkcxFOmDcCfoN1LjjtU+ofGJh5DymZZLTqdw2i/CzV7G0h3snvh6G8jrWtdmNSgZPH4L2VOAsQ==
bare-path@^2.0.0, bare-path@^2.1.0:
version "2.1.3"
resolved "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz#594104c829ef660e43b5589ec8daef7df6cedb3e"
integrity sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==
dependencies:
bare-os "^2.1.0"
bare-stream@^2.0.0:
version "2.3.0"
resolved "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.0.tgz#5bef1cab8222517315fca1385bd7f08dff57f435"
integrity sha512-pVRWciewGUeCyKEuRxwv06M079r+fRjAQjBEK2P6OYGrO43O+Z0LrPZZEjlc4mB6C2RpZ9AxJ1s7NLEtOHO6eA==
dependencies:
b4a "^1.6.6"
streamx "^2.20.0"
base64-arraybuffer@0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
@@ -11491,7 +11526,7 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^5.0.1, chalk@^5.3.0:
chalk@^5.0.1, chalk@^5.1.2, chalk@^5.3.0:
version "5.3.0"
resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==
@@ -11752,9 +11787,9 @@ ci-info@^2.0.0:
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
ci-info@^3.2.0, ci-info@^3.6.1:
version "3.8.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91"
integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==
version "3.9.0"
resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4"
integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==
ci-info@^4.0.0:
version "4.0.0"
@@ -12229,9 +12264,9 @@ commander@^8.3.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
commander@^9.4.0:
commander@^9.3.0, commander@^9.4.0:
version "9.5.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30"
resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30"
integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==
commander@~2.19.0:
@@ -13161,6 +13196,11 @@ data-uri-to-buffer@2.0.1:
dependencies:
"@types/node" "^8.0.7"
data-uri-to-buffer@^4.0.0:
version "4.0.1"
resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
data-uri-to-buffer@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-5.0.1.tgz#db89a9e279c2ffe74f50637a59a32fb23b3e4d7c"
@@ -13238,13 +13278,6 @@ debounce@^1.2.0:
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==
debug@*, debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -13273,6 +13306,13 @@ debug@3.2.6:
dependencies:
ms "^2.1.1"
debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
debug@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
@@ -13326,6 +13366,11 @@ decamelize@^4.0.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
decamelize@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz#8cad4d916fde5c41a264a43d0ecc56fe3d31749e"
integrity sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==
decode-uri-component@^0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9"
@@ -15832,7 +15877,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-fifo@^1.1.0, fast-fifo@^1.2.0:
fast-fifo@^1.2.0, fast-fifo@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c"
integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==
@@ -15974,6 +16019,14 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
version "3.2.0"
resolved "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
dependencies:
node-domexception "^1.0.0"
web-streams-polyfill "^3.0.3"
fetch-retry-ts@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/fetch-retry-ts/-/fetch-retry-ts-1.3.1.tgz#a1572ebe28657fe8b89af0e130820a01feb1e753"
@@ -16147,13 +16200,6 @@ find-cache-dir@^4.0.0:
common-path-prefix "^3.0.0"
pkg-dir "^7.0.0"
find-port@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/find-port/-/find-port-1.0.1.tgz#db084a6cbf99564d99869ae79fbdecf66e8a185c"
integrity sha1-2whKbL+ZVk2Zhprnn73s9m6KGFw=
dependencies:
async "~0.2.9"
find-process@1.4.7:
version "1.4.7"
resolved "https://registry.yarnpkg.com/find-process/-/find-process-1.4.7.tgz#8c76962259216c381ef1099371465b5b439ea121"
@@ -16396,12 +16442,7 @@ folktale@2.3.2:
resolved "https://registry.yarnpkg.com/folktale/-/folktale-2.3.2.tgz#38231b039e5ef36989920cbf805bf6b227bf4fd4"
integrity sha512-+8GbtQBwEqutP0v3uajDDoN64K2ehmHd0cjlghhxh0WpcfPzAIjPA03e1VvHlxL02FVGR0A6lwXsNQKn3H1RNQ==
follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
follow-redirects@^1.15.6:
follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.15.6:
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
@@ -16493,6 +16534,13 @@ formatio@1.1.1:
dependencies:
samsam "~1.1"
formdata-polyfill@^4.0.10:
version "4.0.10"
resolved "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
dependencies:
fetch-blob "^3.1.2"
formidable@^1.2.0, formidable@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9"
@@ -16606,7 +16654,7 @@ fs-extra@^6.0.1:
fs-extra@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
dependencies:
graceful-fs "^4.1.2"
@@ -16786,6 +16834,20 @@ gauge@~2.7.3:
strip-ansi "^3.0.1"
wide-align "^1.1.0"
geckodriver@4.4.2:
version "4.4.2"
resolved "https://registry.npmjs.org/geckodriver/-/geckodriver-4.4.2.tgz#b5b72b3e5deb905947151f214b96f52505c2dd3a"
integrity sha512-/JFJ7DJPJUvDhLjzQk+DwjlkAmiShddfRHhZ/xVL9FWbza5Bi3UMGmmerEKqD69JbRs7R81ZW31co686mdYZyA==
dependencies:
"@wdio/logger" "^8.28.0"
"@zip.js/zip.js" "^2.7.44"
decamelize "^6.0.0"
http-proxy-agent "^7.0.2"
https-proxy-agent "^7.0.4"
node-fetch "^3.3.2"
tar-fs "^3.0.6"
which "^4.0.0"
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
@@ -18185,10 +18247,10 @@ http-proxy-agent@^5.0.0:
agent-base "6"
debug "4"
http-proxy-agent@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz#e9096c5afd071a3fce56e6252bb321583c124673"
integrity sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==
http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.2:
version "7.0.2"
resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
dependencies:
agent-base "^7.1.0"
debug "^4.3.4"
@@ -18297,7 +18359,7 @@ https-proxy-agent@^5.0.0:
agent-base "6"
debug "4"
https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.2:
https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.2, https-proxy-agent@^7.0.4:
version "7.0.5"
resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2"
integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==
@@ -19868,13 +19930,23 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
json-stable-stringify@1.0.1, json-stable-stringify@^1.0.1:
json-stable-stringify@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=
dependencies:
jsonify "~0.0.0"
json-stable-stringify@^1.0.1:
version "1.1.1"
resolved "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz#52d4361b47d49168bcc4e564189a42e5a7439454"
integrity sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==
dependencies:
call-bind "^1.0.5"
isarray "^2.0.5"
jsonify "^0.0.1"
object-keys "^1.1.1"
json-stringify-nice@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz#2c937962b80181d3f317dd39aa323e14f5a60a67"
@@ -19893,11 +19965,6 @@ json-to-pretty-yaml@^1.2.2:
remedial "^1.0.7"
remove-trailing-spaces "^1.0.6"
json-wire-protocol@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-wire-protocol/-/json-wire-protocol-1.0.0.tgz#50a6fd7e5f1406dbaf5a4d3279be2620181276f8"
integrity sha1-UKb9fl8UBtuvWk0yeb4mIBgSdvg=
json3@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
@@ -19963,10 +20030,10 @@ jsonfile@^6.0.1:
optionalDependencies:
graceful-fs "^4.1.6"
jsonify@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
jsonify@^0.0.1, jsonify@~0.0.0:
version "0.0.1"
resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978"
integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==
jsonparse@^1.2.0, jsonparse@^1.3.1:
version "1.3.1"
@@ -21064,10 +21131,15 @@ log-update@^4.0.0:
slice-ansi "^4.0.0"
wrap-ansi "^6.2.0"
loglevel@^1.6.8:
version "1.7.1"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
loglevel-plugin-prefix@^0.8.4:
version "0.8.4"
resolved "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz#2fe0e05f1a820317d98d8c123e634c1bd84ff644"
integrity sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==
loglevel@^1.6.0, loglevel@^1.6.8:
version "1.9.2"
resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz#c2e028d6c757720107df4e64508530db6621ba08"
integrity sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==
lolex@1.3.2:
version "1.3.2"
@@ -21359,16 +21431,6 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
"marionette-client@https://github.com/cypress-io/marionette-client.git#5fc10cdf6c02627e9a2add98ca52de4d0c2fe74d":
version "1.9.5"
resolved "https://github.com/cypress-io/marionette-client.git#5fc10cdf6c02627e9a2add98ca52de4d0c2fe74d"
dependencies:
debug "^4.0.1"
find-port "1.0.1"
json-wire-protocol "^1.0.0"
promise "7.0.4"
socket-retry-connect "0.0.1"
markdown-it@13.0.1:
version "13.0.1"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430"
@@ -22033,7 +22095,7 @@ mobx@5.15.4:
resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.4.tgz#9da1a84e97ba624622f4e55a0bf3300fb931c2ab"
integrity sha512-xRFJxSU2Im3nrGCdjSuOTFmxVDGeqOHL+TyADCGbT0k4HHqGmx5u2yaHNryvoORpI4DfbzjJ5jPmuv+d7sioFw==
"mocha-7.0.1@npm:mocha@7.0.1":
"mocha-7.0.1@npm:mocha@7.0.1", mocha@7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.0.1.tgz#276186d35a4852f6249808c6dd4a1376cbf6c6ce"
integrity sha512-9eWmWTdHLXh72rGrdZjNbG3aa1/3NRPpul1z0D979QpEnFdCG0Q5tv834N+94QEN2cysfV72YocQ3fn87s70fg==
@@ -22150,36 +22212,6 @@ mocha@6.2.2:
yargs-parser "13.1.1"
yargs-unparser "1.6.0"
mocha@7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.0.1.tgz#276186d35a4852f6249808c6dd4a1376cbf6c6ce"
integrity sha512-9eWmWTdHLXh72rGrdZjNbG3aa1/3NRPpul1z0D979QpEnFdCG0Q5tv834N+94QEN2cysfV72YocQ3fn87s70fg==
dependencies:
ansi-colors "3.2.3"
browser-stdout "1.3.1"
chokidar "3.3.0"
debug "3.2.6"
diff "3.5.0"
escape-string-regexp "1.0.5"
find-up "3.0.0"
glob "7.1.3"
growl "1.10.5"
he "1.2.0"
js-yaml "3.13.1"
log-symbols "2.2.0"
minimatch "3.0.4"
mkdirp "0.5.1"
ms "2.1.1"
node-environment-flags "1.0.6"
object.assign "4.1.0"
strip-json-comments "2.0.1"
supports-color "6.0.0"
which "1.3.1"
wide-align "1.1.3"
yargs "13.3.0"
yargs-parser "13.1.1"
yargs-unparser "1.6.0"
mocha@7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.1.0.tgz#c784f579ad0904d29229ad6cb1e2514e4db7d249"
@@ -22855,6 +22887,11 @@ node-api-version@^0.1.4:
dependencies:
semver "^7.3.5"
node-domexception@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
node-emoji@^1.8.1:
version "1.11.0"
resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c"
@@ -22915,6 +22952,15 @@ node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7:
dependencies:
whatwg-url "^5.0.0"
node-fetch@^3.3.2:
version "3.3.2"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b"
integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==
dependencies:
data-uri-to-buffer "^4.0.0"
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"
node-forge@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.0.tgz#37a874ea723855f37db091e6c186e5b67a01d4b2"
@@ -24709,7 +24755,7 @@ pascalcase@^0.1.1:
patch-package@6.4.7:
version "6.4.7"
resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.4.7.tgz#2282d53c397909a0d9ef92dae3fdeb558382b148"
resolved "https://registry.npmjs.org/patch-package/-/patch-package-6.4.7.tgz#2282d53c397909a0d9ef92dae3fdeb558382b148"
integrity sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ==
dependencies:
"@yarnpkg/lockfile" "^1.1.0"
@@ -25535,13 +25581,6 @@ promise-retry@^2.0.1:
err-code "^2.0.2"
retry "^0.12.0"
promise@7.0.4:
version "7.0.4"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.0.4.tgz#363e84a4c36c8356b890fed62c91ce85d02ed539"
integrity sha1-Nj6EpMNsg1a4kP7WLJHOhdAu1Tk=
dependencies:
asap "~2.0.3"
promise@^7.1.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
@@ -25686,10 +25725,10 @@ pump@^2.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
pump@^3.0.0, pump@^3.0.2:
version "3.0.2"
resolved "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8"
integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==
dependencies:
end-of-stream "^1.1.0"
once "^1.3.1"
@@ -28340,13 +28379,6 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
socket-retry-connect@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/socket-retry-connect/-/socket-retry-connect-0.0.1.tgz#6adc74db3e43100320d1d25d91e512bd60218c4b"
integrity sha1-atx02z5DEAMg0dJdkeUSvWAhjEs=
dependencies:
debug "*"
socket.io-adapter@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz#43af9157c4609e74b8addc6867873ac7eb48fda2"
@@ -28905,13 +28937,16 @@ streamsearch@0.1.2:
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
streamx@^2.12.5, streamx@^2.15.0:
version "2.15.6"
resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.15.6.tgz#28bf36997ebc7bf6c08f9eba958735231b833887"
integrity sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw==
streamx@^2.12.5, streamx@^2.15.0, streamx@^2.20.0:
version "2.20.0"
resolved "https://registry.npmjs.org/streamx/-/streamx-2.20.0.tgz#5f3608483499a9346852122b26042f964ceec931"
integrity sha512-ZGd1LhDeGFucr1CUCTBOS58ZhEendd0ttpGT3usTvosS4ntIwKN9LJFp+OeCSprsCPL14BXVRZlHGRY1V9PVzQ==
dependencies:
fast-fifo "^1.1.0"
fast-fifo "^1.3.2"
queue-tick "^1.0.1"
text-decoder "^1.1.0"
optionalDependencies:
bare-events "^2.2.0"
strict-uri-encode@^1.0.0:
version "1.1.0"
@@ -28943,7 +28978,7 @@ string-template@~0.2.1:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=
"string-width-cjs@npm:string-width@^4.2.0":
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -28969,15 +29004,6 @@ string-width@^1.0.1, string-width@^1.0.2:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^3.0.0, string-width@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
@@ -29079,7 +29105,7 @@ stringify-object@^3.0.0, stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -29100,13 +29126,6 @@ strip-ansi@5.2.0, strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@@ -29114,10 +29133,10 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1:
dependencies:
ansi-regex "^2.0.0"
strip-ansi@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
dependencies:
ansi-regex "^6.0.1"
@@ -29541,6 +29560,17 @@ tar-fs@^2.0.0, tar-fs@^2.1.1:
pump "^3.0.0"
tar-stream "^2.1.4"
tar-fs@^3.0.6:
version "3.0.6"
resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz#eaccd3a67d5672f09ca8e8f9c3d2b89fa173f217"
integrity sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==
dependencies:
pump "^3.0.0"
tar-stream "^3.1.5"
optionalDependencies:
bare-fs "^2.1.1"
bare-path "^2.1.0"
tar-fs@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2"
@@ -29772,6 +29802,13 @@ terser@^5.10.0, terser@^5.16.8:
commander "^2.20.0"
source-map-support "~0.5.20"
text-decoder@^1.1.0:
version "1.1.1"
resolved "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz#5df9c224cebac4a7977720b9f083f9efa1aefde8"
integrity sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==
dependencies:
b4a "^1.6.4"
text-extensions@^1.0.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26"
@@ -31459,6 +31496,15 @@ vuex@^4.0.0:
resolved "https://registry.yarnpkg.com/vuex/-/vuex-4.0.0.tgz#ac877aa76a9c45368c979471e461b520d38e6cf5"
integrity sha512-56VPujlHscP5q/e7Jlpqc40sja4vOhC4uJD1llBCWolVI8ND4+VzisDVkUMl+z5y0MpIImW6HjhNc+ZvuizgOw==
wait-port@1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz#e5d64ee071118d985e2b658ae7ad32b2ce29b6b5"
integrity sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==
dependencies:
chalk "^4.1.2"
commander "^9.3.0"
debug "^4.3.4"
walk-up-path@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-1.0.0.tgz#d4745e893dd5fd0dbb58dd0a4c6a33d9c9fec53e"
@@ -31521,6 +31567,11 @@ weak-map@^1.0.5:
resolved "https://registry.yarnpkg.com/weak-map/-/weak-map-1.0.5.tgz#79691584d98607f5070bd3b70a40e6bb22e401eb"
integrity sha1-eWkVhNmGB/UHC9O3CkDmuyLkAes=
web-streams-polyfill@^3.0.3:
version "3.3.3"
resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b"
integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==
webextension-polyfill@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.4.0.tgz#9cc5a60f0f2bf907a6b349fdd7e61701f54956f9"
@@ -32037,7 +32088,7 @@ workerpool@6.2.0:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b"
integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -32080,15 +32131,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
@@ -32359,9 +32401,9 @@ yaml@^1.10.0:
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yaml@^2.0.0, yaml@^2.1.1, yaml@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.2.tgz#7a2b30f2243a5fc299e1f14ca58d475ed4bc5362"
integrity sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==
version "2.5.1"
resolved "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130"
integrity sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==
yargs-parser@13.1.1:
version "13.1.1"