mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-09 16:50:23 -06:00
fix: "bypass" proxying network requests from extra browser tabs/windows (#28188)
Co-authored-by: Ryan Manuel <ryanm@cypress.io> Co-authored-by: Matt Schile <mschile@cypress.io>
This commit is contained in:
@@ -106,6 +106,7 @@ const READONLY_MIDDLEWARE_KEYS: (keyof HttpMiddlewareThis<{}>)[] = [
|
||||
'onResponse',
|
||||
'onError',
|
||||
'skipMiddleware',
|
||||
'onlyRunMiddleware',
|
||||
]
|
||||
|
||||
export type HttpMiddlewareThis<T> = HttpMiddlewareCtx<T> & ServerCtx & Readonly<{
|
||||
@@ -119,6 +120,7 @@ export type HttpMiddlewareThis<T> = HttpMiddlewareCtx<T> & ServerCtx & Readonly<
|
||||
onResponse: (incomingRes: IncomingMessage, resStream: Readable) => void
|
||||
onError: (error: Error) => void
|
||||
skipMiddleware: (name: string) => void
|
||||
onlyRunMiddleware: (names: string[]) => void
|
||||
}>
|
||||
|
||||
export function _runStage (type: HttpStages, ctx: any, onError: Function) {
|
||||
@@ -220,9 +222,12 @@ export function _runStage (type: HttpStages, ctx: any, onError: Function) {
|
||||
_end()
|
||||
},
|
||||
onError: _onError,
|
||||
skipMiddleware: (name) => {
|
||||
skipMiddleware: (name: string) => {
|
||||
ctx.middleware[type] = _.omit(ctx.middleware[type], name)
|
||||
},
|
||||
onlyRunMiddleware: (names: string[]) => {
|
||||
ctx.middleware[type] = _.pick(ctx.middleware[type], names)
|
||||
},
|
||||
...ctx,
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { doesTopNeedToBeSimulated } from './util/top-simulation'
|
||||
|
||||
import type { HttpMiddleware } from './'
|
||||
import type { CypressIncomingRequest } from '../types'
|
||||
|
||||
// do not use a debug namespace in this file - use the per-request `this.debug` instead
|
||||
// available as cypress-verbose:proxy:http
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -31,15 +32,30 @@ const ExtractCypressMetadataHeaders: RequestMiddleware = function () {
|
||||
const span = telemetry.startSpan({ name: 'extract:cypress:metadata:headers', parentSpan: this.reqMiddlewareSpan, isVerbose })
|
||||
|
||||
this.req.isAUTFrame = !!this.req.headers['x-cypress-is-aut-frame']
|
||||
|
||||
span?.setAttributes({
|
||||
isAUTFrame: this.req.isAUTFrame,
|
||||
})
|
||||
this.req.isFromExtraTarget = !!this.req.headers['x-cypress-is-from-extra-target']
|
||||
|
||||
if (this.req.headers['x-cypress-is-aut-frame']) {
|
||||
delete this.req.headers['x-cypress-is-aut-frame']
|
||||
}
|
||||
|
||||
span?.setAttributes({
|
||||
isAUTFrame: this.req.isAUTFrame,
|
||||
isFromExtraTarget: this.req.isFromExtraTarget,
|
||||
})
|
||||
|
||||
// we only want to intercept requests from the main target and not ones from
|
||||
// extra tabs or windows, so run the bare minimum request/response middleware
|
||||
// to send the request/response directly through
|
||||
if (this.req.isFromExtraTarget) {
|
||||
this.debug('request for [%s %s] is from an extra target', this.req.method, this.req.proxiedUrl)
|
||||
|
||||
delete this.req.headers['x-cypress-is-from-extra-target']
|
||||
|
||||
this.onlyRunMiddleware([
|
||||
'SendRequestOutgoing',
|
||||
])
|
||||
}
|
||||
|
||||
span?.end()
|
||||
this.next()
|
||||
}
|
||||
|
||||
@@ -167,6 +167,27 @@ const LogResponse: ResponseMiddleware = function () {
|
||||
this.next()
|
||||
}
|
||||
|
||||
const FilterNonProxiedResponse: ResponseMiddleware = function () {
|
||||
// if the request is from an extra target (i.e. not the main Cypress tab, but
|
||||
// an extra tab/window), we want to skip any manipulation of the response and
|
||||
// only run the middleware necessary to get it back to the browser
|
||||
if (this.req.isFromExtraTarget) {
|
||||
this.debug('response for [%s %s] is from extra target', this.req.method, this.req.proxiedUrl)
|
||||
|
||||
this.onlyRunMiddleware([
|
||||
'AttachPlainTextStreamFn',
|
||||
'PatchExpressSetHeader',
|
||||
'MaybeSendRedirectToClient',
|
||||
'CopyResponseStatusCode',
|
||||
'MaybeEndWithEmptyBody',
|
||||
'GzipBody',
|
||||
'SendResponseBodyToClient',
|
||||
])
|
||||
}
|
||||
|
||||
this.next()
|
||||
}
|
||||
|
||||
const AttachPlainTextStreamFn: ResponseMiddleware = function () {
|
||||
this.makeResStreamPlainText = function () {
|
||||
const span = telemetry.startSpan({ name: 'make:res:stream:plain:text', parentSpan: this.resMiddlewareSpan, isVerbose })
|
||||
@@ -869,6 +890,7 @@ const SendResponseBodyToClient: ResponseMiddleware = function () {
|
||||
|
||||
export default {
|
||||
LogResponse,
|
||||
FilterNonProxiedResponse,
|
||||
AttachPlainTextStreamFn,
|
||||
InterceptResponse,
|
||||
PatchExpressSetHeader,
|
||||
|
||||
@@ -18,6 +18,7 @@ export type CypressIncomingRequest = Request & {
|
||||
followRedirect?: boolean
|
||||
isAUTFrame: boolean
|
||||
credentialsLevel?: RequestCredentialLevel
|
||||
isFromExtraTarget: boolean
|
||||
/**
|
||||
* Resource type from browserPreRequest. Copied to req so intercept matching can work.
|
||||
*/
|
||||
|
||||
@@ -33,49 +33,66 @@ describe('http/request-middleware', () => {
|
||||
describe('ExtractCypressMetadataHeaders', () => {
|
||||
const { ExtractCypressMetadataHeaders } = RequestMiddleware
|
||||
|
||||
it('removes x-cypress-is-aut-frame header when it exists, sets in on the req', async () => {
|
||||
const ctx = {
|
||||
function prepareContext (headers = {}) {
|
||||
return {
|
||||
getAUTUrl: sinon.stub().returns('http://localhost:8080'),
|
||||
onlyRunMiddleware: sinon.stub(),
|
||||
remoteStates: {
|
||||
isPrimarySuperDomainOrigin: sinon.stub().returns(false),
|
||||
},
|
||||
req: {
|
||||
headers: {
|
||||
'x-cypress-is-aut-frame': 'true',
|
||||
},
|
||||
headers,
|
||||
} as Partial<CypressIncomingRequest>,
|
||||
res: {
|
||||
on: (event, listener) => {},
|
||||
off: (event, listener) => {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
|
||||
.then(() => {
|
||||
expect(ctx.req.headers['x-cypress-is-aut-frame']).not.to.exist
|
||||
expect(ctx.req.isAUTFrame).to.be.true
|
||||
context('x-cypress-is-aut-frame', () => {
|
||||
it('when it exists, removes header and sets in on the req', async () => {
|
||||
const ctx = prepareContext({
|
||||
'x-cypress-is-aut-frame': 'true',
|
||||
})
|
||||
|
||||
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
|
||||
.then(() => {
|
||||
expect(ctx.req.headers!['x-cypress-is-aut-frame']).not.to.exist
|
||||
expect(ctx.req.isAUTFrame).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
it('when it does not exist, sets in on the req', async () => {
|
||||
const ctx = prepareContext()
|
||||
|
||||
await testMiddleware([ExtractCypressMetadataHeaders], ctx).then(() => {
|
||||
expect(ctx.req.headers!['x-cypress-is-aut-frame']).not.to.exist
|
||||
expect(ctx.req.isAUTFrame).to.be.false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('removes x-cypress-is-aut-frame header when it does not exist, sets in on the req', async () => {
|
||||
const ctx = {
|
||||
getAUTUrl: sinon.stub().returns('http://localhost:8080'),
|
||||
remoteStates: {
|
||||
isPrimarySuperDomainOrigin: sinon.stub().returns(false),
|
||||
},
|
||||
req: {
|
||||
headers: {},
|
||||
} as Partial<CypressIncomingRequest>,
|
||||
res: {
|
||||
on: (event, listener) => {},
|
||||
off: (event, listener) => {},
|
||||
},
|
||||
}
|
||||
context('x-cypress-is-from-extra-target', () => {
|
||||
it('when it exists, sets in on the req and only runs necessary middleware', async () => {
|
||||
const ctx = prepareContext({
|
||||
'x-cypress-is-from-extra-target': 'true',
|
||||
})
|
||||
|
||||
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
|
||||
.then(() => {
|
||||
expect(ctx.req.headers['x-cypress-is-aut-frame']).not.to.exist
|
||||
expect(ctx.req.isAUTFrame).to.be.false
|
||||
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
|
||||
|
||||
expect(ctx.req.headers!['x-cypress-is-from-extra-target']).not.to.exist
|
||||
expect(ctx.req.isFromExtraTarget).to.be.true
|
||||
expect(ctx.onlyRunMiddleware).to.be.calledWith(['SendRequestOutgoing'])
|
||||
})
|
||||
|
||||
it('when it does not exist, removes header and sets in on the req', async () => {
|
||||
const ctx = prepareContext()
|
||||
|
||||
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
|
||||
|
||||
expect(ctx.req.headers!['x-cypress-is-from-extra-target']).not.to.exist
|
||||
expect(ctx.req.isFromExtraTarget).to.be.false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('http/response-middleware', function () {
|
||||
it('exports the members in the correct order', function () {
|
||||
expect(_.keys(ResponseMiddleware)).to.have.ordered.members([
|
||||
'LogResponse',
|
||||
'FilterNonProxiedResponse',
|
||||
'AttachPlainTextStreamFn',
|
||||
'InterceptResponse',
|
||||
'PatchExpressSetHeader',
|
||||
@@ -99,6 +100,47 @@ describe('http/response-middleware', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('FilterNonProxiedResponse', () => {
|
||||
const { FilterNonProxiedResponse } = ResponseMiddleware
|
||||
let ctx
|
||||
|
||||
beforeEach(() => {
|
||||
ctx = {
|
||||
onlyRunMiddleware: sinon.stub(),
|
||||
req: {},
|
||||
res: {
|
||||
off: (event, listener) => {},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('runs minimal subsequent middleware if request is from an extra target', () => {
|
||||
ctx.req.isFromExtraTarget = true
|
||||
|
||||
return testMiddleware([FilterNonProxiedResponse], ctx)
|
||||
.then(() => {
|
||||
expect(ctx.onlyRunMiddleware).to.be.calledWith([
|
||||
'AttachPlainTextStreamFn',
|
||||
'PatchExpressSetHeader',
|
||||
'MaybeSendRedirectToClient',
|
||||
'CopyResponseStatusCode',
|
||||
'MaybeEndWithEmptyBody',
|
||||
'GzipBody',
|
||||
'SendResponseBodyToClient',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('runs all subsequent middleware if request is not from an extra target', () => {
|
||||
ctx.req.isFromMainTarget = false
|
||||
|
||||
return testMiddleware([FilterNonProxiedResponse], ctx)
|
||||
.then(() => {
|
||||
expect(ctx.onlyRunMiddleware).not.to.be.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('MaybeStripDocumentDomainFeaturePolicy', function () {
|
||||
const { MaybeStripDocumentDomainFeaturePolicy } = ResponseMiddleware
|
||||
let ctx
|
||||
|
||||
@@ -47,7 +47,11 @@ interface ManageTabsOptions {
|
||||
|
||||
interface AttachedToTargetOptions {
|
||||
browserClient: CriClient
|
||||
browserCriClient: BrowserCriClient
|
||||
CriConstructor?: typeof CRI
|
||||
event: Protocol.Target.AttachedToTargetEvent
|
||||
host: string
|
||||
port: number
|
||||
protocolManager?: ProtocolManagerShape
|
||||
}
|
||||
|
||||
@@ -159,6 +163,13 @@ const retryWithIncreasingDelay = async <T>(retryable: () => Promise<T>, browserN
|
||||
return retry()
|
||||
}
|
||||
|
||||
type TargetId = string
|
||||
|
||||
interface ExtraTarget {
|
||||
client: CRI.Client
|
||||
targetInfo: Protocol.Target.TargetInfo
|
||||
}
|
||||
|
||||
export class BrowserCriClient {
|
||||
private browserClient: CriClient
|
||||
private versionInfo: CRI.VersionResult
|
||||
@@ -177,6 +188,7 @@ export class BrowserCriClient {
|
||||
closed = false
|
||||
resettingBrowserTargets = false
|
||||
gracefulShutdown?: Boolean
|
||||
extraTargetClients: Map<TargetId, ExtraTarget> = new Map()
|
||||
onClose: Function | null = null
|
||||
|
||||
private constructor ({ browserClient, versionInfo, host, port, browserName, onAsynchronousError, protocolManager, fullyManageTabs }: BrowserCriClientOptions) {
|
||||
@@ -244,7 +256,7 @@ export class BrowserCriClient {
|
||||
]
|
||||
|
||||
browserClient.on('Target.attachedToTarget', async (event: Protocol.Target.AttachedToTargetEvent) => {
|
||||
await this._onAttachToTarget({ browserClient, event, protocolManager })
|
||||
await this._onAttachToTarget({ browserClient, browserCriClient, event, host, port, protocolManager })
|
||||
})
|
||||
|
||||
browserClient.on('Target.targetDestroyed', (event: Protocol.Target.TargetDestroyedEvent) => {
|
||||
@@ -255,22 +267,115 @@ export class BrowserCriClient {
|
||||
}
|
||||
|
||||
static async _onAttachToTarget (options: AttachedToTargetOptions) {
|
||||
const { browserClient, event, protocolManager } = options
|
||||
const { browserClient, browserCriClient, CriConstructor, event, host, port, protocolManager } = options
|
||||
const CreateCRI = CriConstructor || CRI
|
||||
const { sessionId, targetInfo, waitingForDebugger } = event
|
||||
let { targetId, url } = targetInfo
|
||||
|
||||
debug('Target.attachedToTarget %o', event.targetInfo)
|
||||
debug('Target.attachedToTarget %o', targetInfo)
|
||||
|
||||
try {
|
||||
if (event.targetInfo.type !== 'page') {
|
||||
// The basic approach here is we attach to targets and enable network traffic
|
||||
// We must attach in a paused state so that we can enable network traffic before the target starts running.
|
||||
if (targetInfo.type !== 'page') {
|
||||
await browserClient.send('Network.enable', protocolManager?.networkEnableOptions ?? DEFAULT_NETWORK_ENABLE_OPTIONS, event.sessionId)
|
||||
}
|
||||
|
||||
if (event.waitingForDebugger) {
|
||||
await browserClient.send('Runtime.runIfWaitingForDebugger', undefined, event.sessionId)
|
||||
}
|
||||
} catch (error) {
|
||||
// it's possible that the target was closed before we could enable network and continue, in that case, just ignore
|
||||
debug('error attaching to target browser', error)
|
||||
// it's possible that the target was closed before we could enable
|
||||
// network and continue, in that case, just ignore
|
||||
debug('error running Network.enable:', error)
|
||||
}
|
||||
|
||||
if (!waitingForDebugger) {
|
||||
debug('Not waiting for debugger (id: %s)', targetId)
|
||||
|
||||
// a target created before we started listening won't be waiting
|
||||
// for the debugger and is therefore not an extra target
|
||||
return
|
||||
}
|
||||
|
||||
async function run () {
|
||||
try {
|
||||
await browserClient.send('Runtime.runIfWaitingForDebugger', undefined, sessionId)
|
||||
} catch (error) {
|
||||
// it's possible that the target was closed before we could enable
|
||||
// network and continue, in that case, just ignore
|
||||
debug('error running Runtime.runIfWaitingForDebugger:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// the url often isn't specified with this event, so we get it
|
||||
// from Target.getTargets
|
||||
if (!url) {
|
||||
const { targetInfos } = await browserClient.send('Target.getTargets')
|
||||
|
||||
const thisTarget = targetInfos.find((target) => target.targetId === targetId)
|
||||
|
||||
if (thisTarget) {
|
||||
url = thisTarget.url
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
// if resetting browser targets, the first target attached to is the
|
||||
// main Cypress tab, but hasn't been set as
|
||||
// browserCriClient.currentlyAttachedTarget yet
|
||||
browserCriClient.resettingBrowserTargets
|
||||
// is the main Cypress tab
|
||||
|| targetId === browserCriClient.currentlyAttachedTarget?.targetId
|
||||
// is not a tab/window, such as a service worker
|
||||
|| targetInfo.type !== 'page'
|
||||
// is DevTools
|
||||
|| url.includes('devtools://')
|
||||
// is the Launchpad
|
||||
|| url.includes('__launchpad')
|
||||
// is chrome extension service worker
|
||||
|| url.includes('chrome-extension://')
|
||||
) {
|
||||
debug('Not an extra target (id: %s)', targetId)
|
||||
|
||||
// in these cases, we don't want to track the targets as extras.
|
||||
// we're only interested in extra tabs or windows
|
||||
return await run()
|
||||
}
|
||||
|
||||
debug('Connect as extra target (id: %s)', targetId)
|
||||
|
||||
let extraTargetCriClient
|
||||
|
||||
try {
|
||||
extraTargetCriClient = await CreateCRI({
|
||||
host,
|
||||
port,
|
||||
target: targetId,
|
||||
local: true,
|
||||
useHostName: true,
|
||||
})
|
||||
} catch (err: any) {
|
||||
debug('Errored connecting to target (id: %s): %s', targetId, err?.stack || err)
|
||||
|
||||
return await run()
|
||||
}
|
||||
|
||||
browserCriClient.addExtraTargetClient(targetInfo, extraTargetCriClient)
|
||||
|
||||
await extraTargetCriClient.send('Fetch.enable')
|
||||
|
||||
// we mark extra targets with this header, so that the proxy can recognize
|
||||
// where they came from and run only the minimal middleware necessary
|
||||
extraTargetCriClient.on('Fetch.requestPaused', async (params: Protocol.Fetch.RequestPausedEvent) => {
|
||||
const details: Protocol.Fetch.ContinueRequestRequest = {
|
||||
requestId: params.requestId,
|
||||
headers: [{ name: 'X-Cypress-Is-From-Extra-Target', value: 'true' }],
|
||||
}
|
||||
|
||||
extraTargetCriClient.send('Fetch.continueRequest', details).catch((err) => {
|
||||
// swallow this error so it doesn't crash Cypress
|
||||
debug('continueRequest failed, url: %s, error: %s', params.request.url, err?.stack || err)
|
||||
})
|
||||
})
|
||||
|
||||
await run()
|
||||
}
|
||||
|
||||
static _onTargetDestroyed ({ browserClient, browserCriClient, browserName, event, onAsynchronousError }: TargetDestroyedOptions) {
|
||||
@@ -281,9 +386,17 @@ export class BrowserCriClient {
|
||||
resettingBrowserTargets: browserCriClient.resettingBrowserTargets,
|
||||
})
|
||||
|
||||
// we may have gotten a delayed "Target.targetDestroyed" even for a page that we
|
||||
// have already closed/disposed, so unless this matches our current target then bail
|
||||
if (event.targetId !== browserCriClient.currentlyAttachedTarget?.targetId) {
|
||||
const { targetId } = event
|
||||
|
||||
if (targetId !== browserCriClient.currentlyAttachedTarget?.targetId) {
|
||||
if (browserCriClient.hasExtraTargetClient(targetId)) {
|
||||
debug('Close extra target client (id: %s)')
|
||||
browserCriClient.getExtraTargetClient(targetId)!.client.close().catch(() => { })
|
||||
browserCriClient.removeExtraTargetClient(targetId)
|
||||
}
|
||||
|
||||
// we may have gotten a delayed "Target.targetDestroyed" event for a page that we
|
||||
// have already closed/disposed, so unless this matches our current target then bail
|
||||
return
|
||||
}
|
||||
|
||||
@@ -333,7 +446,7 @@ export class BrowserCriClient {
|
||||
errors.throwErr('BROWSER_PROCESS_CLOSED_UNEXPECTEDLY', browserName)
|
||||
})
|
||||
.catch(Bluebird.TimeoutError, () => {
|
||||
debug('browser websocket did not close, page was closed %o', { targetId: event.targetId })
|
||||
debug('browser websocket did not close, page was closed %o', { targetId })
|
||||
// the browser websocket didn't close meaning
|
||||
// only the page was closed, not the browser
|
||||
errors.throwErr('BROWSER_PAGE_CLOSED_UNEXPECTEDLY', browserName)
|
||||
@@ -444,6 +557,22 @@ export class BrowserCriClient {
|
||||
this.resettingBrowserTargets = false
|
||||
}
|
||||
|
||||
addExtraTargetClient (targetInfo: Protocol.Target.TargetInfo, client: CRI.Client) {
|
||||
this.extraTargetClients.set(targetInfo.targetId, { client, targetInfo })
|
||||
}
|
||||
|
||||
hasExtraTargetClient (targetId: TargetId) {
|
||||
return this.extraTargetClients.has(targetId)
|
||||
}
|
||||
|
||||
getExtraTargetClient (targetId: TargetId) {
|
||||
return this.extraTargetClients.get(targetId)
|
||||
}
|
||||
|
||||
removeExtraTargetClient (targetId: TargetId) {
|
||||
this.extraTargetClients.delete(targetId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the browser client socket as well as the socket for the currently attached page target
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as protocol from '../../../lib/browsers/protocol'
|
||||
import { stripAnsi } from '@packages/errors'
|
||||
import net from 'net'
|
||||
import { ProtocolManagerShape } from '@packages/types'
|
||||
import type { Protocol } from 'devtools-protocol'
|
||||
|
||||
const HOST = '127.0.0.1'
|
||||
const PORT = 50505
|
||||
@@ -129,6 +130,252 @@ describe('lib/browsers/cri-client', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('._onAttachToTarget', () => {
|
||||
let options: any
|
||||
|
||||
beforeEach(() => {
|
||||
options = {
|
||||
browserClient: {
|
||||
send: sinon.stub(),
|
||||
},
|
||||
browserCriClient: {
|
||||
addExtraTargetClient: sinon.stub(),
|
||||
currentlyAttachedTarget: {
|
||||
targetId: 'main-target-id',
|
||||
},
|
||||
resettingBrowserTargets: false,
|
||||
},
|
||||
CriConstructor: sinon.stub(),
|
||||
event: {
|
||||
sessionId: 'session-id',
|
||||
targetInfo: {
|
||||
targetId: 'target-id',
|
||||
type: 'page',
|
||||
url: 'http://the.url',
|
||||
} as Protocol.Target.TargetInfo,
|
||||
waitingForDebugger: true,
|
||||
},
|
||||
host: 'localhost',
|
||||
port: 1234,
|
||||
}
|
||||
})
|
||||
|
||||
it('is a noop if not waiting for debugger', async () => {
|
||||
options.event.waitingForDebugger = false
|
||||
|
||||
await BrowserCriClient._onAttachToTarget(options as any)
|
||||
|
||||
expect(options.browserClient.send).not.to.be.called
|
||||
})
|
||||
|
||||
it('gets url from Target.getTargets if not in event', async () => {
|
||||
options.event.targetInfo.url = ''
|
||||
|
||||
options.browserClient.send.withArgs('Target.getTargets').resolves({
|
||||
targetInfos: [{
|
||||
targetId: 'target-id',
|
||||
url: 'devtools://some.devtools',
|
||||
}],
|
||||
})
|
||||
|
||||
options.browserClient.send.withArgs('Runtime.runIfWaitingForDebugger').resolves()
|
||||
|
||||
await BrowserCriClient._onAttachToTarget(options as any)
|
||||
|
||||
expect(options.browserClient.send).to.be.calledWith('Target.getTargets')
|
||||
})
|
||||
|
||||
it('is a noop sending Runtime.runIfWaitingForDebugger if resetting browser targets', async () => {
|
||||
options.browserCriClient.resettingBrowserTargets = true
|
||||
options.browserClient.send.withArgs('Runtime.runIfWaitingForDebugger').resolves()
|
||||
|
||||
await BrowserCriClient._onAttachToTarget(options as any)
|
||||
|
||||
expect(options.CriConstructor).not.to.be.called
|
||||
expect(options.browserClient.send).to.be.calledWith('Runtime.runIfWaitingForDebugger', undefined, 'session-id')
|
||||
})
|
||||
|
||||
it('is a noop sending Runtime.runIfWaitingForDebugger if target is the main Cypress tab', async () => {
|
||||
options.event.targetInfo.targetId = 'main-target-id'
|
||||
options.browserClient.send.withArgs('Runtime.runIfWaitingForDebugger').resolves()
|
||||
|
||||
await BrowserCriClient._onAttachToTarget(options as any)
|
||||
|
||||
expect(options.CriConstructor).not.to.be.called
|
||||
expect(options.browserClient.send).to.be.calledWith('Runtime.runIfWaitingForDebugger', undefined, 'session-id')
|
||||
})
|
||||
|
||||
it('is a noop sending Runtime.runIfWaitingForDebugger if target is not a tab or window', async () => {
|
||||
options.event.targetInfo.type = 'service_worker'
|
||||
options.browserClient.send.withArgs('Runtime.runIfWaitingForDebugger').resolves()
|
||||
|
||||
await BrowserCriClient._onAttachToTarget(options as any)
|
||||
|
||||
expect(options.CriConstructor).not.to.be.called
|
||||
expect(options.browserClient.send).to.be.calledWith('Runtime.runIfWaitingForDebugger', undefined, 'session-id')
|
||||
})
|
||||
|
||||
it('is a noop sending Runtime.runIfWaitingForDebugger if target is DevTools', async () => {
|
||||
options.event.targetInfo.url = 'devtools://dev.tools'
|
||||
options.browserClient.send.withArgs('Runtime.runIfWaitingForDebugger').resolves()
|
||||
|
||||
await BrowserCriClient._onAttachToTarget(options as any)
|
||||
|
||||
expect(options.CriConstructor).not.to.be.called
|
||||
expect(options.browserClient.send).to.be.calledWith('Runtime.runIfWaitingForDebugger', undefined, 'session-id')
|
||||
})
|
||||
|
||||
it('is a noop sending Runtime.runIfWaitingForDebugger if target is the Launchpad', async () => {
|
||||
options.event.targetInfo.url = 'http://localhost:1234/__launchpad'
|
||||
options.browserClient.send.withArgs('Runtime.runIfWaitingForDebugger').resolves()
|
||||
|
||||
await BrowserCriClient._onAttachToTarget(options as any)
|
||||
|
||||
expect(options.CriConstructor).not.to.be.called
|
||||
expect(options.browserClient.send).to.be.calledWith('Runtime.runIfWaitingForDebugger', undefined, 'session-id')
|
||||
})
|
||||
|
||||
it('is a noop sending Runtime.runIfWaitingForDebugger if part of a chrome extension', async () => {
|
||||
options.event.targetInfo.url = 'chrome-extension://some.extension'
|
||||
options.browserClient.send.withArgs('Runtime.runIfWaitingForDebugger').resolves()
|
||||
|
||||
await BrowserCriClient._onAttachToTarget(options as any)
|
||||
|
||||
expect(options.CriConstructor).not.to.be.called
|
||||
expect(options.browserClient.send).to.be.calledWith('Runtime.runIfWaitingForDebugger', undefined, 'session-id')
|
||||
})
|
||||
|
||||
it('is a noop sending Runtime.runIfWaitingForDebugger if connecting to target errors', async () => {
|
||||
options.CriConstructor.rejects(new Error('failed to connect'))
|
||||
options.browserClient.send.withArgs('Runtime.runIfWaitingForDebugger').resolves()
|
||||
|
||||
await BrowserCriClient._onAttachToTarget(options as any)
|
||||
|
||||
expect(options.CriConstructor).to.be.called
|
||||
expect(options.browserCriClient.addExtraTargetClient).not.to.be.called
|
||||
expect(options.browserClient.send).to.be.calledWith('Runtime.runIfWaitingForDebugger', undefined, 'session-id')
|
||||
})
|
||||
|
||||
it('connects to target and sends Fetch.enable', async () => {
|
||||
const criClient = {
|
||||
send: sinon.stub(),
|
||||
on: sinon.stub(),
|
||||
}
|
||||
|
||||
options.CriConstructor.returns(criClient)
|
||||
options.browserClient.send.withArgs('Fetch.enable').resolves()
|
||||
options.browserClient.send.withArgs('Runtime.runIfWaitingForDebugger').resolves()
|
||||
|
||||
await BrowserCriClient._onAttachToTarget(options as any)
|
||||
|
||||
expect(options.CriConstructor).to.be.called
|
||||
expect(options.browserCriClient.addExtraTargetClient).to.be.calledWith(options.event.targetInfo, criClient)
|
||||
expect(criClient.send).to.be.calledWith('Fetch.enable')
|
||||
expect(criClient.on).to.be.calledWith('Fetch.requestPaused', sinon.match.func)
|
||||
expect(options.browserClient.send).to.be.calledWith('Runtime.runIfWaitingForDebugger', undefined, 'session-id')
|
||||
})
|
||||
|
||||
it('adds X-Cypress-Is-From-Extra-Target header to requests from extra target', async () => {
|
||||
const criClient = {
|
||||
send: sinon.stub(),
|
||||
on: sinon.stub(),
|
||||
}
|
||||
|
||||
options.CriConstructor.returns(criClient)
|
||||
options.browserClient.send.withArgs('Fetch.enable').resolves()
|
||||
options.browserClient.send.withArgs('Runtime.runIfWaitingForDebugger').resolves()
|
||||
criClient.send.withArgs('Fetch.continueRequest').resolves()
|
||||
|
||||
await BrowserCriClient._onAttachToTarget(options as any)
|
||||
await criClient.on.lastCall.args[1]({ requestId: 'request-id' })
|
||||
|
||||
expect(criClient.send).to.be.calledWith('Fetch.continueRequest', {
|
||||
requestId: 'request-id',
|
||||
headers: [{ name: 'X-Cypress-Is-From-Extra-Target', value: 'true' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores any errors from continuing request', async () => {
|
||||
const criClient = {
|
||||
send: sinon.stub(),
|
||||
on: sinon.stub(),
|
||||
}
|
||||
|
||||
options.CriConstructor.returns(criClient)
|
||||
options.browserClient.send.withArgs('Fetch.enable').resolves()
|
||||
options.browserClient.send.withArgs('Runtime.runIfWaitingForDebugger').resolves()
|
||||
criClient.send.withArgs('Fetch.continueRequest').rejects(new Error('continuing request failed'))
|
||||
|
||||
await BrowserCriClient._onAttachToTarget(options as any)
|
||||
await criClient.on.lastCall.args[1]({ requestId: 'request-id', request: { url: '' } })
|
||||
// error is caught or else the test would fail
|
||||
})
|
||||
})
|
||||
|
||||
context('._onTargetDestroyed', () => {
|
||||
describe('when not the currently attached target', () => {
|
||||
let options: any
|
||||
|
||||
beforeEach(() => {
|
||||
options = {
|
||||
browserCriClient: {
|
||||
hasExtraTargetClient: sinon.stub().returns(true),
|
||||
getExtraTargetClient: sinon.stub(),
|
||||
removeExtraTargetClient: sinon.stub(),
|
||||
currentlyAttachedTarget: {
|
||||
targetId: 'main-target-id',
|
||||
close: sinon.stub().resolves(),
|
||||
},
|
||||
resettingBrowserTargets: false,
|
||||
},
|
||||
event: {
|
||||
targetId: 'target-id',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('is noop if target is not currently tracked', () => {
|
||||
options.browserCriClient.hasExtraTargetClient.returns(false)
|
||||
|
||||
BrowserCriClient._onTargetDestroyed(options as any)
|
||||
|
||||
expect(options.browserCriClient.getExtraTargetClient).not.to.be.called
|
||||
expect(options.browserCriClient.currentlyAttachedTarget.close).not.to.be.called
|
||||
})
|
||||
|
||||
it('closes the extra target client', () => {
|
||||
const client = { close: sinon.stub().resolves() }
|
||||
|
||||
options.browserCriClient.getExtraTargetClient.returns({ client })
|
||||
|
||||
BrowserCriClient._onTargetDestroyed(options as any)
|
||||
|
||||
expect(client.close).to.be.called
|
||||
})
|
||||
|
||||
it('ignores errors closing the extra target client', () => {
|
||||
const client = { close: sinon.stub().rejects(new Error('closing failed')) }
|
||||
|
||||
options.browserCriClient.getExtraTargetClient.returns({ client })
|
||||
|
||||
BrowserCriClient._onTargetDestroyed(options as any)
|
||||
|
||||
expect(options.browserCriClient.removeExtraTargetClient).to.be.calledWith('target-id')
|
||||
// error is caught or else the test would fail
|
||||
})
|
||||
|
||||
it('removes the extra target client from the tracker', () => {
|
||||
const client = { close: sinon.stub().resolves() }
|
||||
|
||||
options.browserCriClient.getExtraTargetClient.returns({ client })
|
||||
|
||||
BrowserCriClient._onTargetDestroyed(options as any)
|
||||
|
||||
expect(options.browserCriClient.removeExtraTargetClient).to.be.calledWith('target-id')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('#ensureMinimumProtocolVersion', function () {
|
||||
function withProtocolVersion (actual, test) {
|
||||
return getClient()
|
||||
|
||||
Reference in New Issue
Block a user