diff --git a/packages/resolve-dist/lib/index.ts b/packages/resolve-dist/lib/index.ts index 32840ce388..809e42773b 100644 --- a/packages/resolve-dist/lib/index.ts +++ b/packages/resolve-dist/lib/index.ts @@ -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` } diff --git a/packages/server/lib/browsers/cdp_automation.ts b/packages/server/lib/browsers/cdp_automation.ts index df00b00d05..4d47aa459d 100644 --- a/packages/server/lib/browsers/cdp_automation.ts +++ b/packages/server/lib/browsers/cdp_automation.ts @@ -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 +type SendDebuggerCommand = (message: T, data?: any) => Promise type SendCloseCommand = (shouldKeepTabOpen: boolean) => Promise | void -type OnFn = (eventName: CdpEvent, cb: Function) => void +type OnFn = (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 { const cdpAutomation = new CdpAutomation(sendDebuggerCommandFn, onFn, sendCloseCommandFn, automation, experimentalSessionAndOrigin) diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index c2500b1ee2..2a70a0b941 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -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, 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), ]) diff --git a/packages/server/lib/browsers/electron.ts b/packages/server/lib/browsers/electron.ts index 5e022761a5..7f3eba8173 100644 --- a/packages/server/lib/browsers/electron.ts +++ b/packages/server/lib/browsers/electron.ts @@ -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 diff --git a/packages/server/lib/browsers/firefox.ts b/packages/server/lib/browsers/firefox.ts index cb454643c8..4ebcd132f3 100644 --- a/packages/server/lib/browsers/firefox.ts +++ b/packages/server/lib/browsers/firefox.ts @@ -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 { +export async function open (browser: Browser, url: string, options: BrowserLaunchOpts, automation: Automation): Promise { // 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), diff --git a/packages/server/lib/browsers/utils.ts b/packages/server/lib/browsers/utils.ts index 7c9062534d..6fea932747 100644 --- a/packages/server/lib/browsers/utils.ts +++ b/packages/server/lib/browsers/utils.ts @@ -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', )) diff --git a/packages/server/lib/gui/windows.ts b/packages/server/lib/gui/windows.ts index 5e3a11a928..66695fb525 100644 --- a/packages/server/lib/gui/windows.ts +++ b/packages/server/lib/gui/windows.ts @@ -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 + 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 { +export async function open (projectRoot: string, options: WindowOpenOptions, newBrowserWindow = _newBrowserWindow): Promise { // 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() } diff --git a/packages/server/lib/modes/interactive.ts b/packages/server/lib/modes/interactive.ts index 5796f20239..72e0a8c2b9 100644 --- a/packages/server/lib/modes/interactive.ts +++ b/packages/server/lib/modes/interactive.ts @@ -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) { diff --git a/packages/server/lib/modes/run.ts b/packages/server/lib/modes/run.ts index 7ec8c54a7c..cb00eb5c3f 100644 --- a/packages/server/lib/modes/run.ts +++ b/packages/server/lib/modes/run.ts @@ -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 diff --git a/packages/server/lib/open_project.ts b/packages/server/lib/open_project.ts index e249a09463..c1f4175e42 100644 --- a/packages/server/lib/open_project.ts +++ b/packages/server/lib/open_project.ts @@ -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, diff --git a/packages/server/lib/saved_state.ts b/packages/server/lib/saved_state.ts index 3ee675e2c4..47fbaa7ff4 100644 --- a/packages/server/lib/saved_state.ts +++ b/packages/server/lib/saved_state.ts @@ -13,8 +13,6 @@ const debug = Debug('cypress:server:saved_state') const stateFiles: Record = {} -// 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()) diff --git a/packages/server/lib/video_capture.ts b/packages/server/lib/video_capture.ts index 9d25331978..0bdad7cfbe 100644 --- a/packages/server/lib/video_capture.ts +++ b/packages/server/lib/video_capture.ts @@ -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') diff --git a/packages/server/test/integration/cypress_spec.js b/packages/server/test/integration/cypress_spec.js index caa4fa28d6..55b957073d 100644 --- a/packages/server/test/integration/cypress_spec.js +++ b/packages/server/test/integration/cypress_spec.js @@ -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) diff --git a/packages/server/test/unit/browsers/chrome_spec.js b/packages/server/test/unit/browsers/chrome_spec.js index 75352873ed..7f3faafb32 100644 --- a/packages/server/test/unit/browsers/chrome_spec.js +++ b/packages/server/test/unit/browsers/chrome_spec.js @@ -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 diff --git a/packages/server/test/unit/browsers/electron_spec.js b/packages/server/test/unit/browsers/electron_spec.js index 96f152d61f..194bd1fecd 100644 --- a/packages/server/test/unit/browsers/electron_spec.js +++ b/packages/server/test/unit/browsers/electron_spec.js @@ -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 }) diff --git a/packages/server/test/unit/gui/windows_spec.ts b/packages/server/test/unit/gui/windows_spec.ts index ad7dee54b9..4f14683ce7 100644 --- a/packages/server/test/unit/gui/windows_spec.ts +++ b/packages/server/test/unit/gui/windows_spec.ts @@ -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') }) }) }) diff --git a/packages/server/test/unit/modes/interactive_spec.js b/packages/server/test/unit/modes/interactive_spec.js index 826e5974cc..2c6967e71f 100644 --- a/packages/server/test/unit/modes/interactive_spec.js +++ b/packages/server/test/unit/modes/interactive_spec.js @@ -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 }) diff --git a/packages/types/src/server.ts b/packages/types/src/server.ts index fae60c3fae..ce1540bb09 100644 --- a/packages/types/src/server.ts +++ b/packages/types/src/server.ts @@ -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 // TODO: remove the `Partial` here by making it impossible for openProject.launch to be called w/o OpenProjectLaunchOpts -& Pick +& Pick export type BrowserNewTabOpts = { onInitializeNewBrowserTab: () => void } & BrowserLaunchOpts