Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

draft: Bidi branch cdp #31064

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/cache-version.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Bump this version to force CI to re-create the cache from scratch.

2-10-25
2-10-25-BIDI
11 changes: 6 additions & 5 deletions .circleci/workflows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: 2.1

chrome-stable-version: &chrome-stable-version "133.0.6943.53"
chrome-beta-version: &chrome-beta-version "134.0.6998.3"
firefox-stable-version: &firefox-stable-version "135.0"
firefox-stable-version: &firefox-stable-version "129.0.2"

orbs:
browser-tools: circleci/[email protected]
Expand Down Expand Up @@ -36,6 +36,7 @@ mainBuildFilters: &mainBuildFilters
only:
- develop
- /^release\/\d+\.\d+\.\d+$/
- 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'

Expand All @@ -48,7 +49,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: [ 'chore/browser_spike', << pipeline.git.branch >> ]
- equal: [ 'feat/implement_bidi', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
Expand All @@ -59,7 +60,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: [ 'chore/browser_spike', << pipeline.git.branch >> ]
- equal: [ 'feat/implement_bidi', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
Expand All @@ -82,7 +83,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: [ 'fix/em_dash_priv_command', << pipeline.git.branch >> ]
- equal: [ 'feat/implement_bidi', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
Expand Down Expand Up @@ -158,7 +159,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/"* ]]; 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
Expand Down
6 changes: 5 additions & 1 deletion cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 14.0.3
## 14.1.0

_Released 2/11/2025 (PENDING)_

**Features:**

- Firefox version 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).

**Bugfixes:**

- Fixed an issue in Cypress [`14.0.2`](https://docs.cypress.io/guides/references/changelog#14-0-2) where privileged commands did not run correctly when a spec file or support file contained certain encoded characters. Fixes [#31034](https://github.com/cypress-io/cypress/issues/31034) and [#31060](https://github.com/cypress-io/cypress/issues/31060).
Expand Down
5 changes: 4 additions & 1 deletion packages/driver/cypress/e2e/e2e/service-worker.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
timeout: 8000,
}).should('have.text', 'done')
})
})
23 changes: 21 additions & 2 deletions packages/extension/app/v2/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ const checkIfFirefox = async () => {
return name === 'Firefox'
}

// this check only applies to firefox versioning!
const isBiDiEnabled = async () => {
if (!browser || !get(browser, 'runtime.getBrowserInfo')) {
return false
}

const { version } = await browser.runtime.getBrowserInfo()

const majorVersion = parseInt(version)

return majorVersion >= 135
}

const connect = function (host, path, extraOpts) {
const listenToCookieChanges = once(() => {
return browser.cookies.onChanged.addListener((info) => {
Expand Down Expand Up @@ -147,10 +160,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()

if (!isBiDiTurnedOn) {
listenToOnBeforeHeaders()
}
}
})

Expand Down
138 changes: 79 additions & 59 deletions packages/extension/test/integration/v2/background_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,86 +294,106 @@ 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')
context('BiDi enabled', () => {
beforeEach(() => {
browser.runtime.getBrowserInfo = sinon.stub().resolves({ name: 'Firefox', version: '135' })
})

await this.connect()
it('does not attach onBeforeSendHeaders listener if BiDi is enabled', async function () {
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')

const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
await this.connect()

expect(result).to.be.undefined
expect(browser.webRequest.onBeforeSendHeaders.addListener).not.to.be.called
})
})

it('does not add header if it is a nested frame', async function () {
const details = {
parentFrameId: 12345,
}
context('CDP enabled', () => {
beforeEach(() => {
browser.runtime.getBrowserInfo = sinon.stub().resolves({ name: 'Firefox', version: '134' })
})

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')

const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
await this.connect()

expect(result).to.be.undefined
})
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)

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',
}
expect(result).to.be.undefined
})

sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
it('does not add header if it is a nested frame', async function () {
const details = {
parentFrameId: 12345,
}

await this.connect()
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')

expect(result).to.be.undefined
})
await this.connect()

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' },
],
}
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)

sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
expect(result).to.be.undefined
})

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 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('does not add before-headers listener if in non-Firefox browser', async function () {
browser.runtime.getBrowserInfo = 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' },
],
}

const onBeforeSendHeaders = 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(onBeforeSendHeaders).not.to.be.called
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
})
})
})

Expand Down
17 changes: 17 additions & 0 deletions packages/proxy/lib/http/request-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,22 @@ const CalculateCredentialLevelIfApplicable: RequestMiddleware = function () {
this.next()
}

const FormatCookiesIfApplicable: RequestMiddleware = function () {
if (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 matches = cookies.match(/;\S/gm)

if (matches) {
this.req.headers['cookie'] = cookies.split(';').join('; ')
}
}

return this.next()
}

const MaybeAttachCrossOriginCookies: RequestMiddleware = function () {
const span = telemetry.startSpan({ name: 'maybe:attach:cross:origin:cookies', parentSpan: this.reqMiddlewareSpan, isVerbose })

Expand Down Expand Up @@ -560,6 +576,7 @@ export default {
MaybeSimulateSecHeaders,
CorrelateBrowserPreRequest,
CalculateCredentialLevelIfApplicable,
FormatCookiesIfApplicable,
MaybeAttachCrossOriginCookies,
MaybeEndRequestWithBufferedResponse,
SetMatchingRoutes,
Expand Down
24 changes: 18 additions & 6 deletions packages/proxy/lib/http/util/prerequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
})
}

Expand Down
6 changes: 3 additions & 3 deletions packages/proxy/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading