mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-06 06:29:45 -06:00
feat: implement webdriver BiDi for Firefox versions 135 and greater (#30870)
* feat: implement BiDi automation client base (without full extension cutover). [run ci] * chore: implement suggestions from code review. [run ci] * update error text to be in line with documentation * address comments from code review * address comments from code review * make bidi, cdp, and both active protocols object enumerations to make the code easier to read * address additional comments from code review * fix errors from refactor * update firefox warning
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
# Bump this version to force CI to re-create the cache from scratch.
|
||||
|
||||
2-10-25
|
||||
2-12-25
|
||||
|
||||
@@ -36,7 +36,7 @@ mainBuildFilters: &mainBuildFilters
|
||||
only:
|
||||
- develop
|
||||
- /^release\/\d+\.\d+\.\d+$/
|
||||
- chore/update_wdio_deps
|
||||
- feat/implement_bidi
|
||||
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
|
||||
- 'update-v8-snapshot-cache-on-develop'
|
||||
- 'ryanm/chore/add_internal_studio'
|
||||
@@ -50,7 +50,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/add_internal_studio', << pipeline.git.branch >> ]
|
||||
- equal: [ 'feat/implement_bidi', << pipeline.git.branch >> ]
|
||||
- matches:
|
||||
pattern: /^release\/\d+\.\d+\.\d+$/
|
||||
value: << pipeline.git.branch >>
|
||||
@@ -61,7 +61,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/add_internal_studio', << pipeline.git.branch >> ]
|
||||
- equal: [ 'feat/implement_bidi', << pipeline.git.branch >> ]
|
||||
- matches:
|
||||
pattern: /^release\/\d+\.\d+\.\d+$/
|
||||
value: << pipeline.git.branch >>
|
||||
@@ -84,7 +84,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/add_internal_studio', << pipeline.git.branch >> ]
|
||||
- equal: [ 'feat/implement_bidi', << pipeline.git.branch >> ]
|
||||
- matches:
|
||||
pattern: /^release\/\d+\.\d+\.\d+$/
|
||||
value: << pipeline.git.branch >>
|
||||
@@ -160,7 +160,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/add_internal_studio" ]]; then
|
||||
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "feat/implement_bidi" ]]; 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
|
||||
@@ -651,10 +651,19 @@ commands:
|
||||
description: chrome channel to install
|
||||
type: string
|
||||
default: 'stable'
|
||||
firefox-version:
|
||||
description: firefox version to install
|
||||
type: string
|
||||
default: *firefox-stable-version
|
||||
inject-document-domain:
|
||||
description: run subset of tests with injectDocumentDomain config enabled
|
||||
type: boolean
|
||||
default: false
|
||||
is-firefox-cdp:
|
||||
description: whether or not the group should be associated to the firefox CDP
|
||||
run or not. This is determined by the browser version.
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
steps:
|
||||
- restore_cached_workspace
|
||||
@@ -678,6 +687,7 @@ commands:
|
||||
steps:
|
||||
- install-browsers:
|
||||
install-firefox: true
|
||||
firefox-version: << parameters.firefox-version >>
|
||||
- when:
|
||||
condition:
|
||||
equal: [ webkit, << parameters.browser >> ]
|
||||
@@ -695,6 +705,9 @@ commands:
|
||||
if << parameters.inject-document-domain >> ; then
|
||||
YARN_CMD="cypress:run:inject-document-domain"
|
||||
PARALLEL="--parallel --group 5x-driver-inject-document-domain-<<parameters.browser>>"
|
||||
elif << parameters.is-firefox-cdp >> ; then
|
||||
YARN_CMD="cypress:run"
|
||||
PARALLEL="--parallel --group 5x-driver-cdp-<<parameters.browser>>"
|
||||
else
|
||||
YARN_CMD="cypress:run"
|
||||
PARALLEL="--parallel --group 5x-driver-<<parameters.browser>>"
|
||||
@@ -2182,6 +2195,18 @@ jobs:
|
||||
- run-driver-integration-tests:
|
||||
browser: firefox
|
||||
|
||||
# Runs the driver tests using firefox 134, which does NOT use WebDriver BiDi
|
||||
# This is to test and make sure there aren't regressions with the old CDP driver
|
||||
driver-integration-tests-firefox-cdp:
|
||||
<<: *defaults
|
||||
resource_class: medium+
|
||||
parallelism: 5
|
||||
steps:
|
||||
- run-driver-integration-tests:
|
||||
browser: firefox
|
||||
firefox-version: "134.0.2"
|
||||
is-firefox-cdp: true
|
||||
|
||||
driver-integration-tests-electron:
|
||||
<<: *defaults
|
||||
parallelism: 5
|
||||
@@ -2990,6 +3015,7 @@ linux-x64-workflow: &linux-x64-workflow
|
||||
- run-webpack-dev-server-integration-tests
|
||||
- run-vite-dev-server-integration-tests
|
||||
- driver-integration-tests-firefox
|
||||
- driver-integration-tests-firefox-cdp
|
||||
- driver-integration-tests-chrome
|
||||
- driver-integration-tests-chrome-inject-document-domain
|
||||
- driver-integration-tests-chrome-beta-inject-document-domain
|
||||
@@ -3065,6 +3091,10 @@ linux-x64-workflow: &linux-x64-workflow
|
||||
context: test-runner:cypress-record-key
|
||||
requires:
|
||||
- build
|
||||
- driver-integration-tests-firefox-cdp:
|
||||
context: test-runner:cypress-record-key
|
||||
requires:
|
||||
- build
|
||||
- driver-integration-tests-electron:
|
||||
context: test-runner:cypress-record-key
|
||||
requires:
|
||||
@@ -3207,6 +3237,7 @@ linux-x64-workflow: &linux-x64-workflow
|
||||
- linux-lint
|
||||
- percy-finalize
|
||||
- driver-integration-tests-firefox
|
||||
- driver-integration-tests-firefox-cdp
|
||||
- driver-integration-tests-chrome
|
||||
- driver-integration-tests-chrome-beta
|
||||
- driver-integration-tests-chrome-inject-document-domain
|
||||
@@ -3461,6 +3492,10 @@ linux-x64-contributor-workflow: &linux-x64-contributor-workflow
|
||||
context: test-runner:cypress-record-key
|
||||
requires:
|
||||
- contributor-pr
|
||||
- driver-integration-tests-firefox-cdp:
|
||||
context: test-runner:cypress-record-key
|
||||
requires:
|
||||
- contributor-pr
|
||||
- driver-integration-tests-electron:
|
||||
context: test-runner:cypress-record-key
|
||||
requires:
|
||||
@@ -3602,6 +3637,7 @@ linux-x64-contributor-workflow: &linux-x64-contributor-workflow
|
||||
- linux-lint
|
||||
- percy-finalize
|
||||
- driver-integration-tests-firefox
|
||||
- driver-integration-tests-firefox-cdp
|
||||
- driver-integration-tests-chrome
|
||||
- driver-integration-tests-chrome-beta
|
||||
- driver-integration-tests-electron
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
|
||||
## 14.0.4
|
||||
## 14.1.0
|
||||
|
||||
_Released 2/25/2025 (PENDING)_
|
||||
|
||||
**Features:**
|
||||
|
||||
- Firefox versions 135 and up are now automated with [WebDriver BiDi](https://www.w3.org/TR/webdriver-bidi/) instead of [Chrome Devtools Protocol](https://chromedevtools.github.io/devtools-protocol/). Addresses [#30220](https://github.com/cypress-io/cypress/issues/30220).
|
||||
|
||||
**Misc:**
|
||||
|
||||
- Viewport width, height, and scale now display in a badge above the application under test. The dropdown describing how to set viewport height and width has been removed from the UI. Additionally, component tests now show a notice about URL navigation being disabled in component tests. Addresses [#30999](https://github.com/cypress-io/cypress/issues/30999). Addressed in [#31119](https://github.com/cypress-io/cypress/pull/31119).
|
||||
|
||||
@@ -711,6 +711,9 @@ describe('service workers', { defaultCommandTimeout: 1000, pageLoadTimeout: 1000
|
||||
})
|
||||
|
||||
cy.visit('fixtures/service-worker.html')
|
||||
cy.get('#output').should('have.text', 'done')
|
||||
cy.get('#output', {
|
||||
// request takes a little longer with WebDriver BiDi to return (Firefox 135+ only)
|
||||
timeout: 8000,
|
||||
}).should('have.text', 'done')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1180,6 +1180,9 @@ export const AllCypressErrors = {
|
||||
CDP_RETRYING_CONNECTION: (attempt: string | number, browserName: string, connectRetryThreshold: number) => {
|
||||
return errTemplate`Still waiting to connect to ${fmt.off(_.capitalize(browserName))}, retrying in 1 second ${fmt.meta(`(attempt ${attempt}/${connectRetryThreshold})`)}`
|
||||
},
|
||||
CDP_FIREFOX_DEPRECATED: () => {
|
||||
return errTemplate`Since Firefox 129, Chrome DevTools Protocol (CDP) has been deprecated in Firefox. In Firefox 135 and above, Cypress defaults to automating the Firefox browser with WebDriver BiDi. Cypress will no longer support CDP within Firefox in the future and is planned for removal in Cypress 15.`
|
||||
},
|
||||
BROWSER_PROCESS_CLOSED_UNEXPECTEDLY: (browserName: string) => {
|
||||
return errTemplate`\
|
||||
We detected that the ${fmt.highlight(browserName)} browser process closed unexpectedly.
|
||||
|
||||
@@ -1112,6 +1112,11 @@ describe('visual error templates', () => {
|
||||
default: [1, 'chrome', 62],
|
||||
}
|
||||
},
|
||||
CDP_FIREFOX_DEPRECATED: () => {
|
||||
return {
|
||||
default: [],
|
||||
}
|
||||
},
|
||||
BROWSER_PROCESS_CLOSED_UNEXPECTEDLY: () => {
|
||||
return {
|
||||
default: ['chrome'],
|
||||
|
||||
@@ -31,6 +31,23 @@ const checkIfFirefox = async () => {
|
||||
return name === 'Firefox'
|
||||
}
|
||||
|
||||
// this check only applies to firefox versioning!
|
||||
const isBiDiEnabled = async (config) => {
|
||||
if (!browser || !get(browser, 'runtime.getBrowserInfo') || config.IS_CDP_FORCED_FOR_FIREFOX) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { version } = await browser.runtime.getBrowserInfo()
|
||||
|
||||
if (version) {
|
||||
const [majorVersion] = version.split('.').map(Number)
|
||||
|
||||
return majorVersion >= 135
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const connect = function (host, path, extraOpts) {
|
||||
const listenToCookieChanges = once(() => {
|
||||
return browser.cookies.onChanged.addListener((info) => {
|
||||
@@ -147,10 +164,16 @@ const connect = function (host, path, extraOpts) {
|
||||
const isFirefox = await checkIfFirefox()
|
||||
|
||||
listenToCookieChanges()
|
||||
// Non-Firefox browsers use CDP for these instead
|
||||
if (isFirefox) {
|
||||
// Non-Firefox browsers use CDP for this instead
|
||||
listenToDownloads()
|
||||
listenToOnBeforeHeaders()
|
||||
// if BiDi is enabled, BiDi will handle the network interception.
|
||||
// Otherwise, CDP does not support it for Firefox and we need to listen for it here.
|
||||
const isBiDiTurnedOn = await isBiDiEnabled(config)
|
||||
|
||||
if (!isBiDiTurnedOn) {
|
||||
listenToOnBeforeHeaders()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -294,86 +294,116 @@ describe('app/background', () => {
|
||||
})
|
||||
|
||||
context('add header to aut iframe requests', () => {
|
||||
it('does not add header if it is the top frame', async function () {
|
||||
const details = {
|
||||
parentFrameId: -1,
|
||||
}
|
||||
|
||||
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
|
||||
|
||||
await this.connect()
|
||||
|
||||
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
|
||||
|
||||
expect(result).to.be.undefined
|
||||
beforeEach(() => {
|
||||
browser.runtime.getBrowserInfo = sinon.stub().resolves({ name: 'Firefox', version: '135.0.1' })
|
||||
})
|
||||
|
||||
it('does not add header if it is a nested frame', async function () {
|
||||
const details = {
|
||||
parentFrameId: 12345,
|
||||
}
|
||||
|
||||
it('allows for CDP to be used as an escape hatch if BiDi would otherwise be enabled', async function () {
|
||||
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
|
||||
|
||||
await this.connect()
|
||||
await this.connect({
|
||||
IS_CDP_FORCED_FOR_FIREFOX: true,
|
||||
})
|
||||
|
||||
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
|
||||
|
||||
expect(result).to.be.undefined
|
||||
expect(browser.webRequest.onBeforeSendHeaders.addListener).to.be.called
|
||||
})
|
||||
|
||||
it('does not add header if it is a spec frame request', async function () {
|
||||
const details = {
|
||||
parentFrameId: 0,
|
||||
type: 'sub_frame',
|
||||
url: '/__cypress/integration/spec.js',
|
||||
}
|
||||
context('BiDi enabled', () => {
|
||||
it('does not attach onBeforeSendHeaders listener if BiDi is enabled', async function () {
|
||||
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
|
||||
|
||||
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
|
||||
await this.connect()
|
||||
|
||||
await this.connect()
|
||||
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
|
||||
|
||||
expect(result).to.be.undefined
|
||||
})
|
||||
|
||||
it('appends X-Cypress-Is-AUT-Frame header to AUT iframe request', async function () {
|
||||
const details = {
|
||||
parentFrameId: 0,
|
||||
type: 'sub_frame',
|
||||
url: 'http://localhost:3000/index.html',
|
||||
requestHeaders: [
|
||||
{ name: 'X-Foo', value: 'Bar' },
|
||||
],
|
||||
}
|
||||
|
||||
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
|
||||
|
||||
await this.connect()
|
||||
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
requestHeaders: [
|
||||
{
|
||||
name: 'X-Foo',
|
||||
value: 'Bar',
|
||||
},
|
||||
{
|
||||
name: 'X-Cypress-Is-AUT-Frame',
|
||||
value: 'true',
|
||||
},
|
||||
],
|
||||
expect(browser.webRequest.onBeforeSendHeaders.addListener).not.to.be.called
|
||||
})
|
||||
})
|
||||
|
||||
it('does not add before-headers listener if in non-Firefox browser', async function () {
|
||||
browser.runtime.getBrowserInfo = undefined
|
||||
context('CDP enabled', () => {
|
||||
beforeEach(() => {
|
||||
browser.runtime.getBrowserInfo = sinon.stub().resolves({ name: 'Firefox', version: '134' })
|
||||
})
|
||||
|
||||
const onBeforeSendHeaders = sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
|
||||
it('does not add header if it is the top frame', async function () {
|
||||
const details = {
|
||||
parentFrameId: -1,
|
||||
}
|
||||
|
||||
await this.connect()
|
||||
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
|
||||
|
||||
expect(onBeforeSendHeaders).not.to.be.called
|
||||
await this.connect()
|
||||
|
||||
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
|
||||
|
||||
expect(result).to.be.undefined
|
||||
})
|
||||
|
||||
it('does not add header if it is a nested frame', async function () {
|
||||
const details = {
|
||||
parentFrameId: 12345,
|
||||
}
|
||||
|
||||
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
|
||||
|
||||
await this.connect()
|
||||
|
||||
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
|
||||
|
||||
expect(result).to.be.undefined
|
||||
})
|
||||
|
||||
it('does not add header if it is a spec frame request', async function () {
|
||||
const details = {
|
||||
parentFrameId: 0,
|
||||
type: 'sub_frame',
|
||||
url: '/__cypress/integration/spec.js',
|
||||
}
|
||||
|
||||
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
|
||||
|
||||
await this.connect()
|
||||
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
|
||||
|
||||
expect(result).to.be.undefined
|
||||
})
|
||||
|
||||
it('appends X-Cypress-Is-AUT-Frame header to AUT iframe request', async function () {
|
||||
const details = {
|
||||
parentFrameId: 0,
|
||||
type: 'sub_frame',
|
||||
url: 'http://localhost:3000/index.html',
|
||||
requestHeaders: [
|
||||
{ name: 'X-Foo', value: 'Bar' },
|
||||
],
|
||||
}
|
||||
|
||||
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
|
||||
|
||||
await this.connect()
|
||||
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
requestHeaders: [
|
||||
{
|
||||
name: 'X-Foo',
|
||||
value: 'Bar',
|
||||
},
|
||||
{
|
||||
name: 'X-Cypress-Is-AUT-Frame',
|
||||
value: 'true',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('does not add before-headers listener if in non-Firefox browser', async function () {
|
||||
browser.runtime.getBrowserInfo = undefined
|
||||
|
||||
const onBeforeSendHeaders = sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
|
||||
|
||||
await this.connect()
|
||||
|
||||
expect(onBeforeSendHeaders).not.to.be.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1129,6 +1129,7 @@ enum ErrorTypeEnum {
|
||||
CANNOT_TRASH_ASSETS
|
||||
CDP_COULD_NOT_CONNECT
|
||||
CDP_COULD_NOT_RECONNECT
|
||||
CDP_FIREFOX_DEPRECATED
|
||||
CDP_RETRYING_CONNECTION
|
||||
CDP_VERSION_TOO_OLD
|
||||
CHROME_WEB_SECURITY_NOT_SUPPORTED
|
||||
|
||||
@@ -185,6 +185,24 @@ const CalculateCredentialLevelIfApplicable: RequestMiddleware = function () {
|
||||
this.next()
|
||||
}
|
||||
|
||||
const FormatCookiesIfApplicable: RequestMiddleware = function () {
|
||||
if (this.req.headers['x-cypress-is-webdriver-bidi'] && this.req.headers.cookie) {
|
||||
const cookies = this.req.headers.cookie
|
||||
// in the case of BiDi, cookies come in as foo=bar;bar=baz and not foo=bar; bar=baz,
|
||||
// i.e. they are delimited differently, which impacts some of our tests and our cookie splicing.
|
||||
// this regex is to help make sure the cookies are fed in consistently
|
||||
const bidiStyleCookie = /;\S/gm
|
||||
|
||||
if (cookies.match(bidiStyleCookie)) {
|
||||
this.req.headers.cookie = cookies.replaceAll(';', '; ')
|
||||
}
|
||||
}
|
||||
|
||||
delete this.req.headers['x-cypress-is-webdriver-bidi']
|
||||
|
||||
return this.next()
|
||||
}
|
||||
|
||||
const MaybeAttachCrossOriginCookies: RequestMiddleware = function () {
|
||||
const span = telemetry.startSpan({ name: 'maybe:attach:cross:origin:cookies', parentSpan: this.reqMiddlewareSpan, isVerbose })
|
||||
|
||||
@@ -560,6 +578,7 @@ export default {
|
||||
MaybeSimulateSecHeaders,
|
||||
CorrelateBrowserPreRequest,
|
||||
CalculateCredentialLevelIfApplicable,
|
||||
FormatCookiesIfApplicable,
|
||||
MaybeAttachCrossOriginCookies,
|
||||
MaybeEndRequestWithBufferedResponse,
|
||||
SetMatchingRoutes,
|
||||
|
||||
@@ -186,12 +186,24 @@ export class PreRequests {
|
||||
const pendingRequest = this.pendingRequests.shift(key)
|
||||
|
||||
if (pendingRequest) {
|
||||
let cdpLagDuration; let proxyRequestCorrelationDuration = 0
|
||||
|
||||
if (browserPreRequest.cdpRequestWillBeSentReceivedTimestamp) {
|
||||
if (browserPreRequest.cdpRequestWillBeSentTimestamp) {
|
||||
cdpLagDuration = browserPreRequest.cdpRequestWillBeSentReceivedTimestamp - browserPreRequest.cdpRequestWillBeSentTimestamp
|
||||
}
|
||||
|
||||
if (pendingRequest.proxyRequestReceivedTimestamp) {
|
||||
proxyRequestCorrelationDuration = Math.max(browserPreRequest.cdpRequestWillBeSentReceivedTimestamp - pendingRequest.proxyRequestReceivedTimestamp, 0)
|
||||
}
|
||||
}
|
||||
|
||||
const timings = {
|
||||
cdpRequestWillBeSentTimestamp: browserPreRequest.cdpRequestWillBeSentTimestamp,
|
||||
cdpRequestWillBeSentReceivedTimestamp: browserPreRequest.cdpRequestWillBeSentReceivedTimestamp,
|
||||
cdpRequestWillBeSentTimestamp: browserPreRequest.cdpRequestWillBeSentTimestamp ?? 0,
|
||||
cdpRequestWillBeSentReceivedTimestamp: browserPreRequest.cdpRequestWillBeSentReceivedTimestamp ?? 0,
|
||||
proxyRequestReceivedTimestamp: pendingRequest.proxyRequestReceivedTimestamp,
|
||||
cdpLagDuration: browserPreRequest.cdpRequestWillBeSentReceivedTimestamp - browserPreRequest.cdpRequestWillBeSentTimestamp,
|
||||
proxyRequestCorrelationDuration: Math.max(browserPreRequest.cdpRequestWillBeSentReceivedTimestamp - pendingRequest.proxyRequestReceivedTimestamp, 0),
|
||||
cdpLagDuration,
|
||||
proxyRequestCorrelationDuration,
|
||||
}
|
||||
|
||||
debugVerbose('Incoming pre-request %s matches pending request. %o', key, browserPreRequest)
|
||||
@@ -221,8 +233,8 @@ export class PreRequests {
|
||||
debugVerbose('Caching pre-request %s to be matched later. %o', key, browserPreRequest)
|
||||
this.pendingPreRequests.push(key, {
|
||||
browserPreRequest,
|
||||
cdpRequestWillBeSentTimestamp: browserPreRequest.cdpRequestWillBeSentTimestamp,
|
||||
cdpRequestWillBeSentReceivedTimestamp: browserPreRequest.cdpRequestWillBeSentReceivedTimestamp,
|
||||
cdpRequestWillBeSentTimestamp: browserPreRequest.cdpRequestWillBeSentTimestamp ?? 0,
|
||||
cdpRequestWillBeSentReceivedTimestamp: browserPreRequest.cdpRequestWillBeSentReceivedTimestamp ?? 0,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -65,10 +65,10 @@ export type BrowserPreRequest = {
|
||||
originalResourceType: string | undefined
|
||||
errorHandled?: boolean
|
||||
initiator?: Protocol.Network.Initiator
|
||||
documentURL: string
|
||||
documentURL?: string
|
||||
hasRedirectResponse?: boolean
|
||||
cdpRequestWillBeSentTimestamp: number
|
||||
cdpRequestWillBeSentReceivedTimestamp: number
|
||||
cdpRequestWillBeSentTimestamp?: number
|
||||
cdpRequestWillBeSentReceivedTimestamp?: number
|
||||
}
|
||||
|
||||
export type BrowserPreRequestWithTimings = BrowserPreRequest & ProxyTimings
|
||||
|
||||
@@ -33,6 +33,7 @@ describe('http/request-middleware', () => {
|
||||
'MaybeSimulateSecHeaders',
|
||||
'CorrelateBrowserPreRequest',
|
||||
'CalculateCredentialLevelIfApplicable',
|
||||
'FormatCookiesIfApplicable',
|
||||
'MaybeAttachCrossOriginCookies',
|
||||
'MaybeEndRequestWithBufferedResponse',
|
||||
'SetMatchingRoutes',
|
||||
@@ -237,6 +238,70 @@ describe('http/request-middleware', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('FormatCookiesIfApplicable', () => {
|
||||
const { FormatCookiesIfApplicable } = RequestMiddleware
|
||||
|
||||
it('does nothing if "x-cypress-is-webdriver-bidi" header is not present', async () => {
|
||||
const ctx = {
|
||||
req: {
|
||||
headers: {
|
||||
cookie: 'foo=bar;bar=baz;qux=quux',
|
||||
},
|
||||
},
|
||||
res: {
|
||||
on: (event, listener) => {},
|
||||
off: (event, listener) => {},
|
||||
},
|
||||
}
|
||||
|
||||
await testMiddleware([FormatCookiesIfApplicable], ctx)
|
||||
|
||||
expect(ctx.req.headers['cookie']).to.equal('foo=bar;bar=baz;qux=quux')
|
||||
})
|
||||
|
||||
describe('header present', () => {
|
||||
it('does nothing if cookie header is already formatted correctly', async () => {
|
||||
const ctx = {
|
||||
req: {
|
||||
headers: {
|
||||
'x-cypress-is-webdriver-bidi': true,
|
||||
cookie: 'foo=bar; bar=baz; qux=quux',
|
||||
},
|
||||
},
|
||||
res: {
|
||||
on: (event, listener) => {},
|
||||
off: (event, listener) => {},
|
||||
},
|
||||
}
|
||||
|
||||
await testMiddleware([FormatCookiesIfApplicable], ctx)
|
||||
|
||||
expect(ctx.req.headers['cookie']).to.equal('foo=bar; bar=baz; qux=quux')
|
||||
expect(ctx.req.headers!['x-cypress-is-webdriver-bidi']).not.to.exist
|
||||
})
|
||||
|
||||
it('delimits cookie headers by "; " if no space exists between cookie values', async () => {
|
||||
const ctx = {
|
||||
req: {
|
||||
headers: {
|
||||
'x-cypress-is-webdriver-bidi': true,
|
||||
cookie: 'foo=bar;bar=baz;qux=quux',
|
||||
},
|
||||
},
|
||||
res: {
|
||||
on: (event, listener) => {},
|
||||
off: (event, listener) => {},
|
||||
},
|
||||
}
|
||||
|
||||
await testMiddleware([FormatCookiesIfApplicable], ctx)
|
||||
|
||||
expect(ctx.req.headers['cookie']).to.equal('foo=bar; bar=baz; qux=quux')
|
||||
expect(ctx.req.headers!['x-cypress-is-webdriver-bidi']).not.to.exist
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('MaybeSimulateSecHeaders', () => {
|
||||
const { MaybeSimulateSecHeaders } = RequestMiddleware
|
||||
|
||||
|
||||
277
packages/server/lib/browsers/bidi_automation.ts
Normal file
277
packages/server/lib/browsers/bidi_automation.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import debugModule from 'debug'
|
||||
import type { Automation } from '../automation'
|
||||
import type { BrowserPreRequest, BrowserResponseReceived, ResourceType } from '@packages/proxy'
|
||||
import type { Client as WebDriverClient } from 'webdriver'
|
||||
import type {
|
||||
NetworkBeforeRequestSentParameters,
|
||||
NetworkResponseStartedParameters,
|
||||
NetworkResponseCompletedParameters,
|
||||
NetworkFetchErrorParameters,
|
||||
BrowsingContextInfo,
|
||||
} from 'webdriver/build/bidi/localTypes'
|
||||
const debug = debugModule('cypress:server:browsers:bidi_automation')
|
||||
const debugVerbose = debugModule('cypress-verbose:server:browsers:bidi_automation')
|
||||
|
||||
// NOTE: these types will eventually be generated automatically via the 'webdriver' package
|
||||
// Taken from https://fetch.spec.whatwg.org/#request-initiator-type
|
||||
type RequestInitiatorType = 'audio' | 'beacon' | 'body' | 'css' | 'early-hints' | 'embed' | 'fetch' | 'font' | 'frame' | 'iframe' | 'image' | 'img' | 'input' | 'link' | 'object' | 'ping' | 'script' | 'track' | 'video' | 'xmlhttprequest' | 'other' | null
|
||||
// Taken from https://fetch.spec.whatwg.org/#concept-request-destination
|
||||
type RequestDestination = 'audio' | 'audioworklet' | 'document' | 'embed' | 'font' | 'frame' | 'iframe' | 'image' | 'json' | 'manifest' | 'object' | 'paintworklet' | 'report' | 'script' | 'serviceworker' | 'sharedworker' | 'style' | 'track' | 'video' | 'webidentity' | 'worker' | 'xslt' | ''
|
||||
|
||||
export type NetworkBeforeRequestSentParametersModified = NetworkBeforeRequestSentParameters & {
|
||||
request: {
|
||||
destination: RequestDestination
|
||||
initiatorType: RequestInitiatorType
|
||||
}
|
||||
}
|
||||
|
||||
// maps the network initiator to a ResourceType (which is initially based on CDP).
|
||||
// This provides us with consistency of types in our request/response middleware, which is important for cy.intercept().
|
||||
const normalizeResourceType = (type: RequestInitiatorType): ResourceType => {
|
||||
switch (type) {
|
||||
case 'css':
|
||||
return 'stylesheet'
|
||||
case 'xmlhttprequest':
|
||||
return 'xhr'
|
||||
case 'img':
|
||||
return 'image'
|
||||
case 'iframe':
|
||||
return 'document'
|
||||
// for types we cannot determine, we can set to other.
|
||||
case 'audio':
|
||||
case 'beacon':
|
||||
case 'body':
|
||||
case 'early-hints':
|
||||
case 'embed':
|
||||
case 'frame':
|
||||
case 'input':
|
||||
case 'link':
|
||||
case 'object':
|
||||
case 'track':
|
||||
case 'video':
|
||||
case null:
|
||||
return 'other'
|
||||
default:
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
export class BidiAutomation {
|
||||
// events needed to subscribe to in order for our BiDi automation to work properly
|
||||
static BIDI_EVENTS = [
|
||||
'network.beforeRequestSent',
|
||||
'network.responseStarted',
|
||||
'network.responseCompleted',
|
||||
'network.fetchError',
|
||||
'browsingContext.contextCreated',
|
||||
'browsingContext.contextDestroyed',
|
||||
]
|
||||
private webDriverClient: WebDriverClient
|
||||
private automation: Automation
|
||||
private autContextId: string | undefined = undefined
|
||||
// set in firefox-utils when creating the webdriver session initially and in the 'reset:browser:tabs:for:next:spec' automation hook for subsequent tests when the top level context is recreated
|
||||
private topLevelContextId: string | undefined = undefined
|
||||
private interceptId: string | undefined = undefined
|
||||
|
||||
private constructor (webDriverClient: WebDriverClient, automation: Automation) {
|
||||
this.automation = automation
|
||||
this.webDriverClient = webDriverClient
|
||||
|
||||
// bind Bidi Events to update the standard automation client
|
||||
// Error here is expected until webdriver adds initiatorType and destination to the request object
|
||||
// @ts-expect-error
|
||||
this.webDriverClient.on('network.beforeRequestSent', this.onBeforeRequestSent)
|
||||
this.webDriverClient.on('network.responseStarted', this.onResponseStarted)
|
||||
this.webDriverClient.on('network.responseCompleted', this.onResponseComplete)
|
||||
this.webDriverClient.on('network.fetchError', this.onFetchError)
|
||||
this.webDriverClient.on('browsingContext.contextCreated', this.onBrowsingContextCreated)
|
||||
this.webDriverClient.on('browsingContext.contextDestroyed', this.onBrowsingContextDestroyed)
|
||||
}
|
||||
|
||||
setTopLevelContextId = (contextId?: string) => {
|
||||
debug(`setting top level context ID to: ${contextId}`)
|
||||
this.topLevelContextId = contextId
|
||||
}
|
||||
|
||||
private onBrowsingContextCreated = async (params: BrowsingContextInfo) => {
|
||||
debugVerbose('received browsingContext.contextCreated %o', params)
|
||||
// the AUT iframe is always the FIRST child created by the top level parent (second is the reporter, if it exists which isnt the case for headless/test replay)
|
||||
if (!this.autContextId && params.parent && this.topLevelContextId === params.parent) {
|
||||
debug(`new browsing context ${params.context} created within top-level parent context ${params.parent}.`)
|
||||
debug(`setting browsing context ${params.context} as the AUT context.`)
|
||||
|
||||
this.autContextId = params.context
|
||||
|
||||
// in the case of top reloads for setting the url between specs, the AUT context gets destroyed but the top level context still exists.
|
||||
// in this case, we do NOT have to redefine the top level context intercept but instead update the autContextId to properly identify the
|
||||
// AUT in the request interceptor.
|
||||
if (!this.interceptId) {
|
||||
debugVerbose(`no interceptor defined for top-level context ${params.parent}.`)
|
||||
debugVerbose(`creating interceptor to determine if a request belongs to the AUT.`)
|
||||
// BiDi can only intercept top level tab contexts (i.e., not iframes), so the intercept needs to be defined on the top level parent, which is the AUTs
|
||||
// direct parent in ALL cases. This gets cleaned up in the 'reset:browser:tabs:for:next:spec' automation hook.
|
||||
// error looks something like: Error: WebDriver Bidi command "network.addIntercept" failed with error: invalid argument - Context with id 123456789 is not a top-level browsing context
|
||||
const { intercept } = await this.webDriverClient.networkAddIntercept({ phases: ['beforeRequestSent'], contexts: [params.parent] })
|
||||
|
||||
debugVerbose(`created network intercept ${intercept} for top-level browsing context ${params.parent}`)
|
||||
|
||||
// save a reference to the intercept ID to be cleaned up in the 'reset:browser:tabs:for:next:spec' automation hook.
|
||||
this.interceptId = intercept
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onBrowsingContextDestroyed = async (params: BrowsingContextInfo) => {
|
||||
debugVerbose('received browsingContext.contextDestroyed %o', params)
|
||||
|
||||
// if the top level context gets destroyed, we need to clear the AUT context and destroy the interceptor as it is no longer applicable
|
||||
if (params.context === this.topLevelContextId) {
|
||||
debug(`top level browsing context ${params.context} destroyed`)
|
||||
// if the top level context is destroyed, we can imply that the AUT context is destroyed along with it
|
||||
this.autContextId = undefined
|
||||
this.setTopLevelContextId(undefined)
|
||||
if (this.interceptId) {
|
||||
// since we either have:
|
||||
// 1. a new upper level browser context created above with shouldKeepTabOpen set to true.
|
||||
// 2. all the previous contexts are destroyed.
|
||||
// we should clean up our top level interceptor to prevent a memory leak as we no longer need it
|
||||
await this.webDriverClient.networkRemoveIntercept({
|
||||
intercept: this.interceptId,
|
||||
})
|
||||
|
||||
debug(`destroyed network intercept ${this.interceptId}`)
|
||||
|
||||
this.interceptId = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// if the AUT context is destroyed (possible that the top level context did not), clear the AUT context Id
|
||||
if (params.context === this.autContextId) {
|
||||
debug(`AUT browsing context ${params.context} destroyed within top-level parent context ${params.parent}.`)
|
||||
|
||||
this.autContextId = undefined
|
||||
}
|
||||
}
|
||||
|
||||
private onBeforeRequestSent = async (params: NetworkBeforeRequestSentParametersModified) => {
|
||||
debugVerbose('received network.beforeRequestSend %o', params)
|
||||
|
||||
let url = params.request.url
|
||||
|
||||
const parsedHeaders = {}
|
||||
|
||||
params.request.headers.forEach((header) => {
|
||||
parsedHeaders[header.name] = header.value.value
|
||||
})
|
||||
|
||||
const resourceType = normalizeResourceType(params.request.initiatorType)
|
||||
|
||||
const browserPreRequest: BrowserPreRequest = {
|
||||
requestId: params.request.request,
|
||||
method: params.request.method,
|
||||
url,
|
||||
headers: parsedHeaders,
|
||||
resourceType,
|
||||
originalResourceType: params.request.initiatorType || params.request.destination,
|
||||
initiator: params.initiator,
|
||||
// Since we are NOT using CDP, we set the values to -1 to indicate that we do not have this information.
|
||||
cdpRequestWillBeSentTimestamp: -1,
|
||||
cdpRequestWillBeSentReceivedTimestamp: -1,
|
||||
}
|
||||
|
||||
debugVerbose(`prerequest received for request ID ${params.request.request}: %o`, browserPreRequest)
|
||||
await this.automation.onBrowserPreRequest?.(browserPreRequest)
|
||||
|
||||
// since all requests coming from the top level context are blocked, we need to continue them here
|
||||
// we only want to mutate requests coming from the AUT frame so we can add the X-Cypress-Is-AUT-Frame header
|
||||
// so the request-middleware can identify the request
|
||||
|
||||
if (params.isBlocked) {
|
||||
params.request.headers.push({
|
||||
name: 'X-Cypress-Is-WebDriver-BiDi',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: 'true',
|
||||
},
|
||||
})
|
||||
|
||||
if (params.context === this.autContextId && resourceType === 'document') {
|
||||
debug(`AUT request detected, adding X-Cypress-Is-AUT-Frame for request ID: ${params.request.request}`)
|
||||
|
||||
params.request.headers.push({
|
||||
name: 'X-Cypress-Is-AUT-Frame',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: 'true',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
debug(`continuing request ID: ${params.request.request}`)
|
||||
|
||||
await this.webDriverClient.networkContinueRequest({
|
||||
request: params.request.request,
|
||||
headers: params.request.headers,
|
||||
cookies: params.request.cookies,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
// happens if you kill the Cypress app in the middle of request interception. This error can be ignored
|
||||
if (!(err as Error)?.message.includes('no such request')) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onResponseStarted = (params: NetworkResponseStartedParameters) => {
|
||||
debugVerbose('received network.responseStarted %o', params)
|
||||
|
||||
if (params.response.fromCache) {
|
||||
this.automation.onRemoveBrowserPreRequest?.(params.request.request)
|
||||
}
|
||||
}
|
||||
|
||||
private onResponseComplete = (params: NetworkResponseCompletedParameters) => {
|
||||
debugVerbose('received network.responseComplete %o', params)
|
||||
|
||||
if (params.response.fromCache) {
|
||||
this.automation.onRemoveBrowserPreRequest?.(params.request.request)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const parsedHeaders = {}
|
||||
|
||||
params.response.headers.forEach((header) => {
|
||||
parsedHeaders[header.name] = header.value.value
|
||||
})
|
||||
|
||||
const browserResponseReceived: BrowserResponseReceived = {
|
||||
requestId: params.request.request,
|
||||
status: params.response.status,
|
||||
headers: parsedHeaders,
|
||||
}
|
||||
|
||||
this.automation.onRequestEvent?.('response:received', browserResponseReceived)
|
||||
}
|
||||
|
||||
private onFetchError = (params: NetworkFetchErrorParameters) => {
|
||||
debugVerbose('received network.fetchError %o', params)
|
||||
|
||||
this.automation.onRemoveBrowserPreRequest?.(params.request.request)
|
||||
}
|
||||
|
||||
close () {
|
||||
this.webDriverClient.off('network.beforeRequestSent', this.onBeforeRequestSent)
|
||||
this.webDriverClient.off('network.responseStarted', this.onResponseStarted)
|
||||
this.webDriverClient.off('network.responseCompleted', this.onResponseComplete)
|
||||
this.webDriverClient.off('network.fetchError', this.onFetchError)
|
||||
this.webDriverClient.off('browsingContext.contextCreated', this.onBrowsingContextCreated)
|
||||
this.webDriverClient.off('browsingContext.contextDestroyed', this.onBrowsingContextDestroyed)
|
||||
}
|
||||
|
||||
static create (webdriverClient: WebDriverClient, automation: Automation) {
|
||||
return new BidiAutomation(webdriverClient, automation)
|
||||
}
|
||||
}
|
||||
@@ -351,12 +351,12 @@ export = {
|
||||
|
||||
// https://chromium.googlesource.com/chromium/src/+/da790f920bbc169a6805a4fb83b4c2ab09532d91
|
||||
// https://github.com/cypress-io/cypress/issues/1872
|
||||
if (majorVersion >= CHROME_VERSION_INTRODUCING_PROXY_BYPASS_ON_LOOPBACK) {
|
||||
if (Number(majorVersion) >= CHROME_VERSION_INTRODUCING_PROXY_BYPASS_ON_LOOPBACK) {
|
||||
args.push('--proxy-bypass-list=<-loopback>')
|
||||
}
|
||||
|
||||
if (isHeadless) {
|
||||
if (majorVersion >= CHROME_VERSION_INTRODUCING_HEADLESS_NEW) {
|
||||
if (Number(majorVersion) >= CHROME_VERSION_INTRODUCING_HEADLESS_NEW) {
|
||||
args.push('--headless=new')
|
||||
} else {
|
||||
args.push('--headless')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Debug from 'debug'
|
||||
import { CdpAutomation } from './cdp_automation'
|
||||
import { BidiAutomation } from './bidi_automation'
|
||||
import { BrowserCriClient } from './browser-cri-client'
|
||||
import type { Client as WebDriverClient } from 'webdriver'
|
||||
import type { Automation } from '../automation'
|
||||
@@ -9,7 +10,26 @@ const debug = Debug('cypress:server:browsers:firefox-util')
|
||||
|
||||
let webdriverClient: WebDriverClient
|
||||
|
||||
async function connectToNewTabClassic () {
|
||||
async function connectToNewSpecBiDi (options, automation: Automation, browserBiDiClient: BidiAutomation) {
|
||||
// when connecting to a new spec, we need to re register the existing bidi client to the automation client
|
||||
// as the automation client resets its middleware between specs in run mode
|
||||
debug('firefox: reconnecting to blank tab')
|
||||
const { contexts } = await webdriverClient.browsingContextGetTree({})
|
||||
|
||||
browserBiDiClient.setTopLevelContextId(contexts[0].context)
|
||||
|
||||
await options.onInitializeNewBrowserTab()
|
||||
|
||||
debug(`firefox: navigating to ${options.url}`)
|
||||
await webdriverClient.browsingContextNavigate({
|
||||
context: contexts[0].context,
|
||||
url: options.url,
|
||||
})
|
||||
}
|
||||
|
||||
async function connectToNewSpecCDP (options, automation: Automation, browserCriClient: BrowserCriClient) {
|
||||
debug('firefox: reconnecting to blank tab')
|
||||
|
||||
// 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,
|
||||
@@ -19,12 +39,6 @@ async function connectToNewTabClassic () {
|
||||
await webdriverClient.switchToWindow(handles[0])
|
||||
|
||||
await webdriverClient.navigateTo('about:blank')
|
||||
}
|
||||
|
||||
async function connectToNewSpec (options, automation: Automation, browserCriClient: BrowserCriClient) {
|
||||
debug('firefox: reconnecting to blank tab')
|
||||
|
||||
await connectToNewTabClassic()
|
||||
|
||||
debug('firefox: reconnecting CDP')
|
||||
|
||||
@@ -38,7 +52,16 @@ async function connectToNewSpec (options, automation: Automation, browserCriClie
|
||||
await options.onInitializeNewBrowserTab()
|
||||
|
||||
debug(`firefox: navigating to ${options.url}`)
|
||||
await navigateToUrlClassic(options.url)
|
||||
await webdriverClient.navigateTo(options.url)
|
||||
}
|
||||
|
||||
async function setupBiDi (webdriverClient: WebDriverClient, automation: Automation) {
|
||||
// webdriver needs to subscribe to the correct BiDi events or else the events we are expecting to stream in will not be sent
|
||||
await webdriverClient.sessionSubscribe({ events: BidiAutomation.BIDI_EVENTS })
|
||||
|
||||
const biDiClient = BidiAutomation.create(webdriverClient, automation)
|
||||
|
||||
return biDiClient
|
||||
}
|
||||
|
||||
async function setupCDP (remotePort: number, automation: Automation, onError?: (err: Error) => void): Promise<BrowserCriClient> {
|
||||
@@ -50,10 +73,6 @@ async function setupCDP (remotePort: number, automation: Automation, onError?: (
|
||||
return browserCriClient
|
||||
}
|
||||
|
||||
async function navigateToUrlClassic (url: string) {
|
||||
await webdriverClient.navigateTo(url)
|
||||
}
|
||||
|
||||
export default {
|
||||
async setup ({
|
||||
automation,
|
||||
@@ -61,27 +80,47 @@ export default {
|
||||
url,
|
||||
remotePort,
|
||||
webdriverClient: wdInstance,
|
||||
useWebDriverBiDi,
|
||||
}: {
|
||||
automation: Automation
|
||||
onError?: (err: Error) => void
|
||||
url: string
|
||||
remotePort: number
|
||||
remotePort: number | undefined
|
||||
webdriverClient: WebDriverClient
|
||||
}): Promise<BrowserCriClient> {
|
||||
useWebDriverBiDi: boolean
|
||||
}): Promise<BrowserCriClient | BidiAutomation> {
|
||||
// set the WebDriver classic instance instantiated from geckodriver
|
||||
webdriverClient = wdInstance
|
||||
const [browserCriClient] = await Promise.all([
|
||||
setupCDP(remotePort, automation, onError),
|
||||
])
|
||||
|
||||
await navigateToUrlClassic(url)
|
||||
let client: BrowserCriClient | BidiAutomation
|
||||
|
||||
return browserCriClient
|
||||
if (useWebDriverBiDi) {
|
||||
client = await setupBiDi(webdriverClient, automation)
|
||||
// use the BiDi commands to visit the url as opposed to classic webdriver
|
||||
const { contexts } = await webdriverClient.browsingContextGetTree({})
|
||||
|
||||
// at this point there should only be one context: the top level context.
|
||||
// we need to set this to bind our AUT intercepts correctly. Hopefully we can move this in the future on a more sure implementation
|
||||
client.setTopLevelContextId(contexts[0].context)
|
||||
|
||||
await webdriverClient.browsingContextNavigate({
|
||||
context: contexts[0].context,
|
||||
url,
|
||||
})
|
||||
} else {
|
||||
client = await setupCDP(remotePort as number, automation, onError)
|
||||
// uses webdriver classic to navigate
|
||||
await webdriverClient.navigateTo(url)
|
||||
}
|
||||
|
||||
return client
|
||||
},
|
||||
|
||||
connectToNewSpec,
|
||||
connectToNewSpecBiDi,
|
||||
|
||||
navigateToUrlClassic,
|
||||
connectToNewSpecCDP,
|
||||
|
||||
setupBiDi,
|
||||
|
||||
setupCDP,
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@ import utils from './utils'
|
||||
import type { Browser, BrowserInstance, GracefulShutdownOptions } from './types'
|
||||
import os from 'os'
|
||||
import mimeDb from 'mime-db'
|
||||
import type { BrowserCriClient } from './browser-cri-client'
|
||||
import { BrowserCriClient } from './browser-cri-client'
|
||||
import type { BidiAutomation } from './bidi_automation'
|
||||
import type { Automation } from '../automation'
|
||||
import { getCtx } from '@packages/data-context'
|
||||
import { getError, SerializedError } from '@packages/errors'
|
||||
import { getError, SerializedError, CypressError } from '@packages/errors'
|
||||
import type { BrowserLaunchOpts, BrowserNewTabOpts, RunModeVideoApi } from '@packages/types'
|
||||
import type { RemoteConfig } from 'webdriver'
|
||||
import type { GeckodriverParameters } from 'geckodriver'
|
||||
@@ -219,12 +220,6 @@ const defaultPreferences = {
|
||||
|
||||
'privacy.trackingprotection.enabled': false,
|
||||
|
||||
// CDP is deprecated in Firefox 129 and up.
|
||||
// In order to enable CDP, we need to set
|
||||
// remote.active-protocol=2
|
||||
// @see https://fxdx.dev/deprecating-cdp-support-in-firefox-embracing-the-future-with-webdriver-bidi/
|
||||
// @see https://github.com/cypress-io/cypress/issues/29713
|
||||
'remote.active-protocols': 2,
|
||||
// Enable Remote Agent
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1544393
|
||||
'remote.enabled': true,
|
||||
@@ -340,6 +335,22 @@ const defaultPreferences = {
|
||||
'browser.helperApps.neverAsk.saveToDisk': downloadMimeTypes,
|
||||
}
|
||||
|
||||
// CDP is deprecated in Firefox 129 and up.
|
||||
// To enable BiDi (without CDP), we need to set
|
||||
// remote.active-protocol=1
|
||||
// In order to enable CDP (without BiDi), we need to set
|
||||
// remote.active-protocol=2
|
||||
// both can be enabled via
|
||||
// remote.active-protocol=3
|
||||
// @see https://fxdx.dev/deprecating-cdp-support-in-firefox-embracing-the-future-with-webdriver-bidi/
|
||||
// @see https://github.com/cypress-io/cypress/issues/29713
|
||||
const ACTIVE_PROTOCOLS = Object.freeze({
|
||||
BIDI: 1,
|
||||
CDP: 2,
|
||||
// this key isn't actively used but checked in here if we need to turn it on for internal debugging
|
||||
CDP_AND_BIDI: 3,
|
||||
})
|
||||
|
||||
const FIREFOX_HEADED_USERCSS = `\
|
||||
#urlbar:not(.megabar), #urlbar.megabar > #urlbar-background, #searchbar {
|
||||
background: -moz-Field !important;
|
||||
@@ -377,6 +388,7 @@ toolbox {
|
||||
`
|
||||
|
||||
let browserCriClient: BrowserCriClient | undefined
|
||||
let browserBidiClient: BidiAutomation | undefined
|
||||
|
||||
/**
|
||||
* Clear instance state for the chrome instance, this is normally called in on kill or on exit.
|
||||
@@ -389,10 +401,29 @@ export function clearInstanceState (options: GracefulShutdownOptions = {}) {
|
||||
browserCriClient.close(options.gracefulShutdown).catch(() => {})
|
||||
browserCriClient = undefined
|
||||
}
|
||||
|
||||
if (browserBidiClient) {
|
||||
debug('unbinding bidi client events')
|
||||
browserBidiClient.close()
|
||||
browserBidiClient = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function shouldUseBiDi (browser: Browser): boolean {
|
||||
try {
|
||||
// Gating on firefox version 135 to turn on BiDi as this is when all of our internal Cypress tests were able to pass.
|
||||
return (browser.family === 'firefox' && !process.env.FORCE_FIREFOX_CDP && Number(browser.majorVersion) >= 135)
|
||||
} catch (err: unknown) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function connectToNewSpec (browser: Browser, options: BrowserNewTabOpts, automation: Automation) {
|
||||
await firefoxUtil.connectToNewSpec(options, automation, browserCriClient!)
|
||||
if (shouldUseBiDi(browser)) {
|
||||
await firefoxUtil.connectToNewSpecBiDi(options, automation, browserBidiClient!)
|
||||
} else {
|
||||
await firefoxUtil.connectToNewSpecCDP(options, automation, browserCriClient!)
|
||||
}
|
||||
}
|
||||
|
||||
export function connectToExisting () {
|
||||
@@ -410,6 +441,12 @@ async function recordVideo (videoApi: RunModeVideoApi) {
|
||||
}
|
||||
|
||||
export async function open (browser: Browser, url: string, options: BrowserLaunchOpts, automation: Automation): Promise<BrowserInstance> {
|
||||
const USE_WEBDRIVER_BIDI = shouldUseBiDi(browser)
|
||||
|
||||
if (!USE_WEBDRIVER_BIDI) {
|
||||
errors.warning('CDP_FIREFOX_DEPRECATED')
|
||||
}
|
||||
|
||||
const defaultLaunchOptions = utils.getDefaultLaunchOptions({
|
||||
extensions: [] as string[],
|
||||
preferences: _.extend({}, defaultPreferences),
|
||||
@@ -422,6 +459,8 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
|
||||
],
|
||||
})
|
||||
|
||||
defaultLaunchOptions.preferences['remote.active-protocols'] = USE_WEBDRIVER_BIDI ? ACTIVE_PROTOCOLS.BIDI : ACTIVE_PROTOCOLS.CDP
|
||||
|
||||
if (browser.isHeadless) {
|
||||
defaultLaunchOptions.args.push('-headless')
|
||||
// we don't need to specify width/height since MOZ_HEADLESS_ env vars will be set
|
||||
@@ -635,7 +674,7 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
|
||||
},
|
||||
// @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,
|
||||
'moz:debuggerAddress': !USE_WEBDRIVER_BIDI,
|
||||
// @see https://webdriver.io/docs/capabilities/#wdiogeckodriveroptions
|
||||
// webdriver starts geckodriver with the correct options on behalf of Cypress
|
||||
'wdio:geckodriverOptions': geckoDriverOptions,
|
||||
@@ -676,11 +715,11 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
|
||||
|
||||
try {
|
||||
browserReturnStatus = process.kill(browserPID)
|
||||
} catch (e) {
|
||||
if (e.code === 'ESRCH') {
|
||||
} catch (error: unknown) {
|
||||
if ((error as CypressError)?.code === 'ESRCH') {
|
||||
debugVerbose('browser process no longer exists. continuing...')
|
||||
} else {
|
||||
throw e
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,11 +729,11 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
|
||||
|
||||
try {
|
||||
driverReturnStatus = process.kill(driverPID)
|
||||
} catch (e) {
|
||||
if (e.code === 'ESRCH') {
|
||||
} catch (error: unknown) {
|
||||
if ((error as CypressError)?.code === 'ESRCH') {
|
||||
debugVerbose('geckodriver/webdriver process no longer exists. continuing...')
|
||||
} else {
|
||||
throw e
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -705,24 +744,28 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
|
||||
return browserReturnStatus || driverReturnStatus
|
||||
}
|
||||
|
||||
let cdpPort: number | undefined
|
||||
|
||||
if (!USE_WEBDRIVER_BIDI) {
|
||||
// In some cases, the webdriver session will NOT return the moz:debuggerAddress capability even though
|
||||
// we set it to true in the capabilities. This is out of our control, so when this happens, we fail the browser
|
||||
// and gracefully terminate the related processes and attempt to relaunch the browser in the hopes we get a
|
||||
// CDP address. @see https://github.com/cypress-io/cypress/issues/30352#issuecomment-2405701867 for more details.
|
||||
if (!webdriverClient.capabilities['moz:debuggerAddress']) {
|
||||
debug(`firefox failed to spawn with CDP connection. Failing current instance and retrying`)
|
||||
// since this fails before the instance is created, we need to kill the processes here or else they will stay open
|
||||
browserInstanceWrapper.kill()
|
||||
throw new CDPFailedToStartFirefox(`webdriver session failed to start CDP even though "moz:debuggerAddress" was provided. Please try to relaunch the browser`)
|
||||
if (!webdriverClient.capabilities['moz:debuggerAddress']) {
|
||||
debugVerbose(`firefox failed to spawn with CDP connection. Failing current instance and retrying`)
|
||||
// since this fails before the instance is created, we need to kill the processes here or else they will stay open
|
||||
browserInstanceWrapper.kill()
|
||||
throw new CDPFailedToStartFirefox(`webdriver session failed to start CDP even though "moz:debuggerAddress" was provided. Please try to relaunch the browser`)
|
||||
}
|
||||
|
||||
cdpPort = parseInt(new URL(`ws://${webdriverClient.capabilities['moz:debuggerAddress']}`).port)
|
||||
|
||||
debug(`CDP running on port ${cdpPort}`)
|
||||
|
||||
// makes it so get getRemoteDebuggingPort() is calculated correctly
|
||||
process.env.CYPRESS_REMOTE_DEBUGGING_PORT = cdpPort.toString()
|
||||
}
|
||||
|
||||
const cdpPort = parseInt(new URL(`ws://${webdriverClient.capabilities['moz:debuggerAddress']}`).port)
|
||||
|
||||
debug(`CDP running on port ${cdpPort}`)
|
||||
|
||||
// makes it so get getRemoteDebuggingPort() is calculated correctly
|
||||
process.env.CYPRESS_REMOTE_DEBUGGING_PORT = cdpPort.toString()
|
||||
|
||||
// install the browser extensions
|
||||
await Promise.all(_.map(launchOptions.extensions, async (path) => {
|
||||
debug(`installing extension at path: ${path}`)
|
||||
@@ -734,13 +777,18 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
|
||||
}))
|
||||
|
||||
debug('setting up firefox utils')
|
||||
browserCriClient = await firefoxUtil.setup({ automation, url, webdriverClient, remotePort: cdpPort, onError: options.onError })
|
||||
const client = await firefoxUtil.setup({ automation, url, webdriverClient, remotePort: cdpPort, useWebDriverBiDi: USE_WEBDRIVER_BIDI, onError: options.onError })
|
||||
|
||||
await utils.executeAfterBrowserLaunch(browser, {
|
||||
webSocketDebuggerUrl: browserCriClient.getWebSocketDebuggerUrl(),
|
||||
})
|
||||
} catch (err) {
|
||||
errors.throwErr('FIREFOX_COULD_NOT_CONNECT', err)
|
||||
if (client instanceof BrowserCriClient) {
|
||||
browserCriClient = client
|
||||
await utils.executeAfterBrowserLaunch(browser, {
|
||||
webSocketDebuggerUrl: browserCriClient.getWebSocketDebuggerUrl(),
|
||||
})
|
||||
} else {
|
||||
browserBidiClient = client
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
errors.throwErr('FIREFOX_COULD_NOT_CONNECT', err as Error)
|
||||
}
|
||||
|
||||
return browserInstanceWrapper
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Automation } from '../automation'
|
||||
import type { CDPSocketServer } from '@packages/socket/lib/cdp-socket'
|
||||
|
||||
export type Browser = FoundBrowser & {
|
||||
majorVersion: number
|
||||
majorVersion: number | string
|
||||
isHeadless: boolean
|
||||
isHeaded: boolean
|
||||
}
|
||||
|
||||
@@ -222,7 +222,9 @@ export class SocketBase {
|
||||
debug('automation:client connected')
|
||||
|
||||
// only send the necessary config
|
||||
automationClient.emit('automation:config', {})
|
||||
automationClient.emit('automation:config', {
|
||||
IS_CDP_FORCED_FOR_FIREFOX: !!process.env.FORCE_FIREFOX_CDP,
|
||||
})
|
||||
|
||||
// if our automation disconnects then we're
|
||||
// in trouble and should probably bomb everything
|
||||
|
||||
535
packages/server/test/unit/browsers/bidi_automation_spec.ts
Normal file
535
packages/server/test/unit/browsers/bidi_automation_spec.ts
Normal file
@@ -0,0 +1,535 @@
|
||||
import EventEmitter from 'node:events'
|
||||
import { BidiAutomation } from '../../../lib/browsers/bidi_automation'
|
||||
|
||||
import type { Client as WebDriverClient } from 'webdriver'
|
||||
import type { NetworkBeforeRequestSentParametersModified } from '../../../lib/browsers/bidi_automation'
|
||||
import type { Automation } from '../../../lib/automation'
|
||||
import type { NetworkFetchErrorParameters, NetworkResponseCompletedParameters, NetworkResponseStartedParameters } from 'webdriver/build/bidi/localTypes'
|
||||
|
||||
// make sure testing promises resolve before asserting on async function conditions
|
||||
const flushPromises = () => {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve()
|
||||
}, 10)
|
||||
})
|
||||
}
|
||||
|
||||
describe('lib/browsers/bidi_automation', () => {
|
||||
context('BidiAutomation', () => {
|
||||
let mockWebdriverClient: WebDriverClient
|
||||
let mockAutomationClient: Automation
|
||||
|
||||
beforeEach(() => {
|
||||
mockWebdriverClient = new EventEmitter() as WebDriverClient
|
||||
mockAutomationClient = {
|
||||
onRequestEvent: sinon.stub(),
|
||||
onBrowserPreRequest: sinon.stub().resolves(),
|
||||
onRemoveBrowserPreRequest: sinon.stub().resolves(),
|
||||
} as unknown as Automation
|
||||
})
|
||||
|
||||
it('binds BIDI_EVENTS when a new instance is created', () => {
|
||||
mockWebdriverClient.on = sinon.stub()
|
||||
|
||||
BidiAutomation.create(mockWebdriverClient, mockAutomationClient)
|
||||
|
||||
expect(mockWebdriverClient.on).to.have.been.calledWith('network.beforeRequestSent')
|
||||
expect(mockWebdriverClient.on).to.have.been.calledWith('network.responseStarted')
|
||||
expect(mockWebdriverClient.on).to.have.been.calledWith('network.responseCompleted')
|
||||
expect(mockWebdriverClient.on).to.have.been.calledWith('network.fetchError')
|
||||
expect(mockWebdriverClient.on).to.have.been.calledWith('browsingContext.contextCreated')
|
||||
expect(mockWebdriverClient.on).to.have.been.calledWith('browsingContext.contextDestroyed')
|
||||
})
|
||||
|
||||
it('unbinds BIDI_EVENTS when close() is called', () => {
|
||||
mockWebdriverClient.off = sinon.stub()
|
||||
|
||||
const bidiAutomationInstance = BidiAutomation.create(mockWebdriverClient, mockAutomationClient)
|
||||
|
||||
bidiAutomationInstance.close()
|
||||
|
||||
expect(mockWebdriverClient.off).to.have.been.calledWith('network.beforeRequestSent')
|
||||
expect(mockWebdriverClient.off).to.have.been.calledWith('network.responseStarted')
|
||||
expect(mockWebdriverClient.off).to.have.been.calledWith('network.responseCompleted')
|
||||
expect(mockWebdriverClient.off).to.have.been.calledWith('network.fetchError')
|
||||
expect(mockWebdriverClient.off).to.have.been.calledWith('browsingContext.contextCreated')
|
||||
expect(mockWebdriverClient.off).to.have.been.calledWith('browsingContext.contextDestroyed')
|
||||
})
|
||||
|
||||
describe('BrowsingContext', () => {
|
||||
describe('contextCreated / contextDestroyed', () => {
|
||||
beforeEach(() => {
|
||||
mockWebdriverClient.networkAddIntercept = sinon.stub().resolves({ intercept: 'mockInterceptId' })
|
||||
mockWebdriverClient.networkRemoveIntercept = sinon.stub().resolves()
|
||||
})
|
||||
|
||||
it('does nothing if parent context is not initially assigned', async () => {
|
||||
const bidiAutomationInstance = BidiAutomation.create(mockWebdriverClient, mockAutomationClient)
|
||||
|
||||
mockWebdriverClient.emit('browsingContext.contextCreated', {
|
||||
parent: '123',
|
||||
context: '456',
|
||||
url: 'www.foobar.com',
|
||||
userContext: '',
|
||||
children: [],
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
// @ts-expect-error
|
||||
expect(bidiAutomationInstance.autContextId).to.be.undefined
|
||||
// @ts-expect-error
|
||||
expect(bidiAutomationInstance.interceptId).to.be.undefined
|
||||
expect(mockWebdriverClient.networkAddIntercept).not.to.have.been.called
|
||||
|
||||
mockWebdriverClient.emit('browsingContext.contextDestroyed', {
|
||||
parent: '123',
|
||||
context: '456',
|
||||
url: 'www.foobar.com',
|
||||
userContext: '',
|
||||
children: [],
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(mockWebdriverClient.networkRemoveIntercept).not.to.have.been.called
|
||||
})
|
||||
|
||||
describe('correctly sets the AUT frame and intercepts requests from the frame when the top frame is set.', () => {
|
||||
it('Additionally, tears down the AUT when the contexts are destroyed', async () => {
|
||||
const bidiAutomationInstance = BidiAutomation.create(mockWebdriverClient, mockAutomationClient)
|
||||
|
||||
// manually set the top level context which happens outside the scope of the bidi_automation class
|
||||
bidiAutomationInstance.setTopLevelContextId('123')
|
||||
|
||||
// mock the creation of the AUT context
|
||||
mockWebdriverClient.emit('browsingContext.contextCreated', {
|
||||
parent: '123',
|
||||
context: '456',
|
||||
url: 'www.foobar.com',
|
||||
userContext: '',
|
||||
children: [],
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
// @ts-expect-error
|
||||
expect(bidiAutomationInstance.autContextId).to.equal('456')
|
||||
// @ts-expect-error
|
||||
expect(bidiAutomationInstance.interceptId).to.equal('mockInterceptId')
|
||||
expect(mockWebdriverClient.networkAddIntercept).to.have.been.calledWith({ phases: ['beforeRequestSent'], contexts: ['123'] })
|
||||
|
||||
// mock the destruction of the AUT context
|
||||
mockWebdriverClient.emit('browsingContext.contextDestroyed', {
|
||||
parent: '123',
|
||||
context: '456',
|
||||
url: 'www.foobar.com',
|
||||
userContext: '',
|
||||
children: [],
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
// @ts-expect-error
|
||||
expect(bidiAutomationInstance.autContextId).to.equal(undefined)
|
||||
|
||||
expect(mockWebdriverClient.networkRemoveIntercept).not.to.have.been.called
|
||||
// @ts-expect-error
|
||||
expect(bidiAutomationInstance.topLevelContextId).to.equal('123')
|
||||
})
|
||||
|
||||
it('Additionally, tears down top frame when the contexts are destroyed', async () => {
|
||||
const bidiAutomationInstance = BidiAutomation.create(mockWebdriverClient, mockAutomationClient)
|
||||
|
||||
// manually set the top level context which happens outside the scope of the bidi_automation class
|
||||
bidiAutomationInstance.setTopLevelContextId('123')
|
||||
|
||||
// mock the creation of the AUT context
|
||||
mockWebdriverClient.emit('browsingContext.contextCreated', {
|
||||
parent: '123',
|
||||
context: '456',
|
||||
url: 'www.foobar.com',
|
||||
userContext: '',
|
||||
children: [],
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
// @ts-expect-error
|
||||
expect(bidiAutomationInstance.autContextId).to.equal('456')
|
||||
// @ts-expect-error
|
||||
expect(bidiAutomationInstance.interceptId).to.equal('mockInterceptId')
|
||||
expect(mockWebdriverClient.networkAddIntercept).to.have.been.calledWith({ phases: ['beforeRequestSent'], contexts: ['123'] })
|
||||
|
||||
// Then, mock the destruction of the tab
|
||||
mockWebdriverClient.emit('browsingContext.contextDestroyed', {
|
||||
parent: null,
|
||||
context: '123',
|
||||
url: 'www.foobar.com',
|
||||
userContext: '',
|
||||
children: ['456'],
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(mockWebdriverClient.networkRemoveIntercept).to.have.been.calledWith({
|
||||
intercept: 'mockInterceptId',
|
||||
})
|
||||
|
||||
// @ts-expect-error
|
||||
expect(bidiAutomationInstance.topLevelContextId).to.be.undefined
|
||||
// @ts-expect-error
|
||||
expect(bidiAutomationInstance.interceptId).to.be.undefined
|
||||
// @ts-expect-error
|
||||
expect(bidiAutomationInstance.autContextId).to.equal(undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Network', () => {
|
||||
describe('beforeRequestSent', () => {
|
||||
let mockRequest: NetworkBeforeRequestSentParametersModified
|
||||
|
||||
beforeEach(() => {
|
||||
mockWebdriverClient.networkAddIntercept = sinon.stub().resolves({ intercept: 'mockInterceptId' })
|
||||
mockWebdriverClient.networkContinueRequest = sinon.stub().resolves()
|
||||
|
||||
mockRequest = {
|
||||
context: '123',
|
||||
isBlocked: true,
|
||||
navigation: 'foo',
|
||||
redirectCount: 0,
|
||||
request: {
|
||||
request: 'request1',
|
||||
url: 'https://www.foobar.com',
|
||||
method: 'GET',
|
||||
headers: [
|
||||
{
|
||||
name: 'foo',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
cookies: [
|
||||
{
|
||||
name: 'baz',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: 'bar',
|
||||
},
|
||||
domain: '.foobar.com',
|
||||
path: '/',
|
||||
size: 3,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
expiry: 12345,
|
||||
},
|
||||
],
|
||||
headersSize: 5,
|
||||
bodySize: 10,
|
||||
timings: null,
|
||||
destination: 'script',
|
||||
initiatorType: 'xmlhttprequest',
|
||||
},
|
||||
timestamp: 1234567,
|
||||
intercepts: ['mockIntercept'],
|
||||
initiator: {
|
||||
type: 'preflight',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('correctly pauses the AUT frame to add the X-Cypress-Is-AUT-Frame header (which is later stripped out in the middleware)', async () => {
|
||||
const bidiAutomationInstance = BidiAutomation.create(mockWebdriverClient, mockAutomationClient)
|
||||
|
||||
// manually set the top level context which happens outside the scope of the bidi_automation class
|
||||
bidiAutomationInstance.setTopLevelContextId('123')
|
||||
|
||||
// mock the creation of the AUT context
|
||||
mockWebdriverClient.emit('browsingContext.contextCreated', {
|
||||
parent: '123',
|
||||
context: '456',
|
||||
url: 'www.foobar.com',
|
||||
userContext: '',
|
||||
children: [],
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
mockRequest.request.headers = []
|
||||
mockRequest.request.cookies = []
|
||||
mockRequest.context = '456'
|
||||
mockRequest.request.destination = 'iframe'
|
||||
mockRequest.request.initiatorType = 'iframe'
|
||||
mockRequest.initiator.type = 'other'
|
||||
|
||||
mockWebdriverClient.emit('network.beforeRequestSent', mockRequest)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(mockAutomationClient.onBrowserPreRequest).to.have.been.calledWith({
|
||||
requestId: 'request1',
|
||||
method: 'GET',
|
||||
url: 'https://www.foobar.com',
|
||||
resourceType: 'document',
|
||||
originalResourceType: 'iframe',
|
||||
initiator: {
|
||||
type: 'other',
|
||||
},
|
||||
headers: {},
|
||||
cdpRequestWillBeSentTimestamp: -1,
|
||||
cdpRequestWillBeSentReceivedTimestamp: -1,
|
||||
})
|
||||
|
||||
expect(mockWebdriverClient.networkContinueRequest).to.have.been.calledWith({
|
||||
request: 'request1',
|
||||
headers: [
|
||||
{
|
||||
name: 'X-Cypress-Is-WebDriver-BiDi',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: 'true',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'X-Cypress-Is-AUT-Frame',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: 'true',
|
||||
},
|
||||
},
|
||||
],
|
||||
cookies: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('correctly calculates the browser pre-request for the middleware', async () => {
|
||||
BidiAutomation.create(mockWebdriverClient, mockAutomationClient)
|
||||
|
||||
mockWebdriverClient.emit('network.beforeRequestSent', mockRequest)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(mockAutomationClient.onBrowserPreRequest).to.have.been.calledWith({
|
||||
requestId: 'request1',
|
||||
method: 'GET',
|
||||
url: 'https://www.foobar.com',
|
||||
resourceType: 'xhr',
|
||||
originalResourceType: 'xmlhttprequest',
|
||||
initiator: {
|
||||
type: 'preflight',
|
||||
},
|
||||
headers: {
|
||||
foo: 'bar',
|
||||
},
|
||||
cdpRequestWillBeSentTimestamp: -1,
|
||||
cdpRequestWillBeSentReceivedTimestamp: -1,
|
||||
})
|
||||
|
||||
expect(mockWebdriverClient.networkContinueRequest).to.have.been.calledWith({
|
||||
request: 'request1',
|
||||
headers: [
|
||||
{
|
||||
name: 'foo',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: 'bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'X-Cypress-Is-WebDriver-BiDi',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: 'true',
|
||||
},
|
||||
},
|
||||
],
|
||||
cookies: [
|
||||
{
|
||||
name: 'baz',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: 'bar',
|
||||
},
|
||||
domain: '.foobar.com',
|
||||
path: '/',
|
||||
size: 3,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
expiry: 12345,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('swallows "no such request" messages if thrown via killing the Cypress app', () => {
|
||||
BidiAutomation.create(mockWebdriverClient, mockAutomationClient)
|
||||
|
||||
mockWebdriverClient.networkContinueRequest = sinon.stub().throws('no such request')
|
||||
|
||||
expect(() => {
|
||||
mockWebdriverClient.emit('network.beforeRequestSent', mockRequest)
|
||||
}).not.to.throw()
|
||||
})
|
||||
})
|
||||
|
||||
describe('responseStarted / responseCompleted', () => {
|
||||
let mockRequest: NetworkResponseStartedParameters & NetworkResponseCompletedParameters
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = {
|
||||
context: '123',
|
||||
isBlocked: true,
|
||||
navigation: 'foo',
|
||||
redirectCount: 0,
|
||||
request: {
|
||||
request: 'request123',
|
||||
url: 'https://www.foobar.com',
|
||||
method: 'GET',
|
||||
headers: [
|
||||
{
|
||||
name: 'foo',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
cookies: [
|
||||
{
|
||||
name: 'baz',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: 'bar',
|
||||
},
|
||||
domain: '.foobar.com',
|
||||
path: '/',
|
||||
size: 3,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
expiry: 12345,
|
||||
},
|
||||
],
|
||||
headersSize: 5,
|
||||
bodySize: 10,
|
||||
timings: null,
|
||||
},
|
||||
timestamp: 1234567,
|
||||
intercepts: ['mockIntercept'],
|
||||
response: {
|
||||
url: 'https://www.foobar.com',
|
||||
protocol: 'tcp',
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
fromCache: true,
|
||||
headers: [],
|
||||
mimeType: 'application/json',
|
||||
bytesReceived: 47,
|
||||
headersSize: 6,
|
||||
bodySize: 20,
|
||||
content: {
|
||||
size: 60,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const CACHE_EVENTS = ['network.responseStarted', 'network.responseCompleted']
|
||||
|
||||
CACHE_EVENTS.forEach((CACHE_EVENT) => {
|
||||
it(`removes browser pre-request if served from cache (${CACHE_EVENT})`, async () => {
|
||||
BidiAutomation.create(mockWebdriverClient, mockAutomationClient)
|
||||
|
||||
mockWebdriverClient.emit(CACHE_EVENT, mockRequest)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(mockAutomationClient.onRemoveBrowserPreRequest).to.have.been.calledWith('request123')
|
||||
})
|
||||
})
|
||||
|
||||
it('calls onRequestEvent "response:received" when a response is completed', async () => {
|
||||
BidiAutomation.create(mockWebdriverClient, mockAutomationClient)
|
||||
|
||||
mockRequest.response.fromCache = false
|
||||
|
||||
mockWebdriverClient.emit('network.responseCompleted', mockRequest)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(mockAutomationClient.onRequestEvent).to.have.been.calledWith('response:received', {
|
||||
requestId: 'request123',
|
||||
status: 200,
|
||||
headers: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchError', () => {
|
||||
let mockRequest: NetworkFetchErrorParameters
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = {
|
||||
context: '123',
|
||||
isBlocked: true,
|
||||
navigation: 'foo',
|
||||
redirectCount: 0,
|
||||
request: {
|
||||
request: 'request123',
|
||||
url: 'https://www.foobar.com',
|
||||
method: 'GET',
|
||||
headers: [
|
||||
{
|
||||
name: 'foo',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
cookies: [
|
||||
{
|
||||
name: 'baz',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: 'bar',
|
||||
},
|
||||
domain: '.foobar.com',
|
||||
path: '/',
|
||||
size: 3,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
expiry: 12345,
|
||||
},
|
||||
],
|
||||
headersSize: 5,
|
||||
bodySize: 10,
|
||||
timings: null,
|
||||
},
|
||||
timestamp: 1234567,
|
||||
intercepts: ['mockIntercept'],
|
||||
errorText: 'the request could not be completed!',
|
||||
}
|
||||
})
|
||||
|
||||
it('calls onRemoveBrowserPreRequest when a request errors', async () => {
|
||||
BidiAutomation.create(mockWebdriverClient, mockAutomationClient)
|
||||
|
||||
mockWebdriverClient.emit('network.fetchError', mockRequest)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(mockAutomationClient.onRemoveBrowserPreRequest).to.have.been.calledWith('request123')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,7 @@ import { BrowserCriClient } from '../../../lib/browsers/browser-cri-client'
|
||||
import { ICriClient } from '../../../lib/browsers/cri-client'
|
||||
import { type Client as WebDriverClient, default as webdriver } from 'webdriver'
|
||||
import { EventEmitter } from 'stream'
|
||||
import { BidiAutomation } from '../../../lib/browsers/bidi_automation'
|
||||
|
||||
const path = require('path')
|
||||
const mockfs = require('mock-fs')
|
||||
@@ -22,8 +23,10 @@ const specUtil = require('../../specUtils')
|
||||
|
||||
describe('lib/browsers/firefox', () => {
|
||||
const port = 3333
|
||||
const mockContextId = '1234-5678'
|
||||
let wdInstance: sinon.SinonStubbedInstance<WebDriverClient>
|
||||
let browserCriClient: BrowserCriClient
|
||||
let bidiAutomationClient: BidiAutomation
|
||||
|
||||
afterEach(() => {
|
||||
return mockfs.restore()
|
||||
@@ -42,18 +45,34 @@ describe('lib/browsers/firefox', () => {
|
||||
getWindowHandles: sinon.stub(),
|
||||
switchToWindow: sinon.stub(),
|
||||
navigateTo: sinon.stub(),
|
||||
sessionSubscribe: sinon.stub(),
|
||||
browsingContextGetTree: sinon.stub(),
|
||||
browsingContextNavigate: sinon.stub(),
|
||||
capabilities: {
|
||||
'moz:debuggerAddress': '127.0.0.1:12345',
|
||||
// @ts-expect-error
|
||||
'moz:processID': 1234,
|
||||
'wdio:driverPID': 5678,
|
||||
},
|
||||
on: sinon.stub(),
|
||||
off: sinon.stub(),
|
||||
}
|
||||
|
||||
wdInstance.maximizeWindow.resolves(undefined)
|
||||
wdInstance.installAddOn.resolves(undefined)
|
||||
wdInstance.switchToWindow.resolves(undefined)
|
||||
wdInstance.navigateTo.resolves(undefined)
|
||||
wdInstance.sessionSubscribe.resolves(undefined)
|
||||
wdInstance.browsingContextNavigate.resolves(undefined)
|
||||
wdInstance.browsingContextGetTree.resolves({
|
||||
contexts: [{
|
||||
context: mockContextId,
|
||||
children: null,
|
||||
url: '',
|
||||
userContext: mockContextId,
|
||||
parent: null,
|
||||
}],
|
||||
})
|
||||
|
||||
sinon.stub(webdriver, 'newSession').resolves(wdInstance)
|
||||
})
|
||||
@@ -96,6 +115,11 @@ describe('lib/browsers/firefox', () => {
|
||||
|
||||
sinon.stub(BrowserCriClient, 'create').resolves(browserCriClient)
|
||||
sinon.stub(CdpAutomation, 'create').resolves()
|
||||
|
||||
bidiAutomationClient = sinon.createStubInstance(BidiAutomation)
|
||||
bidiAutomationClient.setTopLevelContextId = sinon.stub().returns(undefined)
|
||||
|
||||
sinon.stub(BidiAutomation, 'create').returns(bidiAutomationClient)
|
||||
})
|
||||
|
||||
context('#connectToNewSpec', () => {
|
||||
@@ -104,7 +128,7 @@ describe('lib/browsers/firefox', () => {
|
||||
this.options.onInitializeNewBrowserTab = sinon.stub()
|
||||
})
|
||||
|
||||
it('calls connectToNewSpec in firefoxUtil', async function () {
|
||||
it('CDP: calls connectToNewSpecCDP in firefoxUtil', async function () {
|
||||
wdInstance.getWindowHandles.resolves(['mock-context-id'])
|
||||
await firefox.open(this.browser, 'http://', this.options, this.automation)
|
||||
|
||||
@@ -120,6 +144,25 @@ describe('lib/browsers/firefox', () => {
|
||||
// second time when navigating to the spec
|
||||
expect(wdInstance.navigateTo).to.have.been.calledWith('next-spec-url')
|
||||
})
|
||||
|
||||
it('BiDi: calls connectToNewSpecBiDi in firefoxUtil', async function () {
|
||||
this.browser.family = 'firefox'
|
||||
this.browser.majorVersion = '135'
|
||||
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(wdInstance.browsingContextGetTree).to.have.been.calledWith({})
|
||||
expect(bidiAutomationClient.setTopLevelContextId).to.have.been.calledWith(mockContextId)
|
||||
|
||||
// Only happens one time when navigating to the spec since the context gets created on about:blank, which is tested in BidiAutomation
|
||||
expect(wdInstance.browsingContextNavigate).to.have.been.calledWith({
|
||||
context: mockContextId,
|
||||
url: 'next-spec-url',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('executes before:browser:launch if registered', async function () {
|
||||
@@ -202,75 +245,19 @@ describe('lib/browsers/firefox', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('creates the WebDriver session and geckodriver instance through capabilities, installs the extension, and passes the correct port to CDP', async function () {
|
||||
await firefox.open(this.browser, 'http://', this.options, this.automation)
|
||||
expect(webdriver.newSession).to.have.been.calledWith({
|
||||
logLevel: 'silent',
|
||||
capabilities: sinon.match({
|
||||
alwaysMatch: {
|
||||
browserName: 'firefox',
|
||||
acceptInsecureCerts: true,
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions
|
||||
'moz:firefoxOptions': {
|
||||
binary: '/path/to/binary',
|
||||
args: [
|
||||
'-new-instance',
|
||||
'-start-debugger-server',
|
||||
'-no-remote',
|
||||
...(os.platform() !== 'linux' ? ['-foreground'] : []),
|
||||
],
|
||||
// only partially match the preferences object because it is so large
|
||||
prefs: sinon.match({
|
||||
'remote.active-protocols': 2,
|
||||
'remote.enabled': true,
|
||||
}),
|
||||
},
|
||||
'moz:debuggerAddress': true,
|
||||
'wdio:geckodriverOptions': {
|
||||
host: '127.0.0.1',
|
||||
marionetteHost: '127.0.0.1',
|
||||
marionettePort: sinon.match(Number),
|
||||
websocketPort: sinon.match(Number),
|
||||
profileRoot: '/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: '720',
|
||||
},
|
||||
}),
|
||||
jsdebugger: false,
|
||||
log: 'error',
|
||||
logNoTruncate: false,
|
||||
|
||||
},
|
||||
},
|
||||
firstMatch: [],
|
||||
}),
|
||||
})
|
||||
|
||||
expect(wdInstance.installAddOn).to.have.been.calledWith('/path/to/ext', true)
|
||||
|
||||
expect(wdInstance.navigateTo).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 })
|
||||
})
|
||||
|
||||
describe('debugging', () => {
|
||||
afterEach(() => {
|
||||
debug.disable()
|
||||
})
|
||||
|
||||
it('sets additional arguments if "DEBUG=cypress-verbose:server:browsers:geckodriver" and "DEBUG=cypress-verbose:server:browsers:webdriver" is set', async function () {
|
||||
debug.enable('cypress-verbose:server:browsers:geckodriver,cypress-verbose:server:browsers:webdriver')
|
||||
|
||||
await firefox.open(this.browser, 'http://', this.options, this.automation)
|
||||
|
||||
expect(webdriver.newSession).to.have.been.calledWith({
|
||||
logLevel: 'info',
|
||||
describe(`webdriver capabilities`, () => {
|
||||
const getExpectedCapabilities = ({
|
||||
shouldUseBiDi,
|
||||
isDebugEnabled,
|
||||
}: {
|
||||
shouldUseBiDi?: boolean
|
||||
isDebugEnabled?: boolean
|
||||
} = {
|
||||
shouldUseBiDi: false,
|
||||
isDebugEnabled: false,
|
||||
}) => {
|
||||
return {
|
||||
logLevel: isDebugEnabled ? 'info' : 'silent',
|
||||
capabilities: sinon.match({
|
||||
alwaysMatch: {
|
||||
browserName: 'firefox',
|
||||
@@ -285,12 +272,12 @@ describe('lib/browsers/firefox', () => {
|
||||
...(os.platform() !== 'linux' ? ['-foreground'] : []),
|
||||
],
|
||||
// only partially match the preferences object because it is so large
|
||||
prefs: sinon.match({
|
||||
'remote.active-protocols': 2,
|
||||
prefs: {
|
||||
'remote.active-protocols': shouldUseBiDi ? 1 : 2,
|
||||
'remote.enabled': true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
'moz:debuggerAddress': true,
|
||||
'moz:debuggerAddress': !shouldUseBiDi,
|
||||
'wdio:geckodriverOptions': {
|
||||
host: '127.0.0.1',
|
||||
marionetteHost: '127.0.0.1',
|
||||
@@ -298,29 +285,89 @@ describe('lib/browsers/firefox', () => {
|
||||
websocketPort: sinon.match(Number),
|
||||
profileRoot: '/path/to/appData/firefox-stable/interactive',
|
||||
binaryPath: undefined,
|
||||
spawnOpts: sinon.match({
|
||||
spawnOpts: {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: {
|
||||
MOZ_REMOTE_SETTINGS_DEVTOOLS: '1',
|
||||
MOZ_HEADLESS_WIDTH: '1280',
|
||||
MOZ_HEADLESS_HEIGHT: '720',
|
||||
},
|
||||
}),
|
||||
jsdebugger: true,
|
||||
log: 'debug',
|
||||
logNoTruncate: true,
|
||||
},
|
||||
jsdebugger: !!isDebugEnabled,
|
||||
log: isDebugEnabled ? 'debug' : 'error',
|
||||
logNoTruncate: !!isDebugEnabled,
|
||||
},
|
||||
},
|
||||
firstMatch: [],
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
describe(`creates the WebDriver session and geckodriver instance through capabilities, installs the extension, and passes the correct port to CDP`, function () {
|
||||
it('for CDP', async function () {
|
||||
await firefox.open(this.browser, 'http://', this.options, this.automation)
|
||||
expect(webdriver.newSession).to.have.been.calledWith((getExpectedCapabilities({ shouldUseBiDi: false })))
|
||||
|
||||
expect(wdInstance.installAddOn).to.have.been.calledWith('/path/to/ext', true)
|
||||
|
||||
expect(wdInstance.navigateTo).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 })
|
||||
})
|
||||
|
||||
expect(wdInstance.installAddOn).to.have.been.calledWith('/path/to/ext', true)
|
||||
it('for BiDi', async function () {
|
||||
this.browser.family = 'firefox'
|
||||
this.browser.majorVersion = '135'
|
||||
await firefox.open(this.browser, 'http://', this.options, this.automation)
|
||||
expect(webdriver.newSession).to.have.been.calledWith((getExpectedCapabilities({ shouldUseBiDi: true })))
|
||||
|
||||
expect(wdInstance.navigateTo).to.have.been.calledWith('http://')
|
||||
expect(wdInstance.installAddOn).to.have.been.calledWith('/path/to/ext', true)
|
||||
|
||||
// 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 })
|
||||
expect(wdInstance.sessionSubscribe).to.be.calledWith({ events: [
|
||||
'network.beforeRequestSent',
|
||||
'network.responseStarted',
|
||||
'network.responseCompleted',
|
||||
'network.fetchError',
|
||||
'browsingContext.contextCreated',
|
||||
'browsingContext.contextDestroyed',
|
||||
] })
|
||||
|
||||
expect(wdInstance.browsingContextGetTree).to.be.calledWith({})
|
||||
|
||||
expect(wdInstance.browsingContextNavigate).to.have.been.calledWith({
|
||||
context: mockContextId,
|
||||
url: 'http://',
|
||||
})
|
||||
|
||||
// make sure Bidi gets created
|
||||
expect(BidiAutomation.create).to.be.calledWith(wdInstance, this.automation)
|
||||
expect(bidiAutomationClient.setTopLevelContextId).to.be.calledWith(mockContextId)
|
||||
})
|
||||
})
|
||||
|
||||
describe('debugging: sets additional arguments if "DEBUG=cypress-verbose:server:browsers:geckodriver" and "DEBUG=cypress-verbose:server:browsers:webdriver" is set', () => {
|
||||
afterEach(() => {
|
||||
debug.disable()
|
||||
})
|
||||
|
||||
it('for CDP', async function () {
|
||||
debug.enable('cypress-verbose:server:browsers:geckodriver,cypress-verbose:server:browsers:webdriver')
|
||||
|
||||
await firefox.open(this.browser, 'http://', this.options, this.automation)
|
||||
|
||||
expect(webdriver.newSession).to.have.been.calledWith((getExpectedCapabilities({ isDebugEnabled: true })))
|
||||
})
|
||||
|
||||
it('for BiDi', async function () {
|
||||
this.browser.family = 'firefox'
|
||||
this.browser.majorVersion = '135'
|
||||
debug.enable('cypress-verbose:server:browsers:geckodriver,cypress-verbose:server:browsers:webdriver')
|
||||
|
||||
await firefox.open(this.browser, 'http://', this.options, this.automation)
|
||||
|
||||
expect(webdriver.newSession).to.have.been.calledWith((getExpectedCapabilities({ isDebugEnabled: true, shouldUseBiDi: true })))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -444,21 +491,37 @@ describe('lib/browsers/firefox', () => {
|
||||
}), this.options)
|
||||
})
|
||||
|
||||
// CDP is deprecated in Firefox 129 and up.
|
||||
// In order to enable CDP, we need to set
|
||||
// remote.active-protocol=2
|
||||
// @see https://fxdx.dev/deprecating-cdp-support-in-firefox-embracing-the-future-with-webdriver-bidi/
|
||||
// @see https://github.com/cypress-io/cypress/issues/29713
|
||||
it('sets "remote.active-protocols"=2 to keep CDP enabled for firefox versions 129 and up', async function () {
|
||||
const executeBeforeBrowserLaunchSpy = sinon.spy(utils, 'executeBeforeBrowserLaunch')
|
||||
describe('sets "remote.active-protocols"', function () {
|
||||
// CDP is deprecated in Firefox 129 and up.
|
||||
// In order to enable CDP, we need to set
|
||||
// remote.active-protocol=2
|
||||
// @see https://fxdx.dev/deprecating-cdp-support-in-firefox-embracing-the-future-with-webdriver-bidi/
|
||||
// @see https://github.com/cypress-io/cypress/issues/29713
|
||||
it('=2 to enable only CDP', async function () {
|
||||
const executeBeforeBrowserLaunchSpy = sinon.spy(utils, 'executeBeforeBrowserLaunch')
|
||||
|
||||
await firefox.open(this.browser, 'http://', this.options, this.automation)
|
||||
await firefox.open(this.browser, 'http://', this.options, this.automation)
|
||||
|
||||
expect(executeBeforeBrowserLaunchSpy).to.have.been.calledWith(this.browser, sinon.match({
|
||||
preferences: {
|
||||
'remote.active-protocols': 2,
|
||||
},
|
||||
}), this.options)
|
||||
expect(executeBeforeBrowserLaunchSpy).to.have.been.calledWith(this.browser, sinon.match({
|
||||
preferences: {
|
||||
'remote.active-protocols': 2,
|
||||
},
|
||||
}), this.options)
|
||||
})
|
||||
|
||||
it('=1 to enable only BiDi', async function () {
|
||||
this.browser.family = 'firefox'
|
||||
this.browser.majorVersion = '135'
|
||||
const executeBeforeBrowserLaunchSpy = sinon.spy(utils, 'executeBeforeBrowserLaunch')
|
||||
|
||||
await firefox.open(this.browser, 'http://', this.options, this.automation)
|
||||
|
||||
expect(executeBeforeBrowserLaunchSpy).to.have.been.calledWith(this.browser, sinon.match({
|
||||
preferences: {
|
||||
'remote.active-protocols': 1,
|
||||
},
|
||||
}), this.options)
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves the browser instance as an event emitter', async function () {
|
||||
@@ -619,7 +682,7 @@ describe('lib/browsers/firefox', () => {
|
||||
})
|
||||
|
||||
context('firefox-util', () => {
|
||||
context('#setupRemote', function () {
|
||||
context('#setupCDP', function () {
|
||||
it('correctly sets up the remote agent', async function () {
|
||||
const criClientStub: ICriClient = {
|
||||
targetId: '',
|
||||
|
||||
@@ -56,7 +56,7 @@ export interface LaunchArgs {
|
||||
onFocusTests?: () => any
|
||||
}
|
||||
|
||||
type NullableMiddlewareHook = (() => void) | null
|
||||
type NullableMiddlewareHook = ((message: unknown, data: unknown) => void) | null
|
||||
|
||||
export type OnRequestEvent = (eventName: string, data: any) => void
|
||||
|
||||
|
||||
55
system-tests/__snapshots__/cdp_deprecated_firefox_spec.ts.js
Normal file
55
system-tests/__snapshots__/cdp_deprecated_firefox_spec.ts.js
Normal file
@@ -0,0 +1,55 @@
|
||||
exports['CDP deprecated in Firefox / logs a warning to the user that CDP is deprecated and will be removed in Cypress 15'] = `
|
||||
|
||||
====================================================================================================
|
||||
|
||||
(Run Starting)
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Cypress: 1.2.3 │
|
||||
│ Browser: FooBrowser 88 │
|
||||
│ Specs: 1 found (simple_passing.cy.js) │
|
||||
│ Searched: cypress/e2e/simple_passing.cy.js │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Running: simple_passing.cy.js (1 of 1)
|
||||
Since Firefox 129, Chrome DevTools Protocol (CDP) has been deprecated in Firefox. In Firefox 135 and above, Cypress defaults to automating the Firefox browser with WebDriver BiDi. Cypress will no longer support CDP within Firefox in the future and is planned for removal in Cypress 15.
|
||||
|
||||
|
||||
simple passing spec
|
||||
✓ passes
|
||||
|
||||
|
||||
1 passing
|
||||
|
||||
|
||||
(Results)
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Tests: 1 │
|
||||
│ Passing: 1 │
|
||||
│ Failing: 0 │
|
||||
│ Pending: 0 │
|
||||
│ Skipped: 0 │
|
||||
│ Screenshots: 0 │
|
||||
│ Video: false │
|
||||
│ Duration: X seconds │
|
||||
│ Spec Ran: simple_passing.cy.js │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
====================================================================================================
|
||||
|
||||
(Run Finished)
|
||||
|
||||
|
||||
Spec Tests Passing Failing Pending Skipped
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ✔ simple_passing.cy.js XX:XX 1 1 - - - │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
✔ All specs passed! XX:XX 1 1 - - -
|
||||
|
||||
|
||||
`
|
||||
18
system-tests/test/cdp_deprecated_firefox_spec.ts
Normal file
18
system-tests/test/cdp_deprecated_firefox_spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import systemTests from '../lib/system-tests'
|
||||
|
||||
describe('CDP deprecated in Firefox', () => {
|
||||
systemTests.setup()
|
||||
|
||||
systemTests.it('logs a warning to the user that CDP is deprecated and will be removed in Cypress 15', {
|
||||
browser: 'firefox',
|
||||
processEnv: {
|
||||
FORCE_FIREFOX_CDP: '1',
|
||||
},
|
||||
expectedExitCode: 0,
|
||||
snapshot: true,
|
||||
spec: 'simple_passing.cy.js',
|
||||
onStdout: (stdout) => {
|
||||
expect(stdout).to.include('Since Firefox 129, Chrome DevTools Protocol (CDP) has been deprecated in Firefox. In Firefox 135 and above, Cypress defaults to automating the Firefox browser with WebDriver BiDi. Cypress will no longer support CDP within Firefox in the future and is planned for removal in Cypress 15.')
|
||||
},
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user