Skip to content

Commit

Permalink
refactor: move more of video capture into browser automations
Browse files Browse the repository at this point in the history
  • Loading branch information
flotwig committed Aug 27, 2022
1 parent 5e74399 commit d25b9e0
Show file tree
Hide file tree
Showing 16 changed files with 164 additions and 238 deletions.
2 changes: 0 additions & 2 deletions packages/resolve-dist/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,5 @@ export const getPathToIndex = (pkg: RunnerPkg) => {
}

export const getPathToDesktopIndex = (graphqlPort: number) => {
// For now, if we see that there's a CYPRESS_INTERNAL_VITE_DEV
// we assume we're running Cypress targeting that (dev server)
return `http://localhost:${graphqlPort}/__launchpad/index.html`
}
14 changes: 12 additions & 2 deletions packages/server/lib/browsers/cdp_automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { URL } from 'url'

import type { Automation } from '../automation'
import type { ResourceType, BrowserPreRequest, BrowserResponseReceived } from '@packages/proxy'
import type { WriteVideoFrame } from '@packages/types'

export type CdpCommand = keyof ProtocolMapping.Commands

Expand Down Expand Up @@ -168,9 +169,9 @@ export const normalizeResourceType = (resourceType: string | undefined): Resourc
return ffToStandardResourceTypeMap[resourceType] || 'other'
}

type SendDebuggerCommand = (message: CdpCommand, data?: any) => Promise<any>
type SendDebuggerCommand = <T extends CdpCommand>(message: T, data?: any) => Promise<ProtocolMapping.Commands[T]['returnType']>
type SendCloseCommand = (shouldKeepTabOpen: boolean) => Promise<any> | void
type OnFn = (eventName: CdpEvent, cb: Function) => void
type OnFn = <T extends CdpEvent>(eventName: T, cb: (data: ProtocolMapping.Events[T][0]) => void) => void

// the intersection of what's valid in CDP and what's valid in FFCDP
// Firefox: https://searchfox.org/mozilla-central/rev/98a9257ca2847fad9a19631ac76199474516b31e/remote/cdp/domains/parent/Network.jsm#22
Expand All @@ -188,6 +189,15 @@ export class CdpAutomation {
onFn('Network.responseReceived', this.onResponseReceived)
}

async startVideoRecording (writeVideoFrame: WriteVideoFrame, screencastOpts?) {
this.onFn('Page.screencastFrame', async (e) => {
writeVideoFrame(Buffer.from(e.data, 'base64'))
await this.sendDebuggerCommandFn('Page.screencastFrameAck', { sessionId: e.sessionId })
})

await this.sendDebuggerCommandFn('Page.startScreencast', screencastOpts)
}

static async create (sendDebuggerCommandFn: SendDebuggerCommand, onFn: OnFn, sendCloseCommandFn: SendCloseCommand, automation: Automation, experimentalSessionAndOrigin: boolean): Promise<CdpAutomation> {
const cdpAutomation = new CdpAutomation(sendDebuggerCommandFn, onFn, sendCloseCommandFn, automation, experimentalSessionAndOrigin)

Expand Down
38 changes: 14 additions & 24 deletions packages/server/lib/browsers/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { BrowserCriClient } from './browser-cri-client'
import type { LaunchedBrowser } from '@packages/launcher/lib/browsers'
import type { CriClient } from './cri-client'
import type { Automation } from '../automation'
import type { BrowserLaunchOpts, BrowserNewTabOpts } from '@packages/types'
import type { BrowserLaunchOpts, BrowserNewTabOpts, WriteVideoFrame } from '@packages/types'

const debug = debugModule('cypress:server:browsers:chrome')

Expand Down Expand Up @@ -249,22 +249,10 @@ const _disableRestorePagesPrompt = function (userDir) {
.catch(() => { })
}

const _maybeRecordVideo = async function (client, options, browserMajorVersion) {
if (!options.onScreencastFrame) {
debug('options.onScreencastFrame is false')
async function _recordVideo (cdpAutomation: CdpAutomation, writeVideoFrame: WriteVideoFrame, browserMajorVersion: number) {
const opts = browserMajorVersion >= CHROME_VERSION_WITH_FPS_INCREASE ? screencastOpts() : screencastOpts(1)

return client
}

debug('starting screencast')
client.on('Page.screencastFrame', (meta) => {
options.onScreencastFrame(meta)
client.send('Page.screencastFrameAck', { sessionId: meta.sessionId })
})

await client.send('Page.startScreencast', browserMajorVersion >= CHROME_VERSION_WITH_FPS_INCREASE ? screencastOpts() : screencastOpts(1))

return client
await cdpAutomation.startVideoRecording(writeVideoFrame, opts)
}

// a utility function that navigates to the given URL
Expand Down Expand Up @@ -434,7 +422,9 @@ const _handlePausedRequests = async (client) => {
const _setAutomation = async (client: CriClient, automation: Automation, resetBrowserTargets: (shouldKeepTabOpen: boolean) => Promise<void>, options: BrowserLaunchOpts) => {
const cdpAutomation = await CdpAutomation.create(client.send, client.on, resetBrowserTargets, automation, !!options.experimentalSessionAndOrigin)

return automation.use(cdpAutomation)
automation.use(cdpAutomation)

return cdpAutomation
}

export = {
Expand All @@ -448,7 +438,7 @@ export = {

_removeRootExtension,

_maybeRecordVideo,
_recordVideo,

_navigateUsingCRI,

Expand All @@ -468,7 +458,7 @@ export = {
return browserCriClient
},

async _writeExtension (browser: Browser, options) {
async _writeExtension (browser: Browser, options: BrowserLaunchOpts) {
if (browser.isHeadless) {
debug('chrome is running headlessly, not installing extension')

Expand Down Expand Up @@ -565,7 +555,7 @@ export = {
await this.attachListeners(browser, options.url, pageCriClient, automation, options)
},

async connectToExisting (browser: Browser, options: BrowserLaunchOpts, automation) {
async connectToExisting (browser: Browser, options: BrowserLaunchOpts, automation: Automation) {
const port = await protocol.getRemoteDebuggingPort()

debug('connecting to existing chrome instance with url and debugging port', { url: options.url, port })
Expand All @@ -580,17 +570,17 @@ export = {
await this._setAutomation(pageCriClient, automation, browserCriClient.resetBrowserTargets, options)
},

async attachListeners (browser: Browser, url: string, pageCriClient, automation: Automation, options: BrowserLaunchOpts & { onInitializeNewBrowserTab?: () => void }) {
async attachListeners (browser: Browser, url: string, pageCriClient: CriClient, automation: Automation, options: BrowserLaunchOpts | BrowserNewTabOpts) {
if (!browserCriClient) throw new Error('Missing browserCriClient in attachListeners')

await this._setAutomation(pageCriClient, automation, browserCriClient.resetBrowserTargets, options)
const cdpAutomation = await this._setAutomation(pageCriClient, automation, browserCriClient.resetBrowserTargets, options)

await pageCriClient.send('Page.enable')

await options.onInitializeNewBrowserTab?.()
await options['onInitializeNewBrowserTab']?.()

await Promise.all([
this._maybeRecordVideo(pageCriClient, options, browser.majorVersion),
options.writeVideoFrame && this._recordVideo(cdpAutomation, options.writeVideoFrame, browser.majorVersion),
this._handleDownloads(pageCriClient, options.downloadsFolder, automation),
])

Expand Down
96 changes: 44 additions & 52 deletions packages/server/lib/browsers/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import { CdpAutomation, screencastOpts, CdpCommand, CdpEvent } from './cdp_autom
import * as savedState from '../saved_state'
import utils from './utils'
import * as errors from '../errors'
import type { BrowserInstance } from './types'
import type { Browser, BrowserInstance } from './types'
import type { BrowserWindow, WebContents } from 'electron'
import type { Automation } from '../automation'
import type { BrowserLaunchOpts, Preferences } from '@packages/types'

// TODO: unmix these two types
type ElectronOpts = Windows.WindowOptions & BrowserLaunchOpts

const debug = Debug('cypress:server:browsers:electron')
const debugVerbose = Debug('cypress-verbose:server:browsers:electron')
Expand Down Expand Up @@ -68,7 +72,7 @@ const _getAutomation = async function (win, options, parent) {
// after upgrading to Electron 8, CDP screenshots can hang if a screencast is not also running
// workaround: start and stop screencasts between screenshots
// @see https://github.com/cypress-io/cypress/pull/6555#issuecomment-596747134
if (!options.onScreencastFrame) {
if (!options.writeVideoFrame) {
await sendCommand('Page.startScreencast', screencastOpts())
const ret = await fn(message, data)

Expand Down Expand Up @@ -105,37 +109,18 @@ function _installExtensions (win: BrowserWindow, extensionPaths: string[], optio
}))
}

const _maybeRecordVideo = async function (webContents, options) {
const { onScreencastFrame } = options

debug('maybe recording video %o', { onScreencastFrame })

if (!onScreencastFrame) {
return
}

webContents.debugger.on('message', (event, method, params) => {
if (method === 'Page.screencastFrame') {
onScreencastFrame(params)
webContents.debugger.sendCommand('Page.screencastFrameAck', { sessionId: params.sessionId })
}
})

await webContents.debugger.sendCommand('Page.startScreencast', screencastOpts())
}

export = {
_defaultOptions (projectRoot, state, options, automation) {
_defaultOptions (projectRoot: string | undefined, state: Preferences, options: BrowserLaunchOpts, automation: Automation): ElectronOpts {
const _this = this

const defaults = {
x: state.browserX,
y: state.browserY,
const defaults: Windows.WindowOptions = {
x: state.browserX || undefined,
y: state.browserY || undefined,
width: state.browserWidth || 1280,
height: state.browserHeight || 720,
devTools: state.isBrowserDevToolsOpen,
minWidth: 100,
minHeight: 100,
devTools: state.isBrowserDevToolsOpen || undefined,
contextMenu: true,
partition: this._getPartition(options),
trackState: {
Expand All @@ -148,8 +133,21 @@ export = {
webPreferences: {
sandbox: true,
},
show: !options.browser.isHeadless,
// prevents a tiny 1px padding around the window
// causing screenshots/videos to be off by 1px
resizable: !options.browser.isHeadless,
onCrashed () {
const err = errors.get('RENDERER_CRASHED')

errors.log(err)

if (!options.onError) throw new Error('Missing onError in onCrashed')

options.onError(err)
},
onFocus () {
if (options.show) {
if (!options.browser.isHeadless) {
return menu.set({ withInternalDevTools: true })
}
},
Expand All @@ -176,18 +174,12 @@ export = {
},
}

if (options.browser.isHeadless) {
// prevents a tiny 1px padding around the window
// causing screenshots/videos to be off by 1px
options.resizable = false
}

return _.defaultsDeep({}, options, defaults)
},

_getAutomation,

async _render (url: string, automation: Automation, preferences, options: { projectRoot: string, isTextTerminal: boolean }) {
async _render (url: string, automation: Automation, preferences, options: { projectRoot?: string, isTextTerminal: boolean }) {
const win = Windows.create(options.projectRoot, preferences)

if (preferences.browser.isHeadless) {
Expand All @@ -212,21 +204,25 @@ export = {

const [parentX, parentY] = parent.getPosition()

options = this._defaultOptions(projectRoot, state, options, automation)
const electronOptions = this._defaultOptions(projectRoot, state, options, automation)

_.extend(options, {
_.extend(electronOptions, {
x: parentX + 100,
y: parentY + 100,
trackState: false,
// in run mode, force new windows to automatically open with show: false
// this prevents window.open inside of javascript client code to cause a new BrowserWindow instance to open
// https://github.com/cypress-io/cypress/issues/123
show: !options.isTextTerminal,
})

const win = Windows.create(projectRoot, options)
const win = Windows.create(projectRoot, electronOptions)

// needed by electron since we prevented default and are creating
// our own BrowserWindow (https://electron.atom.io/docs/api/web-contents/#event-new-window)
e.newGuest = win

return this._launch(win, url, automation, options)
return this._launch(win, url, automation, electronOptions)
},

async _launch (win: BrowserWindow, url: string, automation: Automation, options) {
Expand Down Expand Up @@ -278,7 +274,7 @@ export = {

automation.use(cdpAutomation)
await Promise.all([
_maybeRecordVideo(win.webContents, options),
options.writeVideoFrame && cdpAutomation.startVideoRecording(options.writeVideoFrame),
this._handleDownloads(win, options.downloadsFolder, automation),
])

Expand Down Expand Up @@ -459,30 +455,26 @@ export = {
throw new Error('Attempting to connect to existing browser for Cypress in Cypress which is not yet implemented for electron')
},

async open (browser, url, options, automation) {
const { projectRoot, isTextTerminal } = options

async open (browser: Browser, url: string, options: BrowserLaunchOpts, automation: Automation) {
debug('open %o', { browser, url })

const State = await savedState.create(projectRoot, isTextTerminal)
const State = await savedState.create(options.projectRoot, options.isTextTerminal)
const state = await State.get()

debug('received saved state %o', state)

// get our electron default options
// TODO: this is bad, don't mutate the options object
options = this._defaultOptions(projectRoot, state, options, automation)

// get the GUI window defaults now
options = Windows.defaults(options)
const electronOptions: ElectronOpts = Windows.defaults(
this._defaultOptions(options.projectRoot, state, options, automation),
)

debug('browser window options %o', _.omitBy(options, _.isFunction))
debug('browser window options %o', _.omitBy(electronOptions, _.isFunction))

const defaultLaunchOptions = utils.getDefaultLaunchOptions({
preferences: options,
preferences: electronOptions,
})

const launchOptions = await utils.executeBeforeBrowserLaunch(browser, defaultLaunchOptions, options)
const launchOptions = await utils.executeBeforeBrowserLaunch(browser, defaultLaunchOptions, electronOptions)

const { preferences } = launchOptions

Expand All @@ -493,7 +485,7 @@ export = {
isTextTerminal: options.isTextTerminal,
})

await _installExtensions(win, launchOptions.extensions, options)
await _installExtensions(win, launchOptions.extensions, electronOptions)

// cause the webview to receive focus so that
// native browser focus + blur events fire correctly
Expand Down
10 changes: 5 additions & 5 deletions packages/server/lib/browsers/firefox.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import _ from 'lodash'
import Bluebird from 'bluebird'
import fs from 'fs-extra'
import Debug from 'debug'
import getPort from 'get-port'
Expand All @@ -21,6 +20,7 @@ import type { BrowserCriClient } from './browser-cri-client'
import type { Automation } from '../automation'
import { getCtx } from '@packages/data-context'
import { getError } from '@packages/errors'
import type { BrowserLaunchOpts, BrowserNewTabOpts } from '@packages/types'

const debug = Debug('cypress:server:browsers:firefox')

Expand Down Expand Up @@ -371,15 +371,15 @@ export function _createDetachedInstance (browserInstance: BrowserInstance, brows
return detachedInstance
}

export async function connectToNewSpec (browser: Browser, options: any = {}, automation: Automation) {
export async function connectToNewSpec (browser: Browser, options: BrowserNewTabOpts, automation: Automation) {
await firefoxUtil.connectToNewSpec(options, automation, browserCriClient)
}

export function connectToExisting () {
getCtx().onWarning(getError('UNEXPECTED_INTERNAL_ERROR', new Error('Attempting to connect to existing browser for Cypress in Cypress which is not yet implemented for firefox')))
}

export async function open (browser: Browser, url, options: any = {}, automation): Promise<BrowserInstance> {
export async function open (browser: Browser, url: string, options: BrowserLaunchOpts, automation: Automation): Promise<BrowserInstance> {
// see revision comment here https://wiki.mozilla.org/index.php?title=WebDriver/RemoteProtocol&oldid=1234946
const hasCdp = browser.majorVersion >= 86
const defaultLaunchOptions = utils.getDefaultLaunchOptions({
Expand Down Expand Up @@ -441,7 +441,7 @@ export async function open (browser: Browser, url, options: any = {}, automation
const [
foxdriverPort,
marionettePort,
] = await Bluebird.all([getPort(), getPort()])
] = await Promise.all([getPort(), getPort()])

defaultLaunchOptions.preferences['devtools.debugger.remote-port'] = foxdriverPort
defaultLaunchOptions.preferences['marionette.port'] = marionettePort
Expand All @@ -452,7 +452,7 @@ export async function open (browser: Browser, url, options: any = {}, automation
cacheDir,
extensionDest,
launchOptions,
] = await Bluebird.all([
] = await Promise.all([
utils.ensureCleanCache(browser, options.isTextTerminal),
utils.writeExtension(browser, options.isTextTerminal, options.proxyUrl, options.socketIoRoute),
utils.executeBeforeBrowserLaunch(browser, defaultLaunchOptions, options),
Expand Down
Loading

0 comments on commit d25b9e0

Please sign in to comment.