refactor: move more of video capture into browser automations (#23587)

This commit is contained in:
Zach Bloomquist
2022-08-31 14:55:28 -04:00
committed by GitHub
parent c8b1b30323
commit bcd7548d74
18 changed files with 183 additions and 258 deletions

View File

@@ -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`
}

View File

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

View File

@@ -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')
@@ -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
@@ -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 = {
@@ -448,7 +438,7 @@ export = {
_removeRootExtension,
_maybeRecordVideo,
_recordVideo,
_navigateUsingCRI,
@@ -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')
@@ -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 })
@@ -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),
])

View File

@@ -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')
@@ -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)
@@ -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: {
@@ -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 })
}
},
@@ -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) {
@@ -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) {
@@ -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),
])
@@ -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)
const electronOptions: ElectronOpts = Windows.defaults(
this._defaultOptions(options.projectRoot, state, options, automation),
)
// get the GUI window defaults now
options = Windows.defaults(options)
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
@@ -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

View File

@@ -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'
@@ -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')
@@ -371,7 +371,7 @@ 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)
}
@@ -379,7 +379,7 @@ 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({
@@ -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
@@ -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),

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-redeclare */
import Bluebird from 'bluebird'
import _ from 'lodash'
import type { FoundBrowser } from '@packages/types'
import type { BrowserLaunchOpts, FoundBrowser } from '@packages/types'
import * as errors from '../errors'
import * as plugins from '../plugins'
import { getError } from '@packages/errors'
@@ -132,13 +132,14 @@ async function executeBeforeBrowserLaunch (browser, launchOptions: typeof defaul
return launchOptions
}
function extendLaunchOptionsFromPlugins (launchOptions, pluginConfigResult, options) {
function extendLaunchOptionsFromPlugins (launchOptions, pluginConfigResult, options: BrowserLaunchOpts) {
// if we returned an array from the plugin
// then we know the user is using the deprecated
// interface and we need to warn them
// TODO: remove this logic in >= v5.0.0
if (pluginConfigResult[0]) {
options.onWarning(getError(
// eslint-disable-next-line no-console
(options.onWarning || console.warn)(getError(
'DEPRECATED_BEFORE_BROWSER_LAUNCH_ARGS',
))

View File

@@ -3,34 +3,36 @@ import Bluebird from 'bluebird'
import { BrowserWindow } from 'electron'
import Debug from 'debug'
import * as savedState from '../saved_state'
import { getPathToDesktopIndex } from '@packages/resolve-dist'
const debug = Debug('cypress:server:windows')
export type WindowOptions = Electron.BrowserWindowConstructorOptions & {
type?: 'INDEX'
url?: string
devTools?: boolean
graphqlPort?: number
contextMenu?: boolean
partition?: string
/**
* Synchronizes properties of browserwindow with local state
*/
trackState?: TrackStateMap
onFocus?: () => void
onNewWindow?: (e, url, frameName, disposition, options) => Promise<void>
onCrashed?: () => void
}
export type WindowOpenOptions = WindowOptions & { url: string }
type TrackStateMap = Record<'width' | 'height' | 'x' | 'y' | 'devTools', string>
let windows = {}
let recentlyCreatedWindow = false
const getUrl = function (type, port: number) {
switch (type) {
case 'INDEX':
return getPathToDesktopIndex(port)
default:
throw new Error(`No acceptable window type found for: '${type}'`)
}
}
const getByType = (type) => {
const getByType = (type: string) => {
return windows[type]
}
const setWindowProxy = function (win) {
const setWindowProxy = function (win: BrowserWindow) {
if (!process.env.HTTP_PROXY) {
return
}
@@ -41,7 +43,7 @@ const setWindowProxy = function (win) {
})
}
export function installExtension (win: BrowserWindow, path) {
export function installExtension (win: BrowserWindow, path: string) {
return win.webContents.session.loadExtension(path)
.then((data) => {
debug('electron extension installed %o', { data, path })
@@ -70,7 +72,7 @@ export function reset () {
windows = {}
}
export function destroy (type) {
export function destroy (type: string) {
let win
if (type && (win = getByType(type))) {
@@ -78,7 +80,7 @@ export function destroy (type) {
}
}
export function get (type) {
export function get (type: string) {
return getByType(type) || (() => {
throw new Error(`No window exists for: '${type}'`)
})()
@@ -143,7 +145,7 @@ export function defaults (options = {}) {
})
}
export function create (projectRoot, _options: WindowOptions = {}, newBrowserWindow = _newBrowserWindow) {
export function create (projectRoot, _options: WindowOptions, newBrowserWindow = _newBrowserWindow) {
const options = defaults(_options)
if (options.show === false) {
@@ -213,15 +215,15 @@ export function create (projectRoot, _options: WindowOptions = {}, newBrowserWin
}
// open launchpad BrowserWindow
export function open (projectRoot, launchpadPort: number, options: WindowOptions = {}, newBrowserWindow = _newBrowserWindow): Bluebird<BrowserWindow> {
export async function open (projectRoot: string, options: WindowOpenOptions, newBrowserWindow = _newBrowserWindow): Promise<BrowserWindow> {
// if we already have a window open based
// on that type then just show + focus it!
let win = getByType(options.type)
const knownWin = options.type && getByType(options.type)
if (win) {
win.show()
if (knownWin) {
knownWin.show()
return Bluebird.resolve(win)
return Bluebird.resolve(knownWin)
}
recentlyCreatedWindow = true
@@ -235,11 +237,7 @@ export function open (projectRoot, launchpadPort: number, options: WindowOptions
},
})
if (!options.url) {
options.url = getUrl(options.type, launchpadPort)
}
win = create(projectRoot, options, newBrowserWindow)
const win = create(projectRoot, options, newBrowserWindow)
debug('creating electron window with options %o', options)
@@ -251,21 +249,15 @@ export function open (projectRoot, launchpadPort: number, options: WindowOptions
})
}
// enable our url to be a promise
// and wait for this to be resolved
return Bluebird.join(
options.url,
setWindowProxy(win),
)
.spread((url) => {
// navigate the window here!
win.loadURL(url)
await setWindowProxy(win)
await win.loadURL(options.url)
recentlyCreatedWindow = false
}).thenReturn(win)
recentlyCreatedWindow = false
return win
}
export function trackState (projectRoot, isTextTerminal, win, keys) {
export function trackState (projectRoot, isTextTerminal, win, keys: TrackStateMap) {
const isDestroyed = () => {
return win.isDestroyed()
}

View File

@@ -11,9 +11,10 @@ import { globalPubSub, getCtx, clearCtx } from '@packages/data-context'
// eslint-disable-next-line no-duplicate-imports
import type { WebContents } from 'electron'
import type { LaunchArgs } from '@packages/types'
import type { LaunchArgs, Preferences } from '@packages/types'
import debugLib from 'debug'
import { getPathToDesktopIndex } from '@packages/resolve-dist'
const debug = debugLib('cypress:server:interactive')
@@ -26,7 +27,7 @@ export = {
return os.platform() === 'darwin'
},
getWindowArgs (state) {
getWindowArgs (url: string, state: Preferences) {
// Electron Window's arguments
// These options are passed to Electron's BrowserWindow
const minWidth = Math.round(/* 13" MacBook Air */ 1792 / 3) // Thirds
@@ -46,6 +47,7 @@ export = {
}
const common = {
url,
// The backgroundColor should match the value we will show in the
// launchpad frontend.
@@ -129,16 +131,10 @@ export = {
return args[os.platform()]
},
/**
* @param {import('@packages/types').LaunchArgs} options
* @returns
*/
ready (options: {projectRoot?: string} = {}, port: number) {
async ready (options: LaunchArgs, launchpadPort: number) {
const { projectRoot } = options
const ctx = getCtx()
// TODO: potentially just pass an event emitter
// instance here instead of callback functions
menu.set({
withInternalDevTools: isDev(),
onLogOutClicked () {
@@ -149,15 +145,14 @@ export = {
},
})
return savedState.create(projectRoot, false).then((state) => state.get())
.then((state) => {
return Windows.open(projectRoot, port, this.getWindowArgs(state))
.then((win) => {
ctx?.actions.electron.setBrowserWindow(win)
const State = await savedState.create(projectRoot, false)
const state = await State.get()
const url = getPathToDesktopIndex(launchpadPort)
const win = await Windows.open(projectRoot, this.getWindowArgs(url, state))
return win
})
})
ctx?.actions.electron.setBrowserWindow(win)
return win
},
async run (options: LaunchArgs, _loading: Promise<void>) {

View File

@@ -1,6 +1,5 @@
/* eslint-disable no-console, @cypress/dev/arrow-body-multiline-braces */
import _ from 'lodash'
import la from 'lazy-ass'
import pkg from '@packages/root'
import path from 'path'
import chalk from 'chalk'
@@ -22,7 +21,7 @@ import random from '../util/random'
import system from '../util/system'
import chromePolicyCheck from '../util/chrome_policy_check'
import * as objUtils from '../util/obj_utils'
import type { SpecWithRelativeRoot, SpecFile, TestingType, OpenProjectLaunchOpts, FoundBrowser } from '@packages/types'
import type { SpecWithRelativeRoot, SpecFile, TestingType, OpenProjectLaunchOpts, FoundBrowser, WriteVideoFrame } from '@packages/types'
import type { Cfg } from '../project-base'
import type { Browser } from '../browsers/types'
import * as printResults from '../util/print-run'
@@ -41,7 +40,7 @@ let exitEarly = (err) => {
earlyExitErr = err
}
let earlyExitErr: Error
let currentWriteVideoFrameCallback: videoCapture.WriteVideoFrame
let currentWriteVideoFrameCallback: WriteVideoFrame
let currentSetScreenshotMetadata: SetScreenshotMetadata
const debug = Debug('cypress:server:run')
@@ -121,70 +120,6 @@ async function getProjectId (project, id) {
}
}
const getDefaultBrowserOptsByFamily = (browser, project, writeVideoFrame, onError) => {
la(browserUtils.isBrowserFamily(browser.family), 'invalid browser family in', browser)
if (browser.name === 'electron') {
return getElectronProps(browser.isHeaded, writeVideoFrame, onError)
}
if (browser.family === 'chromium') {
return getCdpVideoProp(writeVideoFrame)
}
if (browser.family === 'firefox') {
return getFirefoxProps(project, writeVideoFrame)
}
return {}
}
const getFirefoxProps = (project, writeVideoFrame) => {
if (writeVideoFrame) {
project.on('capture:video:frames', writeVideoFrame)
return { onScreencastFrame: true }
}
return {}
}
const getCdpVideoProp = (writeVideoFrame) => {
if (!writeVideoFrame) {
return {}
}
return {
onScreencastFrame: (e) => {
// https://chromedevtools.github.io/devtools-protocol/tot/Page#event-screencastFrame
writeVideoFrame(Buffer.from(e.data, 'base64'))
},
}
}
const getElectronProps = (isHeaded, writeVideoFrame, onError) => {
return {
...getCdpVideoProp(writeVideoFrame),
width: 1280,
height: 720,
show: isHeaded,
onCrashed () {
const err = errors.get('RENDERER_CRASHED')
errors.log(err)
onError(err)
},
onNewWindow (e, url, frameName, disposition, options) {
// 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
options.show = false
},
}
}
const sumByProp = (runs, prop) => {
return _.sumBy(runs, prop) || 0
}
@@ -380,15 +315,20 @@ async function postProcessRecording (name, cname, videoCompression, shouldUpload
return continueProcessing(onProgress)
}
function launchBrowser (options: { browser: Browser, spec: SpecWithRelativeRoot, writeVideoFrame?: videoCapture.WriteVideoFrame, setScreenshotMetadata: SetScreenshotMetadata, project: Project, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, onError: (err: Error) => void }) {
const { browser, spec, writeVideoFrame, setScreenshotMetadata, project, screenshots, projectRoot, shouldLaunchNewTab, onError } = options
function launchBrowser (options: { browser: Browser, spec: SpecWithRelativeRoot, writeVideoFrame?: WriteVideoFrame, setScreenshotMetadata: SetScreenshotMetadata, project: Project, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, onError: (err: Error) => void }) {
const { browser, spec, setScreenshotMetadata, project, screenshots, projectRoot, shouldLaunchNewTab, onError } = options
const warnings = {}
if (options.writeVideoFrame && browser.family === 'firefox') {
project.on('capture:video:frames', options.writeVideoFrame)
}
const browserOpts: OpenProjectLaunchOpts = {
...getDefaultBrowserOptsByFamily(browser, project, writeVideoFrame, onError),
projectRoot,
shouldLaunchNewTab,
onError,
writeVideoFrame: options.writeVideoFrame,
automationMiddleware: {
onBeforeRequest (message, data) {
if (message === 'take:screenshot') {
@@ -491,7 +431,7 @@ function writeVideoFrameCallback (data: Buffer) {
return currentWriteVideoFrameCallback(data)
}
function waitForBrowserToConnect (options: { project: Project, socketId: string, onError: (err: Error) => void, writeVideoFrame?: videoCapture.WriteVideoFrame, spec: SpecWithRelativeRoot, isFirstSpec: boolean, testingType: string, experimentalSingleTabRunMode: boolean, browser: Browser, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, webSecurity: boolean }) {
function waitForBrowserToConnect (options: { project: Project, socketId: string, onError: (err: Error) => void, writeVideoFrame?: WriteVideoFrame, spec: SpecWithRelativeRoot, isFirstSpec: boolean, testingType: string, experimentalSingleTabRunMode: boolean, browser: Browser, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, webSecurity: boolean }) {
if (globalThis.CY_TEST_MOCK?.waitForBrowserToConnect) return Promise.resolve()
const { project, socketId, onError, writeVideoFrame, spec } = options

View File

@@ -84,7 +84,7 @@ export class OpenProject {
proxyServer: cfg.proxyServer,
socketIoRoute: cfg.socketIoRoute,
chromeWebSecurity: cfg.chromeWebSecurity,
isTextTerminal: cfg.isTextTerminal,
isTextTerminal: !!cfg.isTextTerminal,
downloadsFolder: cfg.downloadsFolder,
experimentalSessionAndOrigin: cfg.experimentalSessionAndOrigin,
experimentalModifyObstructiveThirdPartyCode: cfg.experimentalModifyObstructiveThirdPartyCode,

View File

@@ -13,8 +13,6 @@ const debug = Debug('cypress:server:saved_state')
const stateFiles: Record<string, typeof FileUtil> = {}
// TODO: remove `showedOnBoardingModal` from this list - it is only included so that misleading `allowed` are not thrown
// now that it has been removed from use
export const formStatePath = (projectRoot?: string) => {
return Bluebird.try(() => {
debug('making saved state from %s', cwd())

View File

@@ -7,8 +7,7 @@ import Bluebird from 'bluebird'
import { path as ffmpegPath } from '@ffmpeg-installer/ffmpeg'
import BlackHoleStream from 'black-hole-stream'
import { fs } from './util/fs'
export type WriteVideoFrame = (data: Buffer) => void
import type { WriteVideoFrame } from '@packages/types'
const debug = Debug('cypress:server:video')
const debugVerbose = Debug('cypress-verbose:server:video')

View File

@@ -510,7 +510,9 @@ describe('lib/cypress', () => {
.then(() => {
expect(browsers.open).to.be.calledWithMatch(ELECTRON_BROWSER, {
proxyServer: 'http://localhost:8888',
show: true,
browser: {
isHeadless: false,
},
})
this.expectExitWith(0)
@@ -1022,7 +1024,7 @@ describe('lib/cypress', () => {
browser: 'electron',
foo: 'bar',
onNewWindow: sinon.match.func,
onScreencastFrame: sinon.match.func,
writeVideoFrame: sinon.match.func,
})
this.expectExitWith(0)

View File

@@ -325,14 +325,14 @@ describe('lib/browsers/chrome', () => {
// https://github.com/cypress-io/cypress/issues/9265
it('respond ACK after receiving new screenshot frame', function () {
const frameMeta = { data: Buffer.from(''), sessionId: '1' }
const frameMeta = { data: Buffer.from('foo'), sessionId: '1' }
const write = sinon.stub()
const options = { onScreencastFrame: write }
const options = { writeVideoFrame: write }
return this.onCriEvent('Page.screencastFrame', frameMeta, options)
.then(() => {
expect(this.pageCriClient.send).to.have.been.calledWith('Page.startScreencast')
expect(write).to.have.been.calledWith(frameMeta)
expect(write).to.have.been.calledWithMatch((arg) => Buffer.isBuffer(arg) && arg.length > 0)
expect(this.pageCriClient.send).to.have.been.calledWith('Page.screencastFrameAck', { sessionId: frameMeta.sessionId })
})
})
@@ -516,12 +516,18 @@ describe('lib/browsers/chrome', () => {
}
let onInitializeNewBrowserTabCalled = false
const options = { ...openOpts, url: 'https://www.google.com', downloadsFolder: '/tmp/folder', onInitializeNewBrowserTab: () => {
onInitializeNewBrowserTabCalled = true
} }
const options = {
...openOpts,
url: 'https://www.google.com',
downloadsFolder: '/tmp/folder',
writeVideoFrame: () => {},
onInitializeNewBrowserTab: () => {
onInitializeNewBrowserTabCalled = true
},
}
sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient)
sinon.stub(chrome, '_maybeRecordVideo').withArgs(pageCriClient, options, 354).resolves()
sinon.stub(chrome, '_recordVideo').withArgs(sinon.match.object, options.writeVideoFrame, 354).resolves()
sinon.stub(chrome, '_navigateUsingCRI').withArgs(pageCriClient, options.url, 354).resolves()
sinon.stub(chrome, '_handleDownloads').withArgs(pageCriClient, options.downloadFolder, automation).resolves()
@@ -529,7 +535,7 @@ describe('lib/browsers/chrome', () => {
expect(automation.use).to.be.called
expect(chrome._getBrowserCriClient).to.be.called
expect(chrome._maybeRecordVideo).to.be.called
expect(chrome._recordVideo).to.be.called
expect(chrome._navigateUsingCRI).to.be.called
expect(chrome._handleDownloads).to.be.called
expect(onInitializeNewBrowserTabCalled).to.be.true

View File

@@ -690,15 +690,16 @@ describe('lib/browsers/electron', () => {
})
it('.onFocus', function () {
let opts = electron._defaultOptions('/foo', this.state, { show: true, browser: {} })
const headlessOpts = electron._defaultOptions('/foo', this.state, { browser: { isHeadless: false } })
opts.onFocus()
headlessOpts.onFocus()
expect(menu.set).to.be.calledWith({ withInternalDevTools: true })
menu.set.reset()
opts = electron._defaultOptions('/foo', this.state, { show: false, browser: {} })
opts.onFocus()
const headedOpts = electron._defaultOptions('/foo', this.state, { browser: { isHeadless: true } })
headedOpts.onFocus()
expect(menu.set).not.to.be.called
})

View File

@@ -9,7 +9,6 @@ import { EventEmitter } from 'events'
import { BrowserWindow } from 'electron'
import * as Windows from '../../../lib/gui/windows'
import * as savedState from '../../../lib/saved_state'
import { getPathToDesktopIndex } from '@packages/resolve-dist'
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Cypress/0.0.0 Chrome/59.0.3071.115 Electron/1.8.2 Safari/537.36'
@@ -42,23 +41,21 @@ describe('lib/gui/windows', () => {
context('.open', () => {
it('sets default options', function () {
const options: Windows.WindowOptions = {
const options: Windows.WindowOpenOptions = {
type: 'INDEX',
url: 'foo',
}
return Windows.open('/path/to/project', 1234, options, () => this.win)
return Windows.open('/path/to/project', options, () => this.win)
.then((win) => {
expect(options).to.include({
height: 500,
width: 600,
type: 'INDEX',
show: true,
url: getPathToDesktopIndex(1234),
})
expect(win.loadURL).to.be.calledWith(getPathToDesktopIndex(
1234,
))
expect(win.loadURL).to.be.calledWith('foo')
})
})
})

View File

@@ -27,13 +27,13 @@ describe('gui/interactive', () => {
context('.getWindowArgs', () => {
it('quits app when onClose is called', () => {
electron.app.quit = sinon.stub()
interactiveMode.getWindowArgs({}).onClose()
interactiveMode.getWindowArgs('http://app', {}).onClose()
expect(electron.app.quit).to.be.called
})
it('tracks state properties', () => {
const { trackState } = interactiveMode.getWindowArgs({})
const { trackState } = interactiveMode.getWindowArgs('http://app', {})
const args = _.pick(trackState, 'width', 'height', 'x', 'y', 'devTools')
@@ -51,49 +51,49 @@ describe('gui/interactive', () => {
// Use the saved value if it's valid
describe('when no dimension', () => {
it('renders with preferred width if no width saved', () => {
expect(interactiveMode.getWindowArgs({}).width).to.equal(1200)
expect(interactiveMode.getWindowArgs('http://app', {}).width).to.equal(1200)
})
it('renders with preferred height if no height saved', () => {
expect(interactiveMode.getWindowArgs({}).height).to.equal(800)
expect(interactiveMode.getWindowArgs('http://app', {}).height).to.equal(800)
})
})
describe('when saved dimension is too small', () => {
it('uses the preferred width', () => {
expect(interactiveMode.getWindowArgs({ appWidth: 1 }).width).to.equal(1200)
expect(interactiveMode.getWindowArgs('http://app', { appWidth: 1 }).width).to.equal(1200)
})
it('uses the preferred height', () => {
expect(interactiveMode.getWindowArgs({ appHeight: 1 }).height).to.equal(800)
expect(interactiveMode.getWindowArgs('http://app', { appHeight: 1 }).height).to.equal(800)
})
})
describe('when saved dimension is within min/max dimension', () => {
it('uses the saved width', () => {
expect(interactiveMode.getWindowArgs({ appWidth: 1500 }).width).to.equal(1500)
expect(interactiveMode.getWindowArgs('http://app', { appWidth: 1500 }).width).to.equal(1500)
})
it('uses the saved height', () => {
expect(interactiveMode.getWindowArgs({ appHeight: 1500 }).height).to.equal(1500)
expect(interactiveMode.getWindowArgs('http://app', { appHeight: 1500 }).height).to.equal(1500)
})
})
})
it('renders with saved x if it exists', () => {
expect(interactiveMode.getWindowArgs({ appX: 3 }).x).to.equal(3)
expect(interactiveMode.getWindowArgs('http://app', { appX: 3 }).x).to.equal(3)
})
it('renders with no x if no x saved', () => {
expect(interactiveMode.getWindowArgs({}).x).to.be.undefined
expect(interactiveMode.getWindowArgs('http://app', {}).x).to.be.undefined
})
it('renders with saved y if it exists', () => {
expect(interactiveMode.getWindowArgs({ appY: 4 }).y).to.equal(4)
expect(interactiveMode.getWindowArgs('http://app', { appY: 4 }).y).to.equal(4)
})
it('renders with no y if no y saved', () => {
expect(interactiveMode.getWindowArgs({}).y).to.be.undefined
expect(interactiveMode.getWindowArgs('http://app', {}).y).to.be.undefined
})
describe('on window focus', () => {
@@ -105,7 +105,7 @@ describe('gui/interactive', () => {
const env = process.env['CYPRESS_INTERNAL_ENV']
process.env['CYPRESS_INTERNAL_ENV'] = 'development'
interactiveMode.getWindowArgs({}).onFocus()
interactiveMode.getWindowArgs('http://app', {}).onFocus()
expect(menu.set.lastCall.args[0].withInternalDevTools).to.be.true
process.env['CYPRESS_INTERNAL_ENV'] = env
})
@@ -114,7 +114,7 @@ describe('gui/interactive', () => {
const env = process.env['CYPRESS_INTERNAL_ENV']
process.env['CYPRESS_INTERNAL_ENV'] = 'production'
interactiveMode.getWindowArgs({}).onFocus()
interactiveMode.getWindowArgs('http://app', {}).onFocus()
expect(menu.set.lastCall.args[0].withInternalDevTools).to.be.false
process.env['CYPRESS_INTERNAL_ENV'] = env
})

View File

@@ -2,23 +2,27 @@ import type { FoundBrowser } from './browser'
import type { ReceivedCypressOptions } from './config'
import type { PlatformName } from './platform'
export type WriteVideoFrame = (data: Buffer) => void
export type OpenProjectLaunchOpts = {
projectRoot: string
shouldLaunchNewTab: boolean
automationMiddleware: AutomationMiddleware
writeVideoFrame?: WriteVideoFrame
onWarning: (err: Error) => void
onError: (err: Error) => void
}
export type BrowserLaunchOpts = {
browsers: FoundBrowser[]
browser: FoundBrowser
browser: FoundBrowser & { isHeadless: boolean }
url: string | undefined
proxyServer: string
isTextTerminal: boolean
onBrowserClose?: (...args: unknown[]) => void
onBrowserOpen?: (...args: unknown[]) => void
onError?: (err: Error) => void
} & Partial<OpenProjectLaunchOpts> // TODO: remove the `Partial` here by making it impossible for openProject.launch to be called w/o OpenProjectLaunchOpts
& Pick<ReceivedCypressOptions, 'userAgent' | 'proxyUrl' | 'socketIoRoute' | 'chromeWebSecurity' | 'isTextTerminal' | 'downloadsFolder' | 'experimentalSessionAndOrigin' | 'experimentalModifyObstructiveThirdPartyCode'>
& Pick<ReceivedCypressOptions, 'userAgent' | 'proxyUrl' | 'socketIoRoute' | 'chromeWebSecurity' | 'downloadsFolder' | 'experimentalSessionAndOrigin' | 'experimentalModifyObstructiveThirdPartyCode'>
export type BrowserNewTabOpts = { onInitializeNewBrowserTab: () => void } & BrowserLaunchOpts