From af3839d9907603f96c700ffb8f207deb07da4e17 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Mon, 30 Sep 2024 12:19:03 -0400 Subject: [PATCH] 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 --- .circleci/cache-version.txt | 2 +- .circleci/workflows.yml | 10 +- cli/CHANGELOG.md | 8 + cli/lib/exec/spawn.js | 16 + cli/package.json | 1 + cli/test/lib/exec/spawn_spec.js | 23 ++ ....html => FIREFOX_GECKODRIVER_FAILURE.html} | 4 +- packages/errors/src/errors.ts | 4 +- .../test/unit/visualSnapshotErrors_spec.ts | 2 +- packages/graphql/schemas/schema.graphql | 2 +- packages/launcher/lib/browsers.ts | 3 +- .../server/lib/browsers/browser-cri-client.ts | 2 +- packages/server/lib/browsers/firefox-util.ts | 139 ++----- packages/server/lib/browsers/firefox.ts | 189 +++++---- .../server/lib/browsers/geckodriver/README.md | 35 ++ .../server/lib/browsers/geckodriver/index.ts | 122 ++++++ .../lib/browsers/webdriver-classic/index.ts | 263 ++++++++++++ packages/server/package.json | 7 +- .../server/patches/geckodriver+4.4.2.patch | 311 +++++++++++++++ .../server/test/unit/browsers/firefox_spec.ts | 298 +++++++------- .../unit/browsers/geckodriver/index_spec.ts | 242 ++++++++++++ .../browsers/webdriver-classic/index_spec.ts | 316 +++++++++++++++ scripts/binary/binary-cleanup.js | 4 +- system-tests/lib/pluginUtils.js | 5 +- .../cypress/e2e/app.cy.js | 2 +- .../cypress/e2e/app_spec2.js | 2 +- .../plugins/index.js | 33 +- .../cypress/e2e/default_size.cy.js | 3 +- yarn.lock | 374 ++++++++++-------- 29 files changed, 1888 insertions(+), 534 deletions(-) rename packages/errors/__snapshot-html__/{FIREFOX_MARIONETTE_FAILURE.html => FIREFOX_GECKODRIVER_FAILURE.html} (79%) create mode 100644 packages/server/lib/browsers/geckodriver/README.md create mode 100644 packages/server/lib/browsers/geckodriver/index.ts create mode 100644 packages/server/lib/browsers/webdriver-classic/index.ts create mode 100644 packages/server/patches/geckodriver+4.4.2.patch create mode 100644 packages/server/test/unit/browsers/geckodriver/index_spec.ts create mode 100644 packages/server/test/unit/browsers/webdriver-classic/index_spec.ts diff --git a/.circleci/cache-version.txt b/.circleci/cache-version.txt index 09377f579d01..636d77afca92 100644 --- a/.circleci/cache-version.txt +++ b/.circleci/cache-version.txt @@ -1,3 +1,3 @@ # Bump this version to force CI to re-create the cache from scratch. -09-12-24 +09-24-24 diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 0ab8928c3def..302cff2ac35a 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -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 diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index de12ae239f90..e26eae8263ef 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,4 +1,12 @@ +## 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_ diff --git a/cli/lib/exec/spawn.js b/cli/lib/exec/spawn.js index e132b964e0a3..da25f03b8224 100644 --- a/cli/lib/exec/spawn.js +++ b/cli/lib/exec/spawn.js @@ -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) diff --git a/cli/package.json b/cli/package.json index c98a335ab6df..498773844011 100644 --- a/cli/package.json +++ b/cli/package.json @@ -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" }, diff --git a/cli/test/lib/exec/spawn_spec.js b/cli/test/lib/exec/spawn_spec.js index 26ad6d6713e7..76a0f7799c66 100644 --- a/cli/test/lib/exec/spawn_spec.js +++ b/cli/test/lib/exec/spawn_spec.js @@ -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) diff --git a/packages/errors/__snapshot-html__/FIREFOX_MARIONETTE_FAILURE.html b/packages/errors/__snapshot-html__/FIREFOX_GECKODRIVER_FAILURE.html similarity index 79% rename from packages/errors/__snapshot-html__/FIREFOX_MARIONETTE_FAILURE.html rename to packages/errors/__snapshot-html__/FIREFOX_GECKODRIVER_FAILURE.html index 6bbfcca54418..737b241eeeef 100644 --- a/packages/errors/__snapshot-html__/FIREFOX_MARIONETTE_FAILURE.html +++ b/packages/errors/__snapshot-html__/FIREFOX_GECKODRIVER_FAILURE.html @@ -36,11 +36,11 @@
Cypress could not connect to Firefox.
 
-An unexpected error was received from Marionette: connection
+An unexpected error was received from GeckoDriver: connection
 
 To avoid this error, ensure that there are no other instances of Firefox launched by Cypress running.
 
 Error: fail whale
     at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
-    at FIREFOX_MARIONETTE_FAILURE (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
+    at FIREFOX_GECKODRIVER_FAILURE (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
 
\ No newline at end of file diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index fb5a6121dd9f..f297495cba6a 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -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. diff --git a/packages/errors/test/unit/visualSnapshotErrors_spec.ts b/packages/errors/test/unit/visualSnapshotErrors_spec.ts index 1ba6621130a5..e4ab2205dfc8 100644 --- a/packages/errors/test/unit/visualSnapshotErrors_spec.ts +++ b/packages/errors/test/unit/visualSnapshotErrors_spec.ts @@ -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 { diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index 2d31e0e19df1..16e34c2a58a2 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -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 diff --git a/packages/launcher/lib/browsers.ts b/packages/launcher/lib/browsers.ts index 6f2f2531b5a3..3a53865e3aeb 100644 --- a/packages/launcher/lib/browsers.ts +++ b/packages/launcher/lib/browsers.ts @@ -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 +// 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 = { 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 }, } diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index fe301cadccb3..c4f53cb8785d 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -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 diff --git a/packages/server/lib/browsers/firefox-util.ts b/packages/server/lib/browsers/firefox-util.ts index 38fff4cf01fa..40d0828c24f3 100644 --- a/packages/server/lib/browsers/firefox-util.ts +++ b/packages/server/lib/browsers/firefox-util.ts @@ -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 { - 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 { + 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 { @@ -219,28 +207,40 @@ export default { return forceGcCc() }, - setup ({ + async setup ({ automation, - extensions, onError, url, - marionettePort, foxdriverPort, remotePort, - }): Bluebird { - return Bluebird.all([ + webDriverClassic: wdcInstance, + }: { + automation: Automation + onError?: (err: Error) => void + url: string + foxdriverPort: number + remotePort: number + webDriverClassic: WebDriverClassic + }): Promise { + // 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. - }, } diff --git a/packages/server/lib/browsers/firefox.ts b/packages/server/lib/browsers/firefox.ts index 0d4fec848f48..c1cfc9a88139 100644 --- a/packages/server/lib/browsers/firefox.ts +++ b/packages/server/lib/browsers/firefox.ts @@ -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 { - // 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(), - ]) - - 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 + // 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: '721', + 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 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)) + + // this command starts the webdriver session and actually opens the browser + const { capabilities } = await wdcInstance.createSession(capabilitiesToSend) - 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) + 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() } + // install the browser extensions + await Promise.all(_.map(launchOptions.extensions, (path) => { + debug(`installing extension at path: ${path}`) + + 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 originalBrowserKill = browserInstance.kill + const originalGeckoDriverKill = geckoDriverInstance.kill - browserInstance.kill = (...args) => { + 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 () { diff --git a/packages/server/lib/browsers/geckodriver/README.md b/packages/server/lib/browsers/geckodriver/README.md new file mode 100644 index 000000000000..fb635abd7203 --- /dev/null +++ b/packages/server/lib/browsers/geckodriver/README.md @@ -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. diff --git a/packages/server/lib/browsers/geckodriver/index.ts b/packages/server/lib/browsers/geckodriver/index.ts new file mode 100644 index 000000000000..8120ddb70736 --- /dev/null +++ b/packages/server/lib/browsers/geckodriver/index.ts @@ -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 + download: (geckodriverVersion?: string, cacheDir?: string) => Promise + } = () => { + /** + * 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 { + 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) + } + } +} diff --git a/packages/server/lib/browsers/webdriver-classic/index.ts b/packages/server/lib/browsers/webdriver-classic/index.ts new file mode 100644 index 000000000000..81eb62e06b87 --- /dev/null +++ b/packages/server/lib/browsers/webdriver-classic/index.ts @@ -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} - the results of the Webdriver Session (enabled through remote.active-protocols) + */ + async createSession (args: { + capabilities: {[key: string]: any} + }): Promise { + 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} All the available top-level contexts/handles + */ + async getWindowHandles (): Promise { + 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} + */ + async switchToWindow (handle: string): Promise { + 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} + */ + async maximizeWindow (): Promise { + 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} + */ + async navigate (url: string): Promise { + 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 + } + } +} diff --git a/packages/server/package.json b/packages/server/package.json index 885844249933..6374ba01e5f5 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -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" ] }, diff --git a/packages/server/patches/geckodriver+4.4.2.patch b/packages/server/patches/geckodriver+4.4.2.patch new file mode 100644 index 000000000000..18628d957f8d --- /dev/null +++ b/packages/server/patches/geckodriver+4.4.2.patch @@ -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[]`
+-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[]`
+-Default: `[]` +- +-### `binary` +-Path to the Firefox binary. +- +-Type: `string` +- +-### `connectExisting` +-Connect to an existing Firefox instance. +- +-Type: `boolean`
+-Default: `false` +- +-### `host` +-Host IP to use for WebDriver server. +- +-Type: `string`
+-Default: `0.0.0.0` +- +-### `jsdebugger` +-Attach browser toolbox debugger for Firefox. +- +-Type: `boolean`
+-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`
+-Default: `127.0.0.1` +- +-### `marionettePort` +-Port to use to connect to Gecko. +- +-Type: `number`
+-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`
+-Default: `process.env.GECKODRIVER_PATH` +- +-### `cacheDir` +-The path to the root of the cache directory. +- +-Type: `string`
+-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 diff --git a/packages/server/test/unit/browsers/firefox_spec.ts b/packages/server/test/unit/browsers/firefox_spec.ts index cbca36a5748f..54efd09c2f51 100644 --- a/packages/server/test/unit/browsers/firefox_spec.ts +++ b/packages/server/test/unit/browsers/firefox_spec.ts @@ -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 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,29 +67,48 @@ describe('lib/browsers/firefox', () => { sinon.stub(protocol, '_connectAsync').resolves(null) - stubMarionette() - stubFoxdriver() - }) - - context('#connectToNewSpec', () => { - beforeEach(function () { - this.browser = { name: 'firefox', channel: 'stable' } - this.automation = { - use: sinon.stub().returns({}), - } + this.browserInstance = { + // should be high enough to not kill any real PIDs + pid: Number.MAX_SAFE_INTEGER, + } - this.options = { - onError: () => {}, - } + 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: '', + }, }) - 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) + sinon.stub(webDriverClassicImport, 'WebDriverClassic').callsFake(() => wdcInstance) - expect(firefoxUtil.connectToNewSpec).to.be.called - }) + stubFoxdriver() }) context('#open', () => { @@ -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(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(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 }) }) + }) - return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => { - expect(marionetteDriver.send).calledWithMatch({ name: 'Addon:Install', params: { path: '/path/to/ext' } }) + it('does not maximize the browser if headless', function () { + this.browser.isHeadless = true - expect(marionetteDriver.send).calledWithMatch({ name: 'Addon:Install', params: { path: '/path/to/user/ext' } }) + return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => { + expect(wdcInstance.maximizeWindow).not.to.have.been.called }) }) - 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 "-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') diff --git a/packages/server/test/unit/browsers/geckodriver/index_spec.ts b/packages/server/test/unit/browsers/geckodriver/index_spec.ts new file mode 100644 index 000000000000..b0d52505a361 --- /dev/null +++ b/packages/server/test/unit/browsers/geckodriver/index_spec.ts @@ -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' + }) + }) + }) +}) diff --git a/packages/server/test/unit/browsers/webdriver-classic/index_spec.ts b/packages/server/test/unit/browsers/webdriver-classic/index_spec.ts new file mode 100644 index 000000000000..d69cedf120a1 --- /dev/null +++ b/packages/server/test/unit/browsers/webdriver-classic/index_spec.ts @@ -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') + }) + }) +}) diff --git a/scripts/binary/binary-cleanup.js b/scripts/binary/binary-cleanup.js index 0f6cf048eea4..da20a87c3f88 100644 --- a/scripts/binary/binary-cleanup.js +++ b/scripts/binary/binary-cleanup.js @@ -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)) } })) diff --git a/system-tests/lib/pluginUtils.js b/system-tests/lib/pluginUtils.js index 640788602f99..b6f455e45647 100644 --- a/system-tests/lib/pluginUtils.js +++ b/system-tests/lib/pluginUtils.js @@ -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 diff --git a/system-tests/projects/plugin-before-browser-launch-deprecation/cypress/e2e/app.cy.js b/system-tests/projects/plugin-before-browser-launch-deprecation/cypress/e2e/app.cy.js index 7696b3805a64..6655ffce2865 100644 --- a/system-tests/projects/plugin-before-browser-launch-deprecation/cypress/e2e/app.cy.js +++ b/system-tests/projects/plugin-before-browser-launch-deprecation/cypress/e2e/app.cy.js @@ -8,5 +8,5 @@ it('asserts on browser args', () => { return } - cy.task('assertPsOutput') + cy.task('assertPsOutput', Cypress.browser.name) }) diff --git a/system-tests/projects/plugin-before-browser-launch-deprecation/cypress/e2e/app_spec2.js b/system-tests/projects/plugin-before-browser-launch-deprecation/cypress/e2e/app_spec2.js index ddfbfb4f0f25..8556e56cb660 100644 --- a/system-tests/projects/plugin-before-browser-launch-deprecation/cypress/e2e/app_spec2.js +++ b/system-tests/projects/plugin-before-browser-launch-deprecation/cypress/e2e/app_spec2.js @@ -8,5 +8,5 @@ it('2 - asserts on browser args', () => { return } - cy.task('assertPsOutput') + cy.task('assertPsOutput', Cypress.browser.name) }) diff --git a/system-tests/projects/plugin-before-browser-launch-deprecation/plugins/index.js b/system-tests/projects/plugin-before-browser-launch-deprecation/plugins/index.js index 8ced89f538ca..ca5138f50890 100644 --- a/system-tests/projects/plugin-before-browser-launch-deprecation/plugins/index.js +++ b/system-tests/projects/plugin-before-browser-launch-deprecation/plugins/index.js @@ -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': diff --git a/system-tests/projects/screen-size/cypress/e2e/default_size.cy.js b/system-tests/projects/screen-size/cypress/e2e/default_size.cy.js index d9c447ad2f81..0c60218985d1 100644 --- a/system-tests/projects/screen-size/cypress/e2e/default_size.cy.js +++ b/system-tests/projects/screen-size/cypress/e2e/default_size.cy.js @@ -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, diff --git a/yarn.lock b/yarn.lock index 3c47ea40d437..427a8a621763 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"