mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-06 14:39:48 -06:00
internal: (studio) add snapshot iframe and pass CDP client (#33109)
This commit is contained in:
@@ -140,7 +140,8 @@ const specs = window.__RUN_MODE_SPECS__
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
iframe.aut-iframe {
|
||||
iframe.aut-iframe,
|
||||
iframe.aut-snapshot-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: white;
|
||||
|
||||
@@ -2,145 +2,192 @@ import { AutIframe } from './aut-iframe'
|
||||
import { createEventManager } from '../../cypress/component/support/ctSupport'
|
||||
import { getElementDimensions } from './dimensions'
|
||||
|
||||
describe('AutIframe._addElementBoxModelLayers', () => {
|
||||
describe('AutIframe', () => {
|
||||
let autIframe: AutIframe
|
||||
let mockGetComputedStyle: typeof getComputedStyle
|
||||
let getComputedStyleCallCount: number
|
||||
let mockJQuery: any
|
||||
|
||||
beforeEach(() => {
|
||||
getComputedStyleCallCount = 0
|
||||
|
||||
mockGetComputedStyle = window.getComputedStyle
|
||||
window.getComputedStyle = (element: Element, pseudoElement?: string | null) => {
|
||||
getComputedStyleCallCount++
|
||||
|
||||
return mockGetComputedStyle.call(window, element, pseudoElement)
|
||||
}
|
||||
|
||||
mockJQuery = (selector: any) => {
|
||||
if (typeof selector === 'string') {
|
||||
return {
|
||||
get: (index?: number) => {
|
||||
if (selector === 'body') {
|
||||
return index === 0 ? document.body : [document.body]
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (selector && (selector.nodeType || selector instanceof HTMLElement || selector instanceof Element)) {
|
||||
return {
|
||||
get: (index?: number) => {
|
||||
return index === 0 ? selector : [selector]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
get: () => null,
|
||||
}
|
||||
}
|
||||
|
||||
const eventManager = createEventManager()
|
||||
|
||||
autIframe = new AutIframe('Test Project', eventManager, mockJQuery)
|
||||
autIframe = new AutIframe('Test Project', eventManager, Cypress.$)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
window.getComputedStyle = mockGetComputedStyle
|
||||
})
|
||||
context('._addElementBoxModelLayers', () => {
|
||||
let originalGetComputedStyle: typeof getComputedStyle
|
||||
let getComputedStyleCallCount: number
|
||||
|
||||
it('should not call getComputedStyle when dimensions are provided', () => {
|
||||
const testElement = document.createElement('div')
|
||||
beforeEach(() => {
|
||||
getComputedStyleCallCount = 0
|
||||
|
||||
testElement.style.width = '100px'
|
||||
testElement.style.height = '50px'
|
||||
testElement.style.padding = '10px'
|
||||
testElement.style.border = '5px solid black'
|
||||
testElement.style.margin = '15px'
|
||||
testElement.style.position = 'absolute'
|
||||
testElement.style.top = '20px'
|
||||
testElement.style.left = '30px'
|
||||
testElement.style.display = 'block'
|
||||
testElement.style.transform = 'translateX(10px)'
|
||||
testElement.style.zIndex = '100'
|
||||
document.body.appendChild(testElement)
|
||||
originalGetComputedStyle = window.getComputedStyle
|
||||
window.getComputedStyle = (element: Element, pseudoElement?: string | null) => {
|
||||
getComputedStyleCallCount++
|
||||
|
||||
// Get dimensions first (this will call getComputedStyle once)
|
||||
const dimensions = getElementDimensions(testElement)
|
||||
|
||||
// Verify dimensions include transform and zIndex
|
||||
expect(dimensions.transform).to.exist
|
||||
expect(dimensions.zIndex).to.exist
|
||||
|
||||
// Reset the counter since getElementDimensions also calls getComputedStyle
|
||||
getComputedStyleCallCount = 0
|
||||
|
||||
const $el = mockJQuery(testElement)
|
||||
const $body = mockJQuery('body')
|
||||
|
||||
// When dimensions are provided, _addElementBoxModelLayers should NOT call getComputedStyle
|
||||
const container = (autIframe as any)._addElementBoxModelLayers($el, $body, dimensions)
|
||||
|
||||
// Verify getComputedStyle was NOT called in _addElementBoxModelLayers
|
||||
// (it should use transform and zIndex from the provided dimensions)
|
||||
expect(getComputedStyleCallCount).to.equal(0, 'getComputedStyle should not be called when dimensions are provided')
|
||||
|
||||
expect(container).to.not.be.undefined
|
||||
expect(container).to.not.be.null
|
||||
expect(container).to.be.instanceof(HTMLElement)
|
||||
expect(container.classList.contains('__cypress-highlight')).to.be.true
|
||||
expect(container.children.length).to.be.greaterThan(0, 'Should create at least one layer')
|
||||
|
||||
const layers = Array.from(container.children) as HTMLElement[]
|
||||
|
||||
layers.forEach((layer) => {
|
||||
expect(layer.style.position).to.equal('absolute')
|
||||
// Verify positions are stored in data attributes (not style properties)
|
||||
expect(layer.getAttribute('data-top')).to.exist
|
||||
expect(layer.getAttribute('data-left')).to.exist
|
||||
expect(parseFloat(layer.getAttribute('data-top')!)).to.be.a('number')
|
||||
expect(parseFloat(layer.getAttribute('data-left')!)).to.be.a('number')
|
||||
expect(layer.getAttribute('data-layer')).to.exist
|
||||
// Verify transform and zIndex were applied from dimensions
|
||||
// Note: getComputedStyle returns computed transform as a matrix, not the original CSS value
|
||||
// So we check that transform is set (not 'none') and matches the computed value from dimensions
|
||||
expect(layer.style.transform).to.equal(dimensions.transform)
|
||||
expect(layer.style.zIndex).to.equal('100')
|
||||
return originalGetComputedStyle.call(window, element, pseudoElement)
|
||||
}
|
||||
})
|
||||
|
||||
document.body.removeChild(testElement)
|
||||
afterEach(() => {
|
||||
window.getComputedStyle = originalGetComputedStyle
|
||||
})
|
||||
|
||||
it('should not call getComputedStyle when dimensions are provided', () => {
|
||||
const testElement = document.createElement('div')
|
||||
|
||||
testElement.style.width = '100px'
|
||||
testElement.style.height = '50px'
|
||||
testElement.style.padding = '10px'
|
||||
testElement.style.border = '5px solid black'
|
||||
testElement.style.margin = '15px'
|
||||
testElement.style.position = 'absolute'
|
||||
testElement.style.top = '20px'
|
||||
testElement.style.left = '30px'
|
||||
testElement.style.display = 'block'
|
||||
testElement.style.transform = 'translateX(10px)'
|
||||
testElement.style.zIndex = '100'
|
||||
document.body.appendChild(testElement)
|
||||
|
||||
// Get dimensions first (this will call getComputedStyle once)
|
||||
const dimensions = getElementDimensions(testElement)
|
||||
|
||||
// Verify dimensions include transform and zIndex
|
||||
expect(dimensions.transform).to.exist
|
||||
expect(dimensions.zIndex).to.exist
|
||||
|
||||
// Reset the counter since getElementDimensions also calls getComputedStyle
|
||||
getComputedStyleCallCount = 0
|
||||
|
||||
const $el = Cypress.$(testElement)
|
||||
const $body = Cypress.$('body')
|
||||
|
||||
// When dimensions are provided, _addElementBoxModelLayers should NOT call getComputedStyle
|
||||
const container = (autIframe as any)._addElementBoxModelLayers($el, $body, dimensions)
|
||||
|
||||
// Verify getComputedStyle was NOT called in _addElementBoxModelLayers
|
||||
// (it should use transform and zIndex from the provided dimensions)
|
||||
expect(getComputedStyleCallCount).to.equal(0, 'getComputedStyle should not be called when dimensions are provided')
|
||||
|
||||
expect(container).to.not.be.undefined
|
||||
expect(container).to.not.be.null
|
||||
expect(container).to.be.instanceof(HTMLElement)
|
||||
expect(container.classList.contains('__cypress-highlight')).to.be.true
|
||||
expect(container.children.length).to.be.greaterThan(0, 'Should create at least one layer')
|
||||
|
||||
const layers = Array.from(container.children) as HTMLElement[]
|
||||
|
||||
layers.forEach((layer) => {
|
||||
expect(layer.style.position).to.equal('absolute')
|
||||
// Verify positions are stored in data attributes (not style properties)
|
||||
expect(layer.getAttribute('data-top')).to.exist
|
||||
expect(layer.getAttribute('data-left')).to.exist
|
||||
expect(parseFloat(layer.getAttribute('data-top')!)).to.be.a('number')
|
||||
expect(parseFloat(layer.getAttribute('data-left')!)).to.be.a('number')
|
||||
expect(layer.getAttribute('data-layer')).to.exist
|
||||
// Verify transform and zIndex were applied from dimensions
|
||||
// Note: getComputedStyle returns computed transform as a matrix, not the original CSS value
|
||||
// So we check that transform is set (not 'none') and matches the computed value from dimensions
|
||||
expect(layer.style.transform).to.equal(dimensions.transform)
|
||||
expect(layer.style.zIndex).to.equal('100')
|
||||
})
|
||||
|
||||
document.body.removeChild(testElement)
|
||||
})
|
||||
|
||||
it('should call getComputedStyle only once when dimensions are not provided', () => {
|
||||
const testElement = document.createElement('div')
|
||||
|
||||
testElement.style.width = '100px'
|
||||
testElement.style.height = '50px'
|
||||
testElement.style.display = 'block'
|
||||
document.body.appendChild(testElement)
|
||||
|
||||
getComputedStyleCallCount = 0
|
||||
|
||||
const $el = Cypress.$(testElement)
|
||||
const $body = Cypress.$('body')
|
||||
|
||||
// Call without providing dimensions (will call getElementDimensions internally)
|
||||
const container = (autIframe as any)._addElementBoxModelLayers($el, $body)
|
||||
|
||||
// getElementDimensions will call getComputedStyle once and return transform/zIndex,
|
||||
// so _addElementBoxModelLayers won't need to call it again
|
||||
// We expect only 1 call total (from getElementDimensions)
|
||||
expect(getComputedStyleCallCount).to.equal(1, 'Should call getComputedStyle only once in getElementDimensions')
|
||||
|
||||
expect(container).to.not.be.undefined
|
||||
expect(container).to.not.be.null
|
||||
expect(container).to.be.instanceof(HTMLElement)
|
||||
expect(container.children.length).to.be.greaterThan(0)
|
||||
|
||||
document.body.removeChild(testElement)
|
||||
})
|
||||
})
|
||||
|
||||
it('should call getComputedStyle only once when dimensions are not provided', () => {
|
||||
const testElement = document.createElement('div')
|
||||
context('.create', () => {
|
||||
it('should create both aut iframe and snapshot iframe', () => {
|
||||
const result = autIframe.create()
|
||||
|
||||
testElement.style.width = '100px'
|
||||
testElement.style.height = '50px'
|
||||
testElement.style.display = 'block'
|
||||
document.body.appendChild(testElement)
|
||||
expect(result).to.have.property('autIframe')
|
||||
expect(result).to.have.property('autSnapshotIframe')
|
||||
expect(autIframe.$iframe).to.equal(result.autIframe)
|
||||
expect(autIframe.$snapshotIframe).to.equal(result.autSnapshotIframe)
|
||||
})
|
||||
|
||||
getComputedStyleCallCount = 0
|
||||
it('should create aut iframe with correct attributes', () => {
|
||||
const result = autIframe.create()
|
||||
const autIframeElement = result.autIframe[0] as HTMLIFrameElement
|
||||
|
||||
const $el = mockJQuery(testElement)
|
||||
const $body = mockJQuery('body')
|
||||
expect(autIframeElement.id).to.equal('Your project: \'Test Project\'')
|
||||
expect(autIframeElement.title).to.equal('Your project: \'Test Project\'')
|
||||
expect(autIframeElement.className).to.equal('aut-iframe')
|
||||
})
|
||||
|
||||
// Call without providing dimensions (will call getElementDimensions internally)
|
||||
const container = (autIframe as any)._addElementBoxModelLayers($el, $body)
|
||||
it('should create snapshot iframe with correct attributes', () => {
|
||||
const result = autIframe.create()
|
||||
const snapshotIframeElement = result.autSnapshotIframe[0] as HTMLIFrameElement
|
||||
|
||||
// getElementDimensions will call getComputedStyle once and return transform/zIndex,
|
||||
// so _addElementBoxModelLayers won't need to call it again
|
||||
// We expect only 1 call total (from getElementDimensions)
|
||||
expect(getComputedStyleCallCount).to.equal(1, 'Should call getComputedStyle only once in getElementDimensions')
|
||||
expect(snapshotIframeElement.id).to.equal('AUT Snapshot: \'Test Project\'')
|
||||
expect(snapshotIframeElement.title).to.equal('AUT Snapshot: \'Test Project\'')
|
||||
expect(snapshotIframeElement.className).to.equal('aut-snapshot-iframe')
|
||||
})
|
||||
|
||||
expect(container).to.not.be.undefined
|
||||
expect(container).to.not.be.null
|
||||
expect(container).to.be.instanceof(HTMLElement)
|
||||
expect(container.children.length).to.be.greaterThan(0)
|
||||
it('verify the snapshot iframe is hidden', () => {
|
||||
const result = autIframe.create()
|
||||
|
||||
document.body.removeChild(testElement)
|
||||
result.autSnapshotIframe.appendTo(document.body)
|
||||
result.autIframe.appendTo(document.body)
|
||||
|
||||
expect(result.autSnapshotIframe.is(':hidden')).to.be.true
|
||||
expect(result.autIframe.is(':hidden')).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
context('.destroy', () => {
|
||||
it('should remove both aut iframe and snapshot iframe', () => {
|
||||
const result = autIframe.create()
|
||||
let autIframeRemoved = false
|
||||
let snapshotIframeRemoved = false
|
||||
|
||||
// Mock remove methods
|
||||
result.autIframe.remove = () => {
|
||||
autIframeRemoved = true
|
||||
|
||||
return result.autIframe
|
||||
}
|
||||
|
||||
result.autSnapshotIframe.remove = () => {
|
||||
snapshotIframeRemoved = true
|
||||
|
||||
return result.autSnapshotIframe
|
||||
}
|
||||
|
||||
autIframe.destroy()
|
||||
|
||||
expect(autIframeRemoved).to.be.true
|
||||
expect(snapshotIframeRemoved).to.be.true
|
||||
})
|
||||
|
||||
it('should throw error when destroy is called without create', () => {
|
||||
expect(() => autIframe.destroy()).to.throw('Cannot call #remove without first calling #create')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,6 +19,8 @@ const jQueryRe = /jquery/i
|
||||
export class AutIframe {
|
||||
debouncedToggleSelectorPlayground: DebouncedFunc<(isEnabled: any) => void>
|
||||
$iframe?: JQuery<HTMLIFrameElement>
|
||||
// the iframe used to display a snapshot of the AUT (currently used to display the studio snapshots)
|
||||
$snapshotIframe?: JQuery<HTMLIFrameElement>
|
||||
_highlightedEl?: Element
|
||||
private _currentHighlightingId: number = 0
|
||||
|
||||
@@ -30,7 +32,7 @@ export class AutIframe {
|
||||
this.debouncedToggleSelectorPlayground = _.debounce(this.toggleSelectorPlayground, 300)
|
||||
}
|
||||
|
||||
create (): JQuery<HTMLIFrameElement> {
|
||||
create (): { autIframe: JQuery<HTMLIFrameElement>, autSnapshotIframe: JQuery<HTMLIFrameElement> } {
|
||||
const $iframe = this.$('<iframe>', {
|
||||
id: `Your project: '${this.projectName}'`,
|
||||
title: `Your project: '${this.projectName}'`,
|
||||
@@ -39,15 +41,28 @@ export class AutIframe {
|
||||
|
||||
this.$iframe = $iframe
|
||||
|
||||
return $iframe
|
||||
const $snapshotIframe: JQuery<HTMLIFrameElement> = this.$('<iframe>', {
|
||||
id: `AUT Snapshot: '${this.projectName}'`,
|
||||
title: `AUT Snapshot: '${this.projectName}'`,
|
||||
class: 'aut-snapshot-iframe',
|
||||
})
|
||||
|
||||
$snapshotIframe.hide() // Auto-hide the snapshot iframe
|
||||
this.$snapshotIframe = $snapshotIframe
|
||||
|
||||
return {
|
||||
autIframe: $iframe,
|
||||
autSnapshotIframe: $snapshotIframe,
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
if (!this.$iframe) {
|
||||
if (!this.$iframe || !this.$snapshotIframe) {
|
||||
throw Error(`Cannot call #remove without first calling #create`)
|
||||
}
|
||||
|
||||
this.$iframe.remove()
|
||||
this.$snapshotIframe.remove()
|
||||
}
|
||||
|
||||
_showInitialBlankPage () {
|
||||
|
||||
@@ -477,7 +477,15 @@ export class EventManager {
|
||||
return getRunnerConfigFromWindow()?.browser?.family === family
|
||||
}
|
||||
|
||||
initialize ($autIframe: JQuery<HTMLIFrameElement>, config: Record<string, any>) {
|
||||
initialize ({
|
||||
$autIframe,
|
||||
$autSnapshotIframe,
|
||||
config,
|
||||
}: {
|
||||
$autIframe: JQuery<HTMLIFrameElement>
|
||||
$autSnapshotIframe?: JQuery<HTMLIFrameElement>
|
||||
config: Record<string, any>
|
||||
}) {
|
||||
performance.mark('initialize-start')
|
||||
|
||||
const testFilter = this.specStore.testFilter
|
||||
@@ -507,6 +515,7 @@ export class EventManager {
|
||||
|
||||
return Cypress.initialize({
|
||||
$autIframe,
|
||||
$autSnapshotIframe,
|
||||
// defining this indicates that the test run should wait for Studio to
|
||||
// be initialized before running the test
|
||||
waitForStudio: isStudio ? waitForStudio : undefined,
|
||||
|
||||
@@ -240,7 +240,10 @@ function runSpecCT (config, spec: SpecFile) {
|
||||
|
||||
// create new AUT
|
||||
const autIframe = getAutIframeModel()
|
||||
const $autIframe: JQuery<HTMLIFrameElement> = autIframe.create().appendTo($container)
|
||||
|
||||
const { autIframe: $autIframe } = autIframe.create()
|
||||
|
||||
$autIframe.appendTo($container)
|
||||
|
||||
// the iframe controller will forward the specpath via header to the devserver.
|
||||
// using a query parameter allows us to recognize relative requests and proxy them to the devserver.
|
||||
@@ -255,7 +258,7 @@ function runSpecCT (config, spec: SpecFile) {
|
||||
$autIframe.prop('src', specSrc)
|
||||
|
||||
// initialize Cypress (driver) with the AUT!
|
||||
getEventManager().initialize($autIframe, config)
|
||||
getEventManager().initialize({ $autIframe, config })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,7 +305,10 @@ async function runSpecE2E (config, spec: SpecFile) {
|
||||
// create new AUT
|
||||
const autIframe = getAutIframeModel()
|
||||
|
||||
const $autIframe: JQuery<HTMLIFrameElement> = autIframe.create().appendTo($container)
|
||||
const { autIframe: $autIframe, autSnapshotIframe: $autSnapshotIframe } = autIframe.create()
|
||||
|
||||
$autIframe.appendTo($container)
|
||||
$autSnapshotIframe.appendTo($container)
|
||||
|
||||
// Remove the spec bridge iframe
|
||||
document.querySelectorAll('iframe.spec-bridge-iframe').forEach((el) => {
|
||||
@@ -327,7 +333,7 @@ async function runSpecE2E (config, spec: SpecFile) {
|
||||
})
|
||||
|
||||
// initialize Cypress (driver) with the AUT!
|
||||
getEventManager().initialize($autIframe, config)
|
||||
getEventManager().initialize({ $autIframe, $autSnapshotIframe, config })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,14 +14,22 @@
|
||||
// should be the first same-origin one we come across
|
||||
const specFrame = window.__isSpecFrame ? window : (() => {
|
||||
const tryFrame = (index) => {
|
||||
if (index >= window[TOP].frames.length) {
|
||||
throw new Error('Spec frame not found')
|
||||
}
|
||||
|
||||
try {
|
||||
// will throw if cross-origin
|
||||
window[TOP].frames[index].location.href
|
||||
const location = window[TOP].frames[index].location
|
||||
|
||||
return window[TOP].frames[index]
|
||||
if (location.pathname.startsWith('/__cypress')) {
|
||||
return window[TOP].frames[index]
|
||||
}
|
||||
} catch (err) {
|
||||
return tryFrame(index + 1)
|
||||
// skip if the frame is cross-origin
|
||||
}
|
||||
|
||||
return tryFrame(index + 1)
|
||||
}
|
||||
|
||||
return tryFrame(1)
|
||||
|
||||
@@ -122,6 +122,7 @@ class $Cypress {
|
||||
downloads: any
|
||||
Commands: any
|
||||
$autIframe: any
|
||||
$autSnapshotIframe?: JQuery<HTMLIFrameElement> | null
|
||||
onSpecReady: any
|
||||
waitForStudio: any
|
||||
events: any
|
||||
@@ -215,6 +216,7 @@ class $Cypress {
|
||||
this.downloads = null
|
||||
this.Commands = null
|
||||
this.$autIframe = null
|
||||
this.$autSnapshotIframe = null
|
||||
this.onSpecReady = null
|
||||
this.waitForStudio = null
|
||||
this.primaryOriginCommunicator = new PrimaryOriginCommunicator()
|
||||
@@ -342,8 +344,9 @@ class $Cypress {
|
||||
return this.action('cypress:config', config)
|
||||
}
|
||||
|
||||
initialize ({ $autIframe, onSpecReady, waitForStudio }) {
|
||||
initialize ({ $autIframe, $autSnapshotIframe, onSpecReady, waitForStudio }) {
|
||||
this.$autIframe = $autIframe
|
||||
this.$autSnapshotIframe = $autSnapshotIframe
|
||||
this.onSpecReady = onSpecReady
|
||||
this.waitForStudio = waitForStudio
|
||||
if (this._onInitialize) {
|
||||
|
||||
84
packages/driver/test/unit/cypress/cypress.spec.ts
Normal file
84
packages/driver/test/unit/cypress/cypress.spec.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest'
|
||||
import $Cypress from '../../../src/cypress'
|
||||
|
||||
describe('$Cypress', () => {
|
||||
let Cypress: any
|
||||
|
||||
beforeEach(() => {
|
||||
Cypress = new $Cypress()
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
describe('initialize', () => {
|
||||
it('should store autIframe and snapshotIframe', () => {
|
||||
const mockAutIframe = { id: 'aut-iframe' } as any
|
||||
const mockSnapshotIframe = { id: 'snapshot-iframe' } as any
|
||||
const mockOnSpecReady = vi.fn()
|
||||
const mockWaitForStudio = vi.fn()
|
||||
|
||||
Cypress.initialize({
|
||||
$autIframe: mockAutIframe,
|
||||
$autSnapshotIframe: mockSnapshotIframe,
|
||||
onSpecReady: mockOnSpecReady,
|
||||
waitForStudio: mockWaitForStudio,
|
||||
})
|
||||
|
||||
expect(Cypress.$autIframe).toBe(mockAutIframe)
|
||||
expect(Cypress.$autSnapshotIframe).toBe(mockSnapshotIframe)
|
||||
expect(Cypress.onSpecReady).toBe(mockOnSpecReady)
|
||||
expect(Cypress.waitForStudio).toBe(mockWaitForStudio)
|
||||
})
|
||||
|
||||
it('should handle snapshotIframe being undefined', () => {
|
||||
const mockAutIframe = { id: 'aut-iframe' } as any
|
||||
const mockOnSpecReady = vi.fn()
|
||||
|
||||
Cypress.initialize({
|
||||
$autIframe: mockAutIframe,
|
||||
$autSnapshotIframe: undefined,
|
||||
onSpecReady: mockOnSpecReady,
|
||||
waitForStudio: undefined,
|
||||
})
|
||||
|
||||
expect(Cypress.$autIframe).toBe(mockAutIframe)
|
||||
expect(Cypress.$autSnapshotIframe).toBeUndefined()
|
||||
expect(Cypress.onSpecReady).toBe(mockOnSpecReady)
|
||||
expect(Cypress.waitForStudio).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should call _onInitialize callback if set', () => {
|
||||
const mockOnInitialize = vi.fn()
|
||||
|
||||
Cypress._onInitialize = mockOnInitialize
|
||||
|
||||
const mockAutIframe = { id: 'aut-iframe' } as any
|
||||
|
||||
Cypress.initialize({
|
||||
$autIframe: mockAutIframe,
|
||||
$autSnapshotIframe: undefined,
|
||||
onSpecReady: vi.fn(),
|
||||
waitForStudio: undefined,
|
||||
})
|
||||
|
||||
expect(mockOnInitialize).toHaveBeenCalledOnce()
|
||||
expect(Cypress._onInitialize).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not call _onInitialize callback if not set', () => {
|
||||
const mockAutIframe = { id: 'aut-iframe' } as any
|
||||
|
||||
Cypress.initialize({
|
||||
$autIframe: mockAutIframe,
|
||||
$autSnapshotIframe: undefined,
|
||||
onSpecReady: vi.fn(),
|
||||
waitForStudio: undefined,
|
||||
})
|
||||
|
||||
// Should not throw and should complete successfully
|
||||
expect(Cypress.$autIframe).toBe(mockAutIframe)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -58,7 +58,7 @@ $spec-list-width: 250px;
|
||||
box-shadow: none;
|
||||
left: 0;
|
||||
|
||||
.aut-iframe {
|
||||
.aut-iframe, .aut-snapshot-iframe {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.aut-iframe {
|
||||
.aut-iframe,
|
||||
.aut-snapshot-iframe {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 6px 10px #555;
|
||||
border: none;
|
||||
@@ -52,20 +53,24 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.studio-is-open .aut-iframe {
|
||||
.studio-is-open .aut-iframe,
|
||||
.studio-is-open .aut-snapshot-iframe {
|
||||
border: 4px solid #4997e4;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.studio-is-loading .aut-iframe {
|
||||
.studio-is-loading .aut-iframe,
|
||||
.studio-is-loading .aut-snapshot-iframe {
|
||||
border: 4px solid #b4d5f8;
|
||||
}
|
||||
|
||||
.studio-is-failed .aut-iframe {
|
||||
.studio-is-failed .aut-iframe,
|
||||
.studio-is-failed .aut-snapshot-iframe {
|
||||
border: 4px solid $error;
|
||||
}
|
||||
|
||||
.studio-is-ready .aut-iframe {
|
||||
.studio-is-ready .aut-iframe,
|
||||
.studio-is-ready .aut-snapshot-iframe {
|
||||
animation: flash-iframe-border 1.5s linear infinite;
|
||||
|
||||
@keyframes flash-iframe-border {
|
||||
|
||||
@@ -181,6 +181,7 @@ export class BrowserCriClient {
|
||||
currentlyAttachedTarget: CriClient | undefined
|
||||
currentlyAttachedProtocolTarget: CriClient | undefined
|
||||
currentlyAttachedCyPromptTarget: CriClient | undefined
|
||||
currentlyAttachedStudioTarget: CriClient | undefined
|
||||
// whenever we instantiate the instance we're already connected bc
|
||||
// we receive an underlying CRI connection
|
||||
// TODO: remove "connected" in favor of closing/closed or disconnected
|
||||
@@ -458,10 +459,11 @@ export class BrowserCriClient {
|
||||
//
|
||||
// otherwise it means the the browser itself was closed
|
||||
|
||||
// always close the connection to the page target because it was destroyed
|
||||
// always close the connection to the page targets because it was destroyed
|
||||
browserCriClient.currentlyAttachedTarget.close().catch(() => { })
|
||||
browserCriClient.currentlyAttachedProtocolTarget?.close().catch(() => { })
|
||||
browserCriClient.currentlyAttachedCyPromptTarget?.close().catch(() => { })
|
||||
browserCriClient.currentlyAttachedStudioTarget?.close().catch(() => { })
|
||||
|
||||
new Bluebird((resolve) => {
|
||||
// this event could fire either expectedly or unexpectedly
|
||||
@@ -588,6 +590,7 @@ export class BrowserCriClient {
|
||||
this.currentlyAttachedTarget.close().catch(() => {}),
|
||||
this.currentlyAttachedProtocolTarget?.close().catch(() => {}),
|
||||
this.currentlyAttachedCyPromptTarget?.close().catch(() => {}),
|
||||
this.currentlyAttachedStudioTarget?.close().catch(() => {}),
|
||||
])
|
||||
|
||||
debug('target client closed', this.currentlyAttachedTarget.targetId)
|
||||
@@ -605,6 +608,10 @@ export class BrowserCriClient {
|
||||
this.browserClient.off(subscription.eventName, subscription.cb as any)
|
||||
})
|
||||
|
||||
this.currentlyAttachedStudioTarget?.queue.subscriptions.forEach((subscription) => {
|
||||
this.browserClient.off(subscription.eventName, subscription.cb as any)
|
||||
})
|
||||
|
||||
if (target) {
|
||||
this.currentlyAttachedTarget = await CriClient.create({
|
||||
target: target.targetId,
|
||||
@@ -626,14 +633,20 @@ export class BrowserCriClient {
|
||||
this.currentlyAttachedCyPromptTarget = await currentTarget.clone()
|
||||
}
|
||||
|
||||
const createStudioTarget = async () => {
|
||||
this.currentlyAttachedStudioTarget = await currentTarget.clone()
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
createProtocolTarget(),
|
||||
createCyPromptTarget(),
|
||||
createStudioTarget(),
|
||||
])
|
||||
} else {
|
||||
this.currentlyAttachedTarget = undefined
|
||||
this.currentlyAttachedProtocolTarget = undefined
|
||||
this.currentlyAttachedCyPromptTarget = undefined
|
||||
this.currentlyAttachedStudioTarget = undefined
|
||||
}
|
||||
|
||||
this.resettingBrowserTargets = false
|
||||
@@ -696,6 +709,7 @@ export class BrowserCriClient {
|
||||
this.currentlyAttachedTarget.close(),
|
||||
this.currentlyAttachedProtocolTarget?.close(),
|
||||
this.currentlyAttachedCyPromptTarget?.close(),
|
||||
this.currentlyAttachedStudioTarget?.close(),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { CriClient } from './cri-client'
|
||||
import type { Automation } from '../automation'
|
||||
import memory from './memory'
|
||||
|
||||
import type { BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManagerShape, CyPromptManagerShape, RunModeVideoApi } from '@packages/types'
|
||||
import type { BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManagerShape, CyPromptManagerShape, StudioManagerShape, RunModeVideoApi } from '@packages/types'
|
||||
import type { CDPSocketServer } from '@packages/socket'
|
||||
import { DEFAULT_CHROME_FLAGS } from '../util/chromium_flags'
|
||||
|
||||
@@ -464,6 +464,18 @@ export = {
|
||||
await options.cyPromptManager?.connectToBrowser(browserCriClient.currentlyAttachedCyPromptTarget)
|
||||
},
|
||||
|
||||
async connectStudioToBrowser (options: { studioManager?: StudioManagerShape }) {
|
||||
const browserCriClient = this._getBrowserCriClient()
|
||||
|
||||
if (!browserCriClient?.currentlyAttachedTarget) throw new Error('Missing pageCriClient in connectStudioToBrowser')
|
||||
|
||||
if (!browserCriClient.currentlyAttachedStudioTarget) {
|
||||
browserCriClient.currentlyAttachedStudioTarget = await browserCriClient.currentlyAttachedTarget.clone()
|
||||
}
|
||||
|
||||
await options.studioManager?.connectToBrowser(browserCriClient.currentlyAttachedStudioTarget)
|
||||
},
|
||||
|
||||
async closeProtocolConnection () {
|
||||
const browserCriClient = this._getBrowserCriClient()
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { Browser, BrowserInstance, GracefulShutdownOptions } from './types'
|
||||
// tslint:disable-next-line no-implicit-dependencies - electron dep needs to be defined
|
||||
import type { BrowserWindow } from 'electron'
|
||||
import type { Automation } from '../automation'
|
||||
import type { BrowserLaunchOpts, Preferences, ProtocolManagerShape, CyPromptManagerShape, RunModeVideoApi } from '@packages/types'
|
||||
import type { BrowserLaunchOpts, Preferences, ProtocolManagerShape, CyPromptManagerShape, StudioManagerShape, RunModeVideoApi } from '@packages/types'
|
||||
import type { CDPSocketServer } from '@packages/socket'
|
||||
import memory from './memory'
|
||||
import { BrowserCriClient } from './browser-cri-client'
|
||||
@@ -515,6 +515,19 @@ export = {
|
||||
await options.cyPromptManager?.connectToBrowser(browserCriClient.currentlyAttachedCyPromptTarget)
|
||||
},
|
||||
|
||||
async connectStudioToBrowser (options: { studioManager?: StudioManagerShape }) {
|
||||
const browserCriClient = this._getBrowserCriClient()
|
||||
|
||||
if (!browserCriClient?.currentlyAttachedTarget) throw new Error('Missing pageCriClient in connectStudioToBrowser')
|
||||
|
||||
// Clone the target here so that we separate the studio client and the main client.
|
||||
if (!browserCriClient.currentlyAttachedStudioTarget) {
|
||||
browserCriClient.currentlyAttachedStudioTarget = await browserCriClient.currentlyAttachedTarget.clone()
|
||||
}
|
||||
|
||||
await options.studioManager?.connectToBrowser(browserCriClient.currentlyAttachedStudioTarget)
|
||||
},
|
||||
|
||||
async closeProtocolConnection () {
|
||||
const browserCriClient = this._getBrowserCriClient()
|
||||
|
||||
|
||||
@@ -404,6 +404,10 @@ export function connectCyPromptToBrowser (): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
export function connectStudioToBrowser (): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
export function closeProtocolConnection (): Promise<void> {
|
||||
throw new Error('Protocol is not yet supported in firefox.')
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as errors from '../errors'
|
||||
import { exec } from 'child_process'
|
||||
import util from 'util'
|
||||
import os from 'os'
|
||||
import { BROWSER_FAMILY, BrowserLaunchOpts, BrowserNewTabOpts, FoundBrowser, ProtocolManagerShape, CyPromptManagerShape } from '@packages/types'
|
||||
import { BROWSER_FAMILY, BrowserLaunchOpts, BrowserNewTabOpts, FoundBrowser, ProtocolManagerShape, CyPromptManagerShape, StudioManagerShape } from '@packages/types'
|
||||
import type { Browser, BrowserInstance, BrowserLauncher } from './types'
|
||||
import type { Automation } from '../automation'
|
||||
import type { DataContext } from '@packages/data-context'
|
||||
@@ -152,6 +152,12 @@ export = {
|
||||
await browserLauncher.connectCyPromptToBrowser(options)
|
||||
},
|
||||
|
||||
async connectStudioToBrowser (options: { browser: Browser, foundBrowsers?: FoundBrowser[], studioManager?: StudioManagerShape }) {
|
||||
const browserLauncher = await getBrowserLauncher(options.browser, options.foundBrowsers || [])
|
||||
|
||||
await browserLauncher.connectStudioToBrowser(options)
|
||||
},
|
||||
|
||||
async closeProtocolConnection (options: { browser: Browser, foundBrowsers?: FoundBrowser[] }) {
|
||||
const browserLauncher = await getBrowserLauncher(options.browser, options.foundBrowsers || [])
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FoundBrowser, BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManagerShape, CyPromptManagerShape } from '@packages/types'
|
||||
import type { FoundBrowser, BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManagerShape, CyPromptManagerShape, StudioManagerShape } from '@packages/types'
|
||||
import type { EventEmitter } from 'events'
|
||||
import type { Automation } from '../automation'
|
||||
import type { CDPSocketServer } from '@packages/socket'
|
||||
@@ -49,6 +49,10 @@ export type BrowserLauncher = {
|
||||
* Used to connect the cy prompt to an existing browser.
|
||||
*/
|
||||
connectCyPromptToBrowser: (options: { cyPromptManager?: CyPromptManagerShape }) => Promise<void>
|
||||
/**
|
||||
* Used to connect studio to an existing browser.
|
||||
*/
|
||||
connectStudioToBrowser: (options: { studioManager?: StudioManagerShape }) => Promise<void>
|
||||
/**
|
||||
* Closes the protocol connection to the browser.
|
||||
*/
|
||||
|
||||
@@ -44,6 +44,10 @@ export function connectCyPromptToBrowser (): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
export function connectStudioToBrowser (): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
export function closeProtocolConnection (): Promise<void> {
|
||||
throw new Error('Protocol is not yet supported in WebKit.')
|
||||
}
|
||||
|
||||
@@ -329,13 +329,16 @@ export class StudioLifecycleManager {
|
||||
|
||||
const studioManager = this.studioManager
|
||||
|
||||
debug('Calling all studio ready listeners')
|
||||
debug('Calling %d studio ready listeners', this.listeners.length)
|
||||
this.listeners.forEach((listener) => {
|
||||
listener(studioManager)
|
||||
})
|
||||
|
||||
debug('Clearing %d studio ready listeners after successful initialization', this.listeners.length)
|
||||
this.listeners = []
|
||||
// In local development, keep listeners so they can be called again after Studio reloads
|
||||
if (!process.env.CYPRESS_LOCAL_STUDIO_PATH) {
|
||||
debug('Clearing %d studio ready listeners after successful initialization', this.listeners.length)
|
||||
this.listeners = []
|
||||
}
|
||||
}
|
||||
|
||||
private setupWatcher ({
|
||||
@@ -393,7 +396,12 @@ export class StudioLifecycleManager {
|
||||
if (this.studioManager) {
|
||||
debug('Studio ready - calling listener immediately')
|
||||
listener(this.studioManager)
|
||||
this.listeners.push(listener)
|
||||
|
||||
// If the studio bundle is local, we need to register the listener
|
||||
// so that we can reload the studio when the bundle changes
|
||||
if (process.env.CYPRESS_LOCAL_STUDIO_PATH) {
|
||||
this.listeners.push(listener)
|
||||
}
|
||||
} else {
|
||||
debug('Studio not ready - registering studio ready listener')
|
||||
this.listeners.push(listener)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, ProtocolManagerShape, StudioCloudApi, StudioAIInitializeOptions, StudioEvent, StudioAddSocketListenersOptions, StudioServerOptions } from '@packages/types'
|
||||
import type { StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, ProtocolManagerShape, StudioCloudApi, StudioAIInitializeOptions, StudioEvent, StudioAddSocketListenersOptions, StudioServerOptions, StudioCDPClient } from '@packages/types'
|
||||
import type { Router } from 'express'
|
||||
import Debug from 'debug'
|
||||
import { requireScript } from '../require_script'
|
||||
import path from 'path'
|
||||
import { reportStudioError, ReportStudioErrorOptions } from '../api/studio/report_studio_error'
|
||||
import crypto, { BinaryLike } from 'crypto'
|
||||
import { StudioElectron } from './StudioElectron'
|
||||
import exception from '../exception'
|
||||
@@ -76,6 +75,12 @@ export class StudioManager implements StudioManagerShape {
|
||||
return !!(await this.invokeAsync('canAccessStudioAI', { isEssential: true }, browser))
|
||||
}
|
||||
|
||||
connectToBrowser (target: StudioCDPClient): void {
|
||||
if (this._studioServer) {
|
||||
return this.invokeSync('connectToBrowser', { isEssential: true }, target)
|
||||
}
|
||||
}
|
||||
|
||||
async initializeStudioAI (options: StudioAIInitializeOptions): Promise<void> {
|
||||
// Only create a studio electron instance when studio AI is enabled
|
||||
if (!this._studioElectron) {
|
||||
@@ -119,6 +124,8 @@ export class StudioManager implements StudioManagerShape {
|
||||
}
|
||||
|
||||
try {
|
||||
debug('invoking sync method %s with args %o', method, args)
|
||||
|
||||
// @ts-expect-error - TS not associating the method & args properly, even though we know it's correct
|
||||
return this._studioServer[method].apply(this._studioServer, args)
|
||||
} catch (error: unknown) {
|
||||
@@ -152,6 +159,8 @@ export class StudioManager implements StudioManagerShape {
|
||||
}
|
||||
|
||||
try {
|
||||
debug('invoking async method %s with args %o', method, args)
|
||||
|
||||
// @ts-expect-error - TS not associating the method & args properly, even though we know it's correct
|
||||
return await this._studioServer[method].apply(this._studioServer, args)
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -5,6 +5,8 @@ export const INITIALIZATION_MARK_NAMES = Object.freeze({
|
||||
CAN_ACCESS_STUDIO_AI_END: 'can-access-studio-ai-end',
|
||||
CONNECT_PROTOCOL_TO_BROWSER_START: 'connect-protocol-to-browser-start',
|
||||
CONNECT_PROTOCOL_TO_BROWSER_END: 'connect-protocol-to-browser-end',
|
||||
CONNECT_STUDIO_TO_BROWSER_START: 'connect-studio-to-browser-start',
|
||||
CONNECT_STUDIO_TO_BROWSER_END: 'connect-studio-to-browser-end',
|
||||
INITIALIZE_STUDIO_AI_START: 'initialize-studio-ai-start',
|
||||
INITIALIZE_STUDIO_AI_END: 'initialize-studio-ai-end',
|
||||
} as const)
|
||||
@@ -15,6 +17,7 @@ export const INITIALIZATION_MEASURE_NAMES = Object.freeze({
|
||||
INITIALIZATION_DURATION: 'initialization-duration',
|
||||
CAN_ACCESS_STUDIO_AI_DURATION: 'can-access-studio-ai-duration',
|
||||
CONNECT_PROTOCOL_TO_BROWSER_DURATION: 'connect-protocol-to-browser-duration',
|
||||
CONNECT_STUDIO_TO_BROWSER_DURATION: 'connect-studio-to-browser-duration',
|
||||
INITIALIZE_STUDIO_AI_DURATION: 'initialize-studio-ai-duration',
|
||||
} as const)
|
||||
|
||||
@@ -33,6 +36,10 @@ export const INITIALIZATION_MEASURES: Record<InitializationMeasureName, [Initial
|
||||
INITIALIZATION_MARK_NAMES.CONNECT_PROTOCOL_TO_BROWSER_START,
|
||||
INITIALIZATION_MARK_NAMES.CONNECT_PROTOCOL_TO_BROWSER_END,
|
||||
],
|
||||
[INITIALIZATION_MEASURE_NAMES.CONNECT_STUDIO_TO_BROWSER_DURATION]: [
|
||||
INITIALIZATION_MARK_NAMES.CONNECT_STUDIO_TO_BROWSER_START,
|
||||
INITIALIZATION_MARK_NAMES.CONNECT_STUDIO_TO_BROWSER_END,
|
||||
],
|
||||
[INITIALIZATION_MEASURE_NAMES.INITIALIZE_STUDIO_AI_DURATION]: [
|
||||
INITIALIZATION_MARK_NAMES.INITIALIZE_STUDIO_AI_START,
|
||||
INITIALIZATION_MARK_NAMES.INITIALIZE_STUDIO_AI_END,
|
||||
@@ -50,6 +57,7 @@ export const INITIALIZATION_TELEMETRY_GROUPS: Record<InitializationTelemetryGrou
|
||||
INITIALIZATION_MEASURE_NAMES.INITIALIZATION_DURATION,
|
||||
INITIALIZATION_MEASURE_NAMES.CAN_ACCESS_STUDIO_AI_DURATION,
|
||||
INITIALIZATION_MEASURE_NAMES.CONNECT_PROTOCOL_TO_BROWSER_DURATION,
|
||||
INITIALIZATION_MEASURE_NAMES.CONNECT_STUDIO_TO_BROWSER_DURATION,
|
||||
INITIALIZATION_MEASURE_NAMES.INITIALIZE_STUDIO_AI_DURATION,
|
||||
],
|
||||
} as const)
|
||||
|
||||
@@ -523,6 +523,10 @@ export class ProjectBase extends EE {
|
||||
await browsers.connectProtocolToBrowser({ browser: this.browser, foundBrowsers: this.options.browsers, protocolManager: studio.protocolManager })
|
||||
telemetryManager.mark(INITIALIZATION_MARK_NAMES.CONNECT_PROTOCOL_TO_BROWSER_END)
|
||||
|
||||
telemetryManager.mark(INITIALIZATION_MARK_NAMES.CONNECT_STUDIO_TO_BROWSER_START)
|
||||
await browsers.connectStudioToBrowser({ browser: this.browser, foundBrowsers: this.options.browsers, studioManager: studio })
|
||||
telemetryManager.mark(INITIALIZATION_MARK_NAMES.CONNECT_STUDIO_TO_BROWSER_END)
|
||||
|
||||
if (!studio.protocolManager.dbPath) {
|
||||
debug('Protocol database path is not set after initializing protocol manager')
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import type { StudioServerShape, StudioServerDefaultShape, StudioEvent } from '@packages/types'
|
||||
import type { StudioServerShape, StudioServerDefaultShape, StudioEvent, StudioCDPClient } from '@packages/types'
|
||||
import type { Router } from 'express'
|
||||
import type { Socket } from '@packages/socket'
|
||||
|
||||
@@ -36,6 +36,10 @@ class StudioServer implements StudioServerShape {
|
||||
updateSessionId (sessionId: string): void {
|
||||
// This is a test implementation that does nothing
|
||||
}
|
||||
|
||||
connectToBrowser (cdpClient: StudioCDPClient): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
const studioServerDefault: StudioServerDefaultShape = {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { expect, proxyquire, sinon } from '../../spec_helper'
|
||||
import * as protocol from '../../../lib/browsers/protocol'
|
||||
import { stripAnsi } from '@packages/errors'
|
||||
import net from 'net'
|
||||
import { ProtocolManagerShape, CyPromptManagerShape } from '@packages/types'
|
||||
import { ProtocolManagerShape, CyPromptManagerShape, StudioManagerShape } from '@packages/types'
|
||||
import type { Protocol } from 'devtools-protocol'
|
||||
import { serviceWorkerClientEventHandlerName } from '@packages/proxy/lib/http/util/service-worker-manager'
|
||||
|
||||
@@ -14,7 +14,6 @@ const THROWS_PORT = 65535
|
||||
|
||||
type GetClientParams = {
|
||||
protocolManager?: ProtocolManagerShape
|
||||
cyPromptManager?: CyPromptManagerShape
|
||||
fullyManageTabs?: boolean
|
||||
}
|
||||
|
||||
@@ -374,6 +373,9 @@ describe('lib/browsers/browser-cri-client', function () {
|
||||
currentlyAttachedCyPromptTarget: {
|
||||
close: sinon.stub().resolves(),
|
||||
},
|
||||
currentlyAttachedStudioTarget: {
|
||||
close: sinon.stub().resolves(),
|
||||
},
|
||||
resettingBrowserTargets: false,
|
||||
},
|
||||
event: {
|
||||
@@ -391,6 +393,7 @@ describe('lib/browsers/browser-cri-client', function () {
|
||||
expect(options.browserCriClient.currentlyAttachedTarget.close).not.to.be.called
|
||||
expect(options.browserCriClient.currentlyAttachedProtocolTarget.close).not.to.be.called
|
||||
expect(options.browserCriClient.currentlyAttachedCyPromptTarget.close).not.to.be.called
|
||||
expect(options.browserCriClient.currentlyAttachedStudioTarget.close).not.to.be.called
|
||||
})
|
||||
|
||||
it('closes the extra target client', () => {
|
||||
@@ -423,6 +426,46 @@ describe('lib/browsers/browser-cri-client', function () {
|
||||
|
||||
expect(options.browserCriClient.removeExtraTargetClient).to.be.calledWith('target-id')
|
||||
})
|
||||
|
||||
it('closes the studio target', () => {
|
||||
options.browserCriClient.gracefulShutdown = true
|
||||
options.event.targetId = 'main-target-id'
|
||||
options.browserCriClient.currentlyAttachedStudioTarget.close.resolves()
|
||||
|
||||
BrowserCriClient._onTargetDestroyed(options as any)
|
||||
|
||||
expect(options.browserCriClient.currentlyAttachedStudioTarget.close).to.be.called
|
||||
})
|
||||
|
||||
it('ignores errors closing the studio target', () => {
|
||||
options.browserCriClient.gracefulShutdown = true
|
||||
options.event.targetId = 'main-target-id'
|
||||
options.browserCriClient.currentlyAttachedStudioTarget.close.rejects(new Error('closing failed'))
|
||||
|
||||
BrowserCriClient._onTargetDestroyed(options as any)
|
||||
|
||||
expect(options.browserCriClient.currentlyAttachedStudioTarget.close).to.be.called
|
||||
})
|
||||
|
||||
it('closes the cyPrompt target', () => {
|
||||
options.browserCriClient.gracefulShutdown = true
|
||||
options.event.targetId = 'main-target-id'
|
||||
options.browserCriClient.currentlyAttachedCyPromptTarget.close.resolves()
|
||||
|
||||
BrowserCriClient._onTargetDestroyed(options as any)
|
||||
|
||||
expect(options.browserCriClient.currentlyAttachedCyPromptTarget.close).to.be.called
|
||||
})
|
||||
|
||||
it('ignores errors closing the cyPrompt target', () => {
|
||||
options.browserCriClient.gracefulShutdown = true
|
||||
options.event.targetId = 'main-target-id'
|
||||
options.browserCriClient.currentlyAttachedCyPromptTarget.close.rejects(new Error('closing failed'))
|
||||
|
||||
BrowserCriClient._onTargetDestroyed(options as any)
|
||||
|
||||
expect(options.browserCriClient.currentlyAttachedCyPromptTarget.close).to.be.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -572,6 +615,17 @@ describe('lib/browsers/browser-cri-client', function () {
|
||||
},
|
||||
}
|
||||
|
||||
const mockCurrentlyAttachedStudioTarget = {
|
||||
targetId: '100',
|
||||
close: sinon.stub().resolves(sinon.stub().resolves()),
|
||||
queue: {
|
||||
subscriptions: [{
|
||||
eventName: 'Network.requestWillBeSent',
|
||||
cb: sinon.stub(),
|
||||
}],
|
||||
},
|
||||
}
|
||||
|
||||
const mockUpdatedCurrentlyAttachedProtocolTarget = {
|
||||
targetId: '101',
|
||||
}
|
||||
@@ -580,9 +634,16 @@ describe('lib/browsers/browser-cri-client', function () {
|
||||
targetId: '101',
|
||||
}
|
||||
|
||||
const mockUpdatedCurrentlyAttachedStudioTarget = {
|
||||
targetId: '101',
|
||||
}
|
||||
|
||||
const mockUpdatedCurrentlyAttachedTarget = {
|
||||
targetId: '101',
|
||||
clone: sinon.stub().onFirstCall().returns(mockUpdatedCurrentlyAttachedProtocolTarget).onSecondCall().returns(mockUpdatedCurrentlyAttachedCyPromptTarget),
|
||||
clone: sinon.stub()
|
||||
.onFirstCall().returns(mockUpdatedCurrentlyAttachedProtocolTarget)
|
||||
.onSecondCall().returns(mockUpdatedCurrentlyAttachedCyPromptTarget)
|
||||
.onThirdCall().returns(mockUpdatedCurrentlyAttachedStudioTarget),
|
||||
}
|
||||
|
||||
send.withArgs('Target.createTarget', { url: 'about:blank' }).resolves(mockUpdatedCurrentlyAttachedTarget)
|
||||
@@ -595,6 +656,7 @@ describe('lib/browsers/browser-cri-client', function () {
|
||||
browserClient.currentlyAttachedTarget = mockCurrentlyAttachedTarget
|
||||
browserClient.currentlyAttachedProtocolTarget = mockCurrentlyAttachedProtocolTarget
|
||||
browserClient.currentlyAttachedCyPromptTarget = mockCurrentlyAttachedCyPromptTarget
|
||||
browserClient.currentlyAttachedStudioTarget = mockCurrentlyAttachedStudioTarget
|
||||
browserClient.browserClient.off = sinon.stub()
|
||||
|
||||
await browserClient.resetBrowserTargets(true)
|
||||
@@ -603,9 +665,11 @@ describe('lib/browsers/browser-cri-client', function () {
|
||||
expect(browserClient.currentlyAttachedTarget).to.eql(mockUpdatedCurrentlyAttachedTarget)
|
||||
expect(browserClient.currentlyAttachedProtocolTarget).to.eql(mockUpdatedCurrentlyAttachedProtocolTarget)
|
||||
expect(browserClient.currentlyAttachedCyPromptTarget).to.eql(mockUpdatedCurrentlyAttachedCyPromptTarget)
|
||||
expect(browserClient.currentlyAttachedStudioTarget).to.eql(mockUpdatedCurrentlyAttachedStudioTarget)
|
||||
expect(browserClient.browserClient.off).to.be.calledWith('Network.requestWillBeSent', mockCurrentlyAttachedTarget.queue.subscriptions[0].cb)
|
||||
expect(browserClient.browserClient.off).to.be.calledWith('Network.requestWillBeSent', mockCurrentlyAttachedProtocolTarget.queue.subscriptions[0].cb)
|
||||
expect(browserClient.browserClient.off).to.be.calledWith('Network.requestWillBeSent', mockCurrentlyAttachedCyPromptTarget.queue.subscriptions[0].cb)
|
||||
expect(browserClient.browserClient.off).to.be.calledWith('Network.requestWillBeSent', mockCurrentlyAttachedStudioTarget.queue.subscriptions[0].cb)
|
||||
})
|
||||
|
||||
it('closes the currently attached target without keeping a tab open', async function () {
|
||||
@@ -633,6 +697,14 @@ describe('lib/browsers/browser-cri-client', function () {
|
||||
},
|
||||
}
|
||||
|
||||
const mockCurrentlyAttachedStudioTarget = {
|
||||
targetId: '100',
|
||||
close: sinon.stub().resolves(sinon.stub().resolves()),
|
||||
queue: {
|
||||
subscriptions: [],
|
||||
},
|
||||
}
|
||||
|
||||
send.withArgs('Target.closeTarget', { targetId: '100' }).resolves()
|
||||
|
||||
const browserClient = await getClient() as any
|
||||
@@ -640,15 +712,18 @@ describe('lib/browsers/browser-cri-client', function () {
|
||||
browserClient.currentlyAttachedTarget = mockCurrentlyAttachedTarget
|
||||
browserClient.currentlyAttachedProtocolTarget = mockCurrentlyAttachedProtocolTarget
|
||||
browserClient.currentlyAttachedCyPromptTarget = mockCurrentlyAttachedCyPromptTarget
|
||||
browserClient.currentlyAttachedStudioTarget = mockCurrentlyAttachedStudioTarget
|
||||
|
||||
await browserClient.resetBrowserTargets(false)
|
||||
|
||||
expect(mockCurrentlyAttachedTarget.close).to.be.called
|
||||
expect(mockCurrentlyAttachedProtocolTarget.close).to.be.called
|
||||
expect(mockCurrentlyAttachedCyPromptTarget.close).to.be.called
|
||||
expect(mockCurrentlyAttachedStudioTarget.close).to.be.called
|
||||
expect(browserClient.currentlyAttachedTarget).to.be.undefined
|
||||
expect(browserClient.currentlyAttachedProtocolTarget).to.be.undefined
|
||||
expect(browserClient.currentlyAttachedCyPromptTarget).to.be.undefined
|
||||
expect(browserClient.currentlyAttachedStudioTarget).to.be.undefined
|
||||
})
|
||||
|
||||
it('throws when there is no currently attached target', async function () {
|
||||
@@ -704,17 +779,23 @@ describe('lib/browsers/browser-cri-client', function () {
|
||||
close: sinon.stub().resolves(),
|
||||
}
|
||||
|
||||
const mockCurrentlyAttachedStudioTarget = {
|
||||
close: sinon.stub().resolves(),
|
||||
}
|
||||
|
||||
const browserClient = await getClient() as any
|
||||
|
||||
browserClient.currentlyAttachedTarget = mockCurrentlyAttachedTarget
|
||||
browserClient.currentlyAttachedProtocolTarget = mockCurrentlyAttachedProtocolTarget
|
||||
browserClient.currentlyAttachedCyPromptTarget = mockCurrentlyAttachedCyPromptTarget
|
||||
browserClient.currentlyAttachedStudioTarget = mockCurrentlyAttachedStudioTarget
|
||||
|
||||
await browserClient.close()
|
||||
|
||||
expect(mockCurrentlyAttachedTarget.close).to.be.called
|
||||
expect(mockCurrentlyAttachedProtocolTarget.close).to.be.called
|
||||
expect(mockCurrentlyAttachedCyPromptTarget.close).to.be.called
|
||||
expect(mockCurrentlyAttachedStudioTarget.close).to.be.called
|
||||
})
|
||||
|
||||
it('just the browser client with no currently attached target', async function () {
|
||||
|
||||
@@ -169,6 +169,20 @@ describe('lib/browsers/index', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('.connectStudioToBrowser', () => {
|
||||
it('connects browser to studio', async () => {
|
||||
sinon.stub(chrome, 'connectStudioToBrowser').resolves()
|
||||
await browsers.connectStudioToBrowser({
|
||||
browser: {
|
||||
family: 'chromium',
|
||||
},
|
||||
studioManager: {} as any,
|
||||
})
|
||||
|
||||
expect(chrome.connectStudioToBrowser).to.be.called
|
||||
})
|
||||
})
|
||||
|
||||
context('.closeProtocolConnection', () => {
|
||||
it('calls close on instance', async () => {
|
||||
sinon.stub(chrome, 'closeProtocolConnection').resolves()
|
||||
|
||||
@@ -754,6 +754,82 @@ describe('lib/browsers/chrome', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('#connectStudioToBrowser', () => {
|
||||
it('connects to the browser cri client', async function () {
|
||||
const studioManager = {
|
||||
connectToBrowser: sinon.stub().resolves(),
|
||||
}
|
||||
|
||||
const mockCurrentlyAttachedStudioTarget = {}
|
||||
|
||||
const pageCriClient = {
|
||||
clone: sinon.stub().returns(mockCurrentlyAttachedStudioTarget),
|
||||
}
|
||||
|
||||
const browserCriClient = {
|
||||
currentlyAttachedTarget: pageCriClient,
|
||||
currentlyAttachedStudioTarget: mockCurrentlyAttachedStudioTarget,
|
||||
}
|
||||
|
||||
sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient)
|
||||
|
||||
await chrome.connectStudioToBrowser({ studioManager })
|
||||
|
||||
expect(pageCriClient.clone).not.to.be.called
|
||||
expect(studioManager.connectToBrowser).to.be.calledWith(mockCurrentlyAttachedStudioTarget)
|
||||
})
|
||||
|
||||
it('connects to the browser cri client when the studio target has not been created', async function () {
|
||||
const studioManager = {
|
||||
connectToBrowser: sinon.stub().resolves(),
|
||||
}
|
||||
|
||||
const mockCurrentlyAttachedStudioTarget = {}
|
||||
|
||||
const pageCriClient = {
|
||||
clone: sinon.stub().resolves(mockCurrentlyAttachedStudioTarget),
|
||||
}
|
||||
|
||||
const browserCriClient = {
|
||||
currentlyAttachedTarget: pageCriClient,
|
||||
}
|
||||
|
||||
sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient)
|
||||
|
||||
await chrome.connectStudioToBrowser({ studioManager })
|
||||
|
||||
expect(pageCriClient.clone).to.be.called
|
||||
expect(studioManager.connectToBrowser).to.be.calledWith(mockCurrentlyAttachedStudioTarget)
|
||||
expect(browserCriClient.currentlyAttachedStudioTarget).to.eq(mockCurrentlyAttachedStudioTarget)
|
||||
})
|
||||
|
||||
it('throws error if there is no browser cri client', function () {
|
||||
const studioManager = {
|
||||
connectToBrowser: sinon.stub().resolves(),
|
||||
}
|
||||
|
||||
sinon.stub(chrome, '_getBrowserCriClient').returns(null)
|
||||
|
||||
expect(chrome.connectStudioToBrowser({ studioManager })).to.be.rejectedWith('Missing pageCriClient in connectStudioToBrowser')
|
||||
expect(studioManager.connectToBrowser).not.to.be.called
|
||||
})
|
||||
|
||||
it('throws error if there is no page cri client', function () {
|
||||
const studioManager = {
|
||||
connectToBrowser: sinon.stub().resolves(),
|
||||
}
|
||||
|
||||
const browserCriClient = {
|
||||
currentlyAttachedTarget: null,
|
||||
}
|
||||
|
||||
sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient)
|
||||
|
||||
expect(chrome.connectStudioToBrowser({ studioManager })).to.be.rejectedWith('Missing pageCriClient in connectStudioToBrowser')
|
||||
expect(studioManager.connectToBrowser).not.to.be.called
|
||||
})
|
||||
})
|
||||
|
||||
context('#closeProtocolConnection', () => {
|
||||
it('closes the protocol connection', async function () {
|
||||
const mockCurrentlyAttachedProtocolTarget = {
|
||||
|
||||
@@ -28,6 +28,10 @@ describe('lib/browsers/electron', () => {
|
||||
connectToBrowser: sinon.stub().resolves(),
|
||||
}
|
||||
|
||||
this.studioManager = {
|
||||
connectToBrowser: sinon.stub().resolves(),
|
||||
}
|
||||
|
||||
this.url = 'https://foo.com'
|
||||
this.state = {}
|
||||
this.options = {
|
||||
@@ -325,6 +329,47 @@ describe('lib/browsers/electron', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('.connectStudioToBrowser', () => {
|
||||
it('connects to the browser cri client', async function () {
|
||||
const mockCurrentlyAttachedStudioTarget = {}
|
||||
|
||||
this.browserCriClient.currentlyAttachedStudioTarget = mockCurrentlyAttachedStudioTarget
|
||||
sinon.stub(electron, '_getBrowserCriClient').returns(this.browserCriClient)
|
||||
|
||||
await electron.connectStudioToBrowser({ studioManager: this.studioManager })
|
||||
expect(this.pageCriClient.clone).not.to.be.called
|
||||
expect(this.studioManager.connectToBrowser).to.be.calledWith(mockCurrentlyAttachedStudioTarget)
|
||||
})
|
||||
|
||||
it('connects to the browser cri client when the studio target has not been created', async function () {
|
||||
const mockCurrentlyAttachedStudioTarget = {}
|
||||
|
||||
this.pageCriClient.clone.resolves(mockCurrentlyAttachedStudioTarget)
|
||||
sinon.stub(electron, '_getBrowserCriClient').returns(this.browserCriClient)
|
||||
|
||||
await electron.connectStudioToBrowser({ studioManager: this.studioManager })
|
||||
expect(this.pageCriClient.clone).to.be.called
|
||||
expect(this.studioManager.connectToBrowser).to.be.calledWith(mockCurrentlyAttachedStudioTarget)
|
||||
expect(this.browserCriClient.currentlyAttachedStudioTarget).to.eq(mockCurrentlyAttachedStudioTarget)
|
||||
})
|
||||
|
||||
it('throws error if there is no browser cri client', function () {
|
||||
sinon.stub(electron, '_getBrowserCriClient').returns(null)
|
||||
|
||||
expect(electron.connectStudioToBrowser({ studioManager: this.studioManager })).to.be.rejectedWith('Missing pageCriClient in connectStudioToBrowser')
|
||||
expect(this.studioManager.connectToBrowser).not.to.be.called
|
||||
})
|
||||
|
||||
it('throws error if there is no page cri client', async function () {
|
||||
this.browserCriClient.currentlyAttachedTarget = null
|
||||
|
||||
sinon.stub(electron, '_getBrowserCriClient').returns(this.browserCriClient)
|
||||
|
||||
expect(electron.connectStudioToBrowser({ studioManager: this.studioManager })).to.be.rejectedWith('Missing pageCriClient in connectStudioToBrowser')
|
||||
expect(this.studioManager.connectToBrowser).not.to.be.called
|
||||
})
|
||||
})
|
||||
|
||||
context('#closeProtocolConnection', () => {
|
||||
it('closes the protocol connection', async function () {
|
||||
const mockCurrentlyAttachedProtocolTarget = {
|
||||
|
||||
@@ -346,4 +346,38 @@ describe('lib/cloud/studio', () => {
|
||||
expect(studio.destroy).to.be.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('connectToBrowser', () => {
|
||||
it('calls connectToBrowser on the studio server', () => {
|
||||
const mockCDPClient = {
|
||||
send: sinon.stub(),
|
||||
on: sinon.stub(),
|
||||
off: sinon.stub(),
|
||||
}
|
||||
|
||||
sinon.stub(studio, 'connectToBrowser')
|
||||
|
||||
studioManager.connectToBrowser(mockCDPClient as any)
|
||||
|
||||
expect(studio.connectToBrowser).to.be.calledWith(mockCDPClient)
|
||||
})
|
||||
|
||||
it('does not call connectToBrowser when studio server is not defined', () => {
|
||||
// Set _studioServer to undefined
|
||||
(studioManager as any)._studioServer = undefined
|
||||
|
||||
// Create a spy on invokeSync to verify it's not called
|
||||
const invokeSyncSpy = sinon.spy(studioManager, 'invokeSync')
|
||||
|
||||
const mockCDPClient = {
|
||||
send: sinon.stub(),
|
||||
on: sinon.stub(),
|
||||
off: sinon.stub(),
|
||||
}
|
||||
|
||||
studioManager.connectToBrowser(mockCDPClient as any)
|
||||
|
||||
expect(invokeSyncSpy).to.not.be.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -870,6 +870,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
sinon.stub(browsers, 'closeProtocolConnection').resolves()
|
||||
|
||||
sinon.stub(browsers, 'connectProtocolToBrowser').resolves()
|
||||
sinon.stub(browsers, 'connectStudioToBrowser').resolves()
|
||||
sinon.stub(this.project, 'protocolManager').get(() => {
|
||||
return this.project['_protocolManager']
|
||||
}).set((protocolManager) => {
|
||||
@@ -905,6 +906,12 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
protocolManager: studioManager.protocolManager,
|
||||
})
|
||||
|
||||
expect(browsers.connectStudioToBrowser).to.be.calledWith({
|
||||
browser: this.project.browser,
|
||||
foundBrowsers: this.project.options.browsers,
|
||||
studioManager: studioManager,
|
||||
})
|
||||
|
||||
expect(this.project['_protocolManager']).to.eq(studioManager.protocolManager)
|
||||
|
||||
expect(markStub).to.be.calledWith(MARK_NAMES.INITIALIZATION_START)
|
||||
@@ -913,6 +920,8 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
expect(markStub).to.be.calledWith(MARK_NAMES.CAN_ACCESS_STUDIO_AI_END)
|
||||
expect(markStub).to.be.calledWith(MARK_NAMES.CONNECT_PROTOCOL_TO_BROWSER_START)
|
||||
expect(markStub).to.be.calledWith(MARK_NAMES.CONNECT_PROTOCOL_TO_BROWSER_END)
|
||||
expect(markStub).to.be.calledWith(MARK_NAMES.CONNECT_STUDIO_TO_BROWSER_START)
|
||||
expect(markStub).to.be.calledWith(MARK_NAMES.CONNECT_STUDIO_TO_BROWSER_END)
|
||||
expect(markStub).to.be.calledWith(MARK_NAMES.INITIALIZE_STUDIO_AI_START)
|
||||
expect(markStub).to.be.calledWith(MARK_NAMES.INITIALIZE_STUDIO_AI_END)
|
||||
expect(reportTelemetryStub).to.be.calledWith(TELEMETRY_GROUP_NAMES.INITIALIZE_STUDIO, {
|
||||
@@ -967,6 +976,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
sinon.stub(browsers, 'closeProtocolConnection').resolves()
|
||||
|
||||
sinon.stub(browsers, 'connectProtocolToBrowser').resolves()
|
||||
sinon.stub(browsers, 'connectStudioToBrowser').resolves()
|
||||
sinon.stub(this.project, 'protocolManager').get(() => {
|
||||
return this.project['_protocolManager']
|
||||
}).set((protocolManager) => {
|
||||
@@ -1032,6 +1042,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
|
||||
sinon.stub(browsers, 'closeProtocolConnection').resolves()
|
||||
sinon.stub(browsers, 'connectProtocolToBrowser').resolves()
|
||||
sinon.stub(browsers, 'connectStudioToBrowser').resolves()
|
||||
sinon.stub(this.project, 'protocolManager').get(() => {
|
||||
return this.project['_protocolManager']
|
||||
}).set((protocolManager) => {
|
||||
@@ -1087,6 +1098,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
sinon.stub(browsers, 'closeProtocolConnection').resolves()
|
||||
|
||||
sinon.stub(browsers, 'connectProtocolToBrowser').resolves()
|
||||
sinon.stub(browsers, 'connectStudioToBrowser').resolves()
|
||||
sinon.stub(this.project, 'protocolManager').get(() => {
|
||||
return this.project['_protocolManager']
|
||||
}).set((protocolManager) => {
|
||||
@@ -1166,6 +1178,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
sinon.stub(browsers, 'closeProtocolConnection').resolves()
|
||||
|
||||
sinon.stub(browsers, 'connectProtocolToBrowser').resolves()
|
||||
sinon.stub(browsers, 'connectStudioToBrowser').resolves()
|
||||
sinon.stub(this.project, 'protocolManager').get(() => {
|
||||
return this.project['_protocolManager']
|
||||
}).set((protocolManager) => {
|
||||
@@ -1187,6 +1200,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
expect(mockSetupProtocol).not.to.be.called
|
||||
expect(mockBeforeSpec).not.to.be.called
|
||||
expect(browsers.connectProtocolToBrowser).not.to.be.called
|
||||
expect(browsers.connectStudioToBrowser).not.to.be.called
|
||||
expect(this.project['_protocolManager']).to.be.undefined
|
||||
|
||||
expect(markStub).to.be.calledWith(MARK_NAMES.INITIALIZATION_START)
|
||||
@@ -1195,6 +1209,8 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
expect(markStub).to.be.calledWith(MARK_NAMES.CAN_ACCESS_STUDIO_AI_END)
|
||||
expect(markStub).not.to.be.calledWith(MARK_NAMES.CONNECT_PROTOCOL_TO_BROWSER_START)
|
||||
expect(markStub).not.to.be.calledWith(MARK_NAMES.CONNECT_PROTOCOL_TO_BROWSER_END)
|
||||
expect(markStub).not.to.be.calledWith(MARK_NAMES.CONNECT_STUDIO_TO_BROWSER_START)
|
||||
expect(markStub).not.to.be.calledWith(MARK_NAMES.CONNECT_STUDIO_TO_BROWSER_END)
|
||||
expect(markStub).not.to.be.calledWith(MARK_NAMES.INITIALIZE_STUDIO_AI_START)
|
||||
expect(markStub).not.to.be.calledWith(MARK_NAMES.INITIALIZE_STUDIO_AI_END)
|
||||
expect(reportTelemetryStub).to.be.calledWith(TELEMETRY_GROUP_NAMES.INITIALIZE_STUDIO, {
|
||||
@@ -1203,62 +1219,6 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
|
||||
})
|
||||
})
|
||||
|
||||
it('does not capture studio started event if the user is accessing cloud studio', async function () {
|
||||
process.env.CYPRESS_LOCAL_STUDIO_PATH = 'false'
|
||||
|
||||
const mockAccessStudioAI = sinon.stub().resolves(true)
|
||||
const mockCaptureStudioEvent = sinon.stub().resolves()
|
||||
|
||||
this.project.spec = {}
|
||||
|
||||
this.project._cfg = this.project._cfg || {}
|
||||
this.project._cfg.projectId = 'test-project-id'
|
||||
this.project.ctx.coreData.user = { email: 'test@example.com' }
|
||||
this.project.ctx.coreData.machineId = Promise.resolve('test-machine-id')
|
||||
|
||||
const studioManager = new StudioManager()
|
||||
|
||||
studioManager.canAccessStudioAI = mockAccessStudioAI
|
||||
studioManager.captureStudioEvent = mockCaptureStudioEvent
|
||||
const studioLifecycleManager = new StudioLifecycleManager()
|
||||
|
||||
this.project.ctx.coreData.studioLifecycleManager = studioLifecycleManager
|
||||
|
||||
studioLifecycleManager.studioManagerPromise = Promise.resolve(studioManager)
|
||||
|
||||
studioLifecycleManager.isStudioReady = sinon.stub().returns(true)
|
||||
|
||||
// Create a browser object
|
||||
this.project.browser = {
|
||||
name: 'chrome',
|
||||
family: 'chromium',
|
||||
}
|
||||
|
||||
this.project.options = { browsers: [this.project.browser] }
|
||||
|
||||
sinon.stub(browsers, 'closeProtocolConnection').resolves()
|
||||
|
||||
sinon.stub(browsers, 'connectProtocolToBrowser').resolves()
|
||||
sinon.stub(this.project, 'protocolManager').get(() => {
|
||||
return this.project['_protocolManager']
|
||||
}).set((protocolManager) => {
|
||||
this.project['_protocolManager'] = protocolManager
|
||||
})
|
||||
|
||||
let studioInitPromise
|
||||
|
||||
this.project.server.startWebsockets.callsFake(async (automation, config, callbacks) => {
|
||||
studioInitPromise = callbacks.onStudioInit()
|
||||
})
|
||||
|
||||
this.project.startWebsockets({}, {})
|
||||
|
||||
const { canAccessStudioAI } = await studioInitPromise
|
||||
|
||||
expect(canAccessStudioAI).to.be.false
|
||||
expect(mockCaptureStudioEvent).not.to.be.called
|
||||
})
|
||||
|
||||
it('onStudioDestroy destroys studio when it is initialized', async function () {
|
||||
// Set up minimal required properties
|
||||
this.project.ctx = this.project.ctx || {}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.d'
|
||||
import type { Router } from 'express'
|
||||
import type { AxiosInstance } from 'axios'
|
||||
import type { Socket } from 'socket.io'
|
||||
@@ -112,11 +113,36 @@ export interface StudioAddSocketListenersOptions {
|
||||
onAfterSave: (options: { error?: Error }) => void
|
||||
}
|
||||
|
||||
export type StudioCDPCommands = ProtocolMapping.Commands
|
||||
|
||||
export type StudioCDPCommand<T extends keyof StudioCDPCommands> =
|
||||
StudioCDPCommands[T]
|
||||
|
||||
export type StudioCDPEvents = ProtocolMapping.Events
|
||||
|
||||
export type StudioCDPEvent<T extends keyof StudioCDPEvents> = StudioCDPEvents[T]
|
||||
|
||||
export interface StudioCDPClient {
|
||||
send<T extends Extract<keyof StudioCDPCommands, string>>(
|
||||
command: T,
|
||||
params?: StudioCDPCommand<T>['paramsType'][0]
|
||||
): Promise<StudioCDPCommand<T>['returnType']>
|
||||
on<T extends Extract<keyof StudioCDPEvents, string>>(
|
||||
eventName: T,
|
||||
cb: (event: StudioCDPEvent<T>[0]) => void | Promise<unknown>
|
||||
): void
|
||||
off<T extends Extract<keyof StudioCDPEvents, string>>(
|
||||
eventName: T,
|
||||
cb: (event: StudioCDPEvent<T>[0]) => void | Promise<unknown>
|
||||
): void
|
||||
}
|
||||
|
||||
export interface StudioServerShape {
|
||||
initializeRoutes(router: Router): void
|
||||
canAccessStudioAI(browser: Cypress.Browser): Promise<boolean>
|
||||
addSocketListeners(options: StudioAddSocketListenersOptions | Socket): void
|
||||
initializeStudioAI(options: StudioAIInitializeOptions): Promise<void>
|
||||
connectToBrowser(cdpClient: StudioCDPClient): void
|
||||
updateSessionId(sessionId: string): void
|
||||
reportError(
|
||||
error: unknown,
|
||||
|
||||
Reference in New Issue
Block a user