Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(server): convert remaining browsers code to typescript #23556

Merged
merged 13 commits into from
Aug 29, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout:
expect(ctx.actions.browser.setActiveBrowserById).to.have.been.calledWith(browserId)
expect(genId).to.eql('firefox-firefox-stable')
expect(ctx.actions.project.launchProject).to.have.been.calledWith(
ctx.coreData.currentTestingType, {}, o.sinon.match(new RegExp('cypress\-in\-cypress\/src\/TestComponent\.spec\.jsx$')),
ctx.coreData.currentTestingType, undefined, o.sinon.match(new RegExp('cypress\-in\-cypress\/src\/TestComponent\.spec\.jsx$')),
)
})
})
Expand Down
2 changes: 1 addition & 1 deletion packages/app/cypress/e2e/top-nav.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe('App Top Nav Workflows', () => {
expect(ctx.actions.browser.setActiveBrowserById).to.have.been.calledWith(browserId)
expect(genId).to.eql('edge-chromium-stable')
expect(ctx.actions.project.launchProject).to.have.been.calledWith(
ctx.coreData.currentTestingType, {}, undefined,
ctx.coreData.currentTestingType, undefined, undefined,
)
})
})
Expand Down
8 changes: 4 additions & 4 deletions packages/data-context/src/actions/ProjectActions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CodeGenType, MutationSetProjectPreferencesInGlobalCacheArgs, NexusGenObjects, NexusGenUnions } from '@packages/graphql/src/gen/nxs.gen'
import type { InitializeProjectOptions, FoundBrowser, FoundSpec, LaunchOpts, OpenProjectLaunchOptions, Preferences, TestingType, ReceivedCypressOptions, AddProject, FullConfig, AllowedState, SpecWithRelativeRoot } from '@packages/types'
import type { InitializeProjectOptions, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, Preferences, TestingType, ReceivedCypressOptions, AddProject, FullConfig, AllowedState, SpecWithRelativeRoot, OpenProjectLaunchOpts } from '@packages/types'
import type { EventEmitter } from 'events'
import execa from 'execa'
import path from 'path'
Expand All @@ -22,7 +22,7 @@ export interface ProjectApiShape {
* order for CT to startup
*/
openProjectCreate(args: InitializeProjectOptions, options: OpenProjectLaunchOptions): Promise<unknown>
launchProject(browser: FoundBrowser, spec: Cypress.Spec, options: LaunchOpts): Promise<void>
launchProject(browser: FoundBrowser, spec: Cypress.Spec, options?: OpenProjectLaunchOpts): Promise<void>
insertProjectToCache(projectRoot: string): Promise<void>
removeProjectFromCache(projectRoot: string): Promise<void>
getProjectRootsFromCache(): Promise<ProjectShape[]>
Expand Down Expand Up @@ -175,7 +175,7 @@ export class ProjectActions {
// When switching testing type, the project should be relaunched in the previously selected browser
if (this.ctx.coreData.app.relaunchBrowser) {
this.ctx.project.setRelaunchBrowser(false)
await this.ctx.actions.project.launchProject(this.ctx.coreData.currentTestingType, {})
await this.ctx.actions.project.launchProject(this.ctx.coreData.currentTestingType)
}
})
} catch (e) {
Expand Down Expand Up @@ -228,7 +228,7 @@ export class ProjectActions {
}
}

async launchProject (testingType: Cypress.TestingType | null, options: LaunchOpts, specPath?: string | null) {
async launchProject (testingType: Cypress.TestingType | null, options?: OpenProjectLaunchOpts, specPath?: string | null) {
if (!this.ctx.currentProject) {
return null
}
Expand Down
2 changes: 1 addition & 1 deletion packages/data-context/src/data/ProjectLifecycleManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ export class ProjectLifecycleManager {
if (this.ctx.coreData.activeBrowser) {
// if `cypress open` was launched with a `--project` and `--testingType`, go ahead and launch the `--browser`
if (this.ctx.modeOptions.project && this.ctx.modeOptions.testingType) {
await this.ctx.actions.project.launchProject(this.ctx.coreData.currentTestingType, {})
await this.ctx.actions.project.launchProject(this.ctx.coreData.currentTestingType)
}

return
Expand Down
1 change: 1 addition & 0 deletions packages/frontend-shared/cypress/e2e/support/e2eSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ function startAppServer (mode: 'component' | 'e2e' = 'e2e', options: { skipMocki
if (!ctx.lifecycleManager.browsers?.length) throw new Error('No browsers available in startAppServer')

await ctx.actions.browser.setActiveBrowser(ctx.lifecycleManager.browsers[0])
// @ts-expect-error this interface is strict about the options it expects
await ctx.actions.project.launchProject(o.mode, { url: o.url })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the interface is

export interface LaunchOpts {
  browser?: FoundBrowser
  url?: string
  automationMiddleware?: AutomationMiddleware
  projectRoot?: string
  shouldLaunchNewTab?: boolean
  onBrowserClose?: (...args: unknown[]) => void
  onBrowserOpen?: (...args: unknown[]) => void
  onError?: 
}

I wonder why we need the ts-expect-error? What is the error?

Copy link
Contributor Author

@flotwig flotwig Aug 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only OpenProjectLaunchOpts at this point, which doesn't have url. This is an area that should be refactored, we're putting a lot of functionality into project.launchProject that makes it difficult to reason with (and difficult to sanely type)


if (!o.skipMockingPrompts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export const mutation = mutationType({
specPath: stringArg(),
},
resolve: async (_, args, ctx) => {
await ctx.actions.project.launchProject(ctx.coreData.currentTestingType, {}, args.specPath)
await ctx.actions.project.launchProject(ctx.coreData.currentTestingType, undefined, args.specPath)

return ctx.lifecycleManager
},
Expand Down
10 changes: 5 additions & 5 deletions packages/server/lib/browsers/browser-cri-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import CRI from 'chrome-remote-interface'
import Debug from 'debug'
import { _connectAsync, _getDelayMsForRetry } from './protocol'
import * as errors from '../errors'
import { create, CRIWrapper } from './cri-client'
import { create, CriClient } from './cri-client'

const HOST = '127.0.0.1'

Expand Down Expand Up @@ -67,8 +67,8 @@ const retryWithIncreasingDelay = async <T>(retryable: () => Promise<T>, browserN
}

export class BrowserCriClient {
currentlyAttachedTarget: CRIWrapper.Client | undefined
private constructor (private browserClient: CRIWrapper.Client, private versionInfo, private port: number, private browserName: string, private onAsynchronousError: Function) {}
currentlyAttachedTarget: CriClient | undefined
private constructor (private browserClient: CriClient, private versionInfo, private port: number, private browserName: string, private onAsynchronousError: Function) {}

/**
* Factory method for the browser cri client. Connects to the browser and then returns a chrome remote interface wrapper around the
Expand All @@ -79,7 +79,7 @@ export class BrowserCriClient {
* @param onAsynchronousError callback for any cdp fatal errors
* @returns a wrapper around the chrome remote interface that is connected to the browser target
*/
static async create (port: number, browserName: string, onAsynchronousError: Function, onReconnect?: (client: CRIWrapper.Client) => void): Promise<BrowserCriClient> {
static async create (port: number, browserName: string, onAsynchronousError: Function, onReconnect?: (client: CriClient) => void): Promise<BrowserCriClient> {
await ensureLiveBrowser(port, browserName)

return retryWithIncreasingDelay(async () => {
Expand Down Expand Up @@ -110,7 +110,7 @@ export class BrowserCriClient {
* @param url the url to attach to
* @returns the chrome remote interface wrapper for the target
*/
attachToTargetUrl = async (url: string): Promise<CRIWrapper.Client> => {
attachToTargetUrl = async (url: string): Promise<CriClient> => {
// Continue trying to re-attach until succcessful.
// If the browser opens slowly, this will fail until
// The browser and automation API is ready, so we try a few
Expand Down
11 changes: 8 additions & 3 deletions packages/server/lib/browsers/cdp_automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
import _ from 'lodash'
import Bluebird from 'bluebird'
import type { Protocol } from 'devtools-protocol'
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping'
import { cors, uri } from '@packages/network'
import debugModule from 'debug'
import { URL } from 'url'

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

export type CdpCommand = keyof ProtocolMapping.Commands

export type CdpEvent = keyof ProtocolMapping.Events

const debugVerbose = debugModule('cypress-verbose:server:browsers:cdp_automation')

export type CyCookie = Pick<chrome.cookies.Cookie, 'name' | 'value' | 'expirationDate' | 'hostOnly' | 'domain' | 'path' | 'secure' | 'httpOnly'> & {
Expand Down Expand Up @@ -163,9 +168,9 @@ export const normalizeResourceType = (resourceType: string | undefined): Resourc
return ffToStandardResourceTypeMap[resourceType] || 'other'
}

type SendDebuggerCommand = (message: string, data?: any) => Promise<any>
type SendCloseCommand = (shouldKeepTabOpen: boolean) => Promise<any>
type OnFn = (eventName: string, cb: Function) => void
type SendDebuggerCommand = (message: CdpCommand, data?: any) => Promise<any>
type SendCloseCommand = (shouldKeepTabOpen: boolean) => Promise<any> | void
type OnFn = (eventName: CdpEvent, cb: Function) => 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 Down
85 changes: 44 additions & 41 deletions packages/server/lib/browsers/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@ import utils from './utils'
import type { Browser } from './types'
import { BrowserCriClient } from './browser-cri-client'
import type { LaunchedBrowser } from '@packages/launcher/lib/browsers'
import type { CRIWrapper } from './cri-client'
import type { CriClient } from './cri-client'
import type { Automation } from '../automation'

// TODO: this is defined in `cypress-npm-api` but there is currently no way to get there
type CypressConfiguration = any
import type { BrowserLaunchOpts, BrowserNewTabOpts } from '@packages/types'

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

Expand Down Expand Up @@ -123,7 +121,7 @@ const DEFAULT_ARGS = [
'--disable-dev-shm-usage',
]

let browserCriClient
let browserCriClient: BrowserCriClient | undefined

/**
* Reads all known preference files (CHROME_PREFERENCE_PATHS) from disk and retur
Expand Down Expand Up @@ -320,15 +318,15 @@ const _handleDownloads = async function (client, dir, automation) {
let frameTree
let gettingFrameTree

const onReconnect = (client: CRIWrapper.Client) => {
const onReconnect = (client: CriClient) => {
// if the client disconnects (e.g. due to a computer sleeping), update
// the frame tree on reconnect in cases there were changes while
// the client was disconnected
return _updateFrameTree(client, 'onReconnect')()
}

// eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces
const _updateFrameTree = (client: CRIWrapper.Client, eventName) => async () => {
const _updateFrameTree = (client: CriClient, eventName) => async () => {
debug(`update frame tree for ${eventName}`)

gettingFrameTree = new Promise<void>(async (resolve) => {
Expand Down Expand Up @@ -433,8 +431,8 @@ const _handlePausedRequests = async (client) => {
})
}

const _setAutomation = async (client: CRIWrapper.Client, automation: Automation, resetBrowserTargets: (shouldKeepTabOpen: boolean) => Promise<void>, options: CypressConfiguration = {}) => {
const cdpAutomation = await CdpAutomation.create(client.send, client.on, resetBrowserTargets, automation, options.experimentalSessionAndOrigin)
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)
}
Expand Down Expand Up @@ -490,7 +488,7 @@ export = {
return extensionDest
},

_getArgs (browser: Browser, options: CypressConfiguration, port: string) {
_getArgs (browser: Browser, options: BrowserLaunchOpts, port: string) {
const args = ([] as string[]).concat(DEFAULT_ARGS)

if (os.platform() === 'linux') {
Expand Down Expand Up @@ -551,43 +549,60 @@ export = {
return args
},

async connectToNewSpec (browser: Browser, options: CypressConfiguration = {}, automation: Automation) {
async connectToNewSpec (browser: Browser, options: BrowserNewTabOpts, automation: Automation) {
debug('connecting to new chrome tab in existing instance with url and debugging port', { url: options.url })

const browserCriClient = this._getBrowserCriClient()

if (!browserCriClient) throw new Error('Missing browserCriClient in connectToNewSpec')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think assert is more common for this kind of thing, eg

assert(this.ctx.currentProject, `Cannot checkIfFileExists without active project`)

    assert(browserCriClient, 'Missing browserCriClient in connectToNewSpec')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert doesn't seem to provide the type guarding needed, which is why I added this conditional. With assert, I still get browserCriClient is possibly 'undefined'

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should actually close #22451

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, at least it will give a clearer error message.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well...it doesn't fix it but it just give a better warning. I remember talking with @ryanthemanuel about this

we close the browser on a retry:
https://github.com/cypress-io/cypress/blob/develop/packages/server/lib/modes/run.js#L1127
but likely don't flip the indicator that tells it to try and launch the browser from scratch


const pageCriClient = browserCriClient.currentlyAttachedTarget

if (!pageCriClient) throw new Error('Missing pageCriClient in connectToNewSpec')

if (!options.url) throw new Error('Missing url in connectToNewSpec')

await this.attachListeners(browser, options.url, pageCriClient, automation, options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of marking this as asyncand doingawait this.attachListenershould we just removeasyncand thenreturn this.attachListeners? It looks like we do awaitwhere we call this function so marking it asasyncand then callingawait` at the end doesn't seem to do anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readability-wise, I prefer awaiting a Promise over returning it, because it makes it obvious you're doing an asynchronous operation at the "end" of the function.

},

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

debug('connecting to existing chrome instance with url and debugging port', { url: options.url, port })
if (!options.onError) throw new Error('Missing onError in connectToExisting')

const browserCriClient = await BrowserCriClient.create(port, browser.displayName, options.onError, onReconnect)

if (!options.url) throw new Error('Missing url in connectToExisting')

const pageCriClient = await browserCriClient.attachToTargetUrl(options.url)

await this._setAutomation(pageCriClient, automation, browserCriClient.resetBrowserTargets, options)
},

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

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

// make sure page events are re enabled or else frame tree updates will NOT work as well as other items listening for page events
await pageCriClient.send('Page.enable')

await options.onInitializeNewBrowserTab()
await options.onInitializeNewBrowserTab?.()

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

await this._navigateUsingCRI(pageCriClient, options.url)
await this._navigateUsingCRI(pageCriClient, url)

if (options.experimentalSessionAndOrigin) {
await this._handlePausedRequests(pageCriClient)
_listenForFrameTreeChanges(pageCriClient)
}
},

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

debug('connecting to existing chrome instance with url and debugging port', { url: options.url, port })
const browserCriClient = await BrowserCriClient.create(port, browser.displayName, options.onError, onReconnect)
const pageCriClient = await browserCriClient.attachToTargetUrl(options.url)

await this._setAutomation(pageCriClient, automation, browserCriClient.resetBrowserTargets, options)
},

async open (browser: Browser, url, options: CypressConfiguration = {}, automation: Automation): Promise<LaunchedBrowser> {
async open (browser: Browser, url, options: BrowserLaunchOpts, automation: Automation): Promise<LaunchedBrowser> {
const { isTextTerminal } = options

const userDir = utils.getProfileDir(browser, isTextTerminal)
Expand Down Expand Up @@ -646,6 +661,8 @@ export = {
// SECOND connect to the Chrome remote interface
// and when the connection is ready
// navigate to the actual url
if (!options.onError) throw new Error('Missing onError in chrome#open')

browserCriClient = await BrowserCriClient.create(port, browser.displayName, options.onError, onReconnect)

la(browserCriClient, 'expected Chrome remote interface reference', browserCriClient)
Expand All @@ -669,7 +686,7 @@ export = {
debug('closing remote interface client')

// Do nothing on failure here since we're shutting down anyway
browserCriClient.close().catch()
browserCriClient?.close().catch()
browserCriClient = undefined

debug('closing chrome')
Expand All @@ -679,21 +696,7 @@ export = {

const pageCriClient = await browserCriClient.attachToTargetUrl('about:blank')

await this._setAutomation(pageCriClient, automation, browserCriClient.resetBrowserTargets, options)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice deduping of this code here. Good clean up


await pageCriClient.send('Page.enable')

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

await this._navigateUsingCRI(pageCriClient, url)

if (options.experimentalSessionAndOrigin) {
await this._handlePausedRequests(pageCriClient)
_listenForFrameTreeChanges(pageCriClient)
}
await this.attachListeners(browser, url, pageCriClient, automation, options)

// return the launched browser process
// with additional method to close the remote connection
Expand Down
Loading