From 519d33e93b0ac98a1dff77419790692ebd13dbc5 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Thu, 9 Mar 2023 17:03:59 -0600 Subject: [PATCH 01/18] feat: initial protocol api work --- packages/app/src/runner/event-manager.ts | 18 +++--- .../server/lib/browsers/browser-cri-client.ts | 2 +- packages/server/lib/browsers/chrome.ts | 6 ++ packages/server/lib/cloud/api.ts | 7 +++ packages/server/lib/cloud/protocol.ts | 62 +++++++++++++++++++ packages/server/lib/modes/index.ts | 4 -- packages/server/lib/modes/record.js | 6 +- packages/server/lib/modes/run.ts | 51 +++++++++++---- packages/server/lib/open_project.ts | 2 +- packages/server/lib/project-base.ts | 5 +- packages/server/lib/server-base.ts | 6 +- packages/server/lib/socket-base.ts | 9 ++- packages/server/lib/socket-ct.ts | 5 +- packages/server/lib/socket-e2e.ts | 6 +- packages/server/package.json | 1 + packages/types/src/index.ts | 2 + packages/types/src/protocol.ts | 8 +++ packages/types/src/server.ts | 6 ++ yarn.lock | 19 +++--- 19 files changed, 178 insertions(+), 47 deletions(-) create mode 100644 packages/server/lib/cloud/protocol.ts create mode 100644 packages/types/src/protocol.ts diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index 223cf6310e73..18450cd91019 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -542,10 +542,6 @@ export class EventManager { }) }) - Cypress.on('test:before:run:async', (test, _runnable) => { - this.reporterBus.emit('test:before:run:async', test) - }) - Cypress.on('test:after:run', (test, _runnable) => { this.reporterBus.emit('test:after:run', test, Cypress.config('isInteractive')) }) @@ -591,7 +587,11 @@ export class EventManager { this.localBus.emit('script:error', err) }) - Cypress.on('test:before:run:async', async (_attr, test) => { + Cypress.on('test:before:run:async', async (args) => { + const [attr, test] = args + + this.reporterBus.emit('test:before:run:async', test) + this.studioStore.interceptTest(test) // if the experimental flag is on and we are in a chromium based browser, @@ -601,6 +601,10 @@ export class EventManager { test: { title: test.title, order: test.order, currentRetry: test.currentRetry() }, }) } + + await Cypress.backend('protocol:test:before:run:async', attr, test) + + Cypress.primaryOriginCommunicator.toAllSpecBridges('test:before:run:async', ...args) }) Cypress.on('test:after:run', (test) => { @@ -615,10 +619,6 @@ export class EventManager { Cypress.primaryOriginCommunicator.toAllSpecBridges('test:before:run', ...args) }) - Cypress.on('test:before:run:async', (...args) => { - Cypress.primaryOriginCommunicator.toAllSpecBridges('test:before:run:async', ...args) - }) - // Inform all spec bridges that the primary origin has begun to unload. Cypress.on('window:before:unload', () => { Cypress.primaryOriginCommunicator.toAllSpecBridges('before:unload', window.origin) diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index 7978ee2e8dd2..9c683304e81f 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -91,7 +91,7 @@ const retryWithIncreasingDelay = async (retryable: () => Promise, browserN export class BrowserCriClient { currentlyAttachedTarget: CriClient | undefined - private constructor (private browserClient: CriClient, private versionInfo, private host: string, private port: number, private browserName: string, private onAsynchronousError: Function) {} + private constructor (private browserClient: CriClient, private versionInfo, public host: string, public 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 diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index 8d035f149b74..e2470895568d 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -640,6 +640,12 @@ export = { this._handleDownloads(pageCriClient, options.downloadsFolder, automation), ]) + await options.protocolManager?.connectToBrowser({ + target: pageCriClient.targetId, + host: browserCriClient.host, + port: browserCriClient.port, + }) + await this._navigateUsingCRI(pageCriClient, url) await this._handlePausedRequests(pageCriClient) diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts index 0afda703c4b4..635779310616 100644 --- a/packages/server/lib/cloud/api.ts +++ b/packages/server/lib/cloud/api.ts @@ -241,6 +241,7 @@ export type CreateRunOptions = { tags: string[] testingType: 'e2e' | 'component' timeout?: number + protocolManager?: any } let preflightResult = { @@ -330,6 +331,12 @@ module.exports = { }) }) }) + .then(async (result) => { + // TODO: Get url for the protocol code and pass it down to download + await options.protocolManager.setupProtocol() + + return result + }) .catch(RequestErrors.StatusCodeError, formatResponseBody) .catch(tagError) }, diff --git a/packages/server/lib/cloud/protocol.ts b/packages/server/lib/cloud/protocol.ts new file mode 100644 index 000000000000..09a8e595d20f --- /dev/null +++ b/packages/server/lib/cloud/protocol.ts @@ -0,0 +1,62 @@ +import fs from 'fs-extra' +import { NodeVM } from 'vm2' +import Debug from 'debug' +import CDP from 'chrome-remote-interface' +import type { ProtocolManager } from '@packages/types' + +const debug = Debug('cypress:server:protocol') + +const setupProtocol = async () => { + let script: string + + // TODO: We will need to remove this option in production + if (process.env.CYPRESS_LOCAL_PROTOCOL_PATH) { + script = await fs.readFile(process.env.CYPRESS_LOCAL_PROTOCOL_PATH, 'utf8') + } else { + // TODO: Download the protocol script from the cloud + script = '' + } + + const vm = new NodeVM({ + console: 'inherit', + sandbox: { Debug, CDP }, + }) + + const { Capture } = vm.run(script) + + return new Capture() +} + +class ProtocolManagerImpl implements ProtocolManager { + private protocol: any + + async setupProtocol () { + debug('setting up protocol') + + this.protocol = await setupProtocol() + } + + connectToBrowser (options) { + debug('connecting to browser for new spec') + this.protocol.connectToBrowser(options) + } + + beforeSpec (spec) { + debug('initializing new spec %O', spec.relative) + this.protocol.beforeSpec(spec) + + // Initialize DB here + } + + afterSpec () { + debug('after spec') + this.protocol.afterSpec() + } + + beforeTest (attr, test) { + debug('initialize new test %O', test.title) + this.protocol.beforeTest(attr, test) + } +} + +export default ProtocolManagerImpl diff --git a/packages/server/lib/modes/index.ts b/packages/server/lib/modes/index.ts index a1389911bd66..b386ee9f0ed5 100644 --- a/packages/server/lib/modes/index.ts +++ b/packages/server/lib/modes/index.ts @@ -5,10 +5,6 @@ import { makeDataContext } from '../makeDataContext' import random from '../util/random' export = (mode, options) => { - if (mode === 'record') { - return require('./record').run(options) - } - if (mode === 'smokeTest') { return require('./smoke_test').run(options) } diff --git a/packages/server/lib/modes/record.js b/packages/server/lib/modes/record.js index 0d52e25c7b08..ddeff44896a8 100644 --- a/packages/server/lib/modes/record.js +++ b/packages/server/lib/modes/record.js @@ -267,7 +267,7 @@ const createRun = Promise.method((options = {}) => { ciBuildId: null, }) - let { projectRoot, projectId, recordKey, platform, git, specPattern, specs, parallel, ciBuildId, group, tags, testingType, autoCancelAfterFailures } = options + let { projectRoot, projectId, recordKey, platform, git, specPattern, specs, parallel, ciBuildId, group, tags, testingType, autoCancelAfterFailures, protocolManager } = options if (recordKey == null) { recordKey = env.get('CYPRESS_RECORD_KEY') @@ -324,6 +324,7 @@ const createRun = Promise.method((options = {}) => { ci, commit, autoCancelAfterFailures, + protocolManager, }) .tap((response) => { if (!(response && response.warnings && response.warnings.length)) { @@ -597,6 +598,7 @@ const createRunAndRecordSpecs = (options = {}) => { testingType, quiet, autoCancelAfterFailures, + protocolManager, } = options const recordKey = options.key @@ -632,6 +634,7 @@ const createRunAndRecordSpecs = (options = {}) => { testingType, configFile: config ? config.configFile : null, autoCancelAfterFailures, + protocolManager, }) .then((resp) => { if (!resp) { @@ -672,6 +675,7 @@ const createRunAndRecordSpecs = (options = {}) => { .pick('spec', 'claimedInstances', 'totalInstances') .extend({ estimated: resp.estimatedWallClockDuration, + instanceId, }) .value() }) diff --git a/packages/server/lib/modes/run.ts b/packages/server/lib/modes/run.ts index adc11abac84b..934211931a90 100644 --- a/packages/server/lib/modes/run.ts +++ b/packages/server/lib/modes/run.ts @@ -26,6 +26,7 @@ import type { Cfg } from '../project-base' import type { Browser } from '../browsers/types' import { debugElapsedTime } from '../util/performance_benchmark' import * as printResults from '../util/print-run' +import ProtocolManager from '../cloud/protocol' type SetScreenshotMetadata = (data: TakeScreenshotProps) => void type ScreenshotMetadata = ReturnType @@ -55,8 +56,8 @@ const relativeSpecPattern = (projectRoot, pattern) => { return pattern.map((x) => x.replace(`${projectRoot}/`, '')) } -const iterateThroughSpecs = function (options: { specs: SpecFile[], runEachSpec: RunEachSpec, beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, config: Cfg }) { - const { specs, runEachSpec, beforeSpecRun, afterSpecRun, config } = options +const iterateThroughSpecs = function (options: { specs: SpecFile[], runEachSpec: RunEachSpec, beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, config: Cfg, protocolManager?: any }) { + const { specs, runEachSpec, beforeSpecRun, afterSpecRun, config, protocolManager } = options const serial = () => { return Bluebird.mapSeries(specs, runEachSpec) @@ -65,7 +66,7 @@ const iterateThroughSpecs = function (options: { specs: SpecFile[], runEachSpec: const ranSpecs: SpecFile[] = [] async function parallelAndSerialWithRecord (runs) { - const { spec, claimedInstances, totalInstances, estimated } = await beforeSpecRun() + const { spec, claimedInstances, totalInstances, estimated, instanceId } = await beforeSpecRun() // no more specs to run? then we're done! if (!spec) return runs @@ -77,6 +78,13 @@ const iterateThroughSpecs = function (options: { specs: SpecFile[], runEachSpec: if (!specObject) throw new Error(`Unable to find specObject for spec '${spec}'`) + if (protocolManager) { + protocolManager.beforeSpec({ + ...specObject, + instanceId, + }) + } + ranSpecs.push(specObject) const results = await runEachSpec( @@ -88,6 +96,10 @@ const iterateThroughSpecs = function (options: { specs: SpecFile[], runEachSpec: runs.push(results) + if (protocolManager) { + protocolManager.afterSpec() + } + await afterSpecRun(specObject, results, config) // recurse @@ -158,6 +170,7 @@ const openProjectCreate = (projectRoot, socketId, args) => { onWarning, spec: args.spec, onError: args.onError, + protocolManager: args.protocolManager, } return openProject.create(projectRoot, args, options) @@ -345,12 +358,13 @@ async function postProcessRecording (options: { quiet: boolean, videoCompression return continueProcessing(onProgress) } -function launchBrowser (options: { browser: Browser, spec: SpecWithRelativeRoot, setScreenshotMetadata: SetScreenshotMetadata, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, onError: (err: Error) => void, videoRecording?: VideoRecording }) { - const { browser, spec, setScreenshotMetadata, screenshots, projectRoot, shouldLaunchNewTab, onError } = options +function launchBrowser (options: { browser: Browser, spec: SpecWithRelativeRoot, setScreenshotMetadata: SetScreenshotMetadata, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, onError: (err: Error) => void, videoRecording?: VideoRecording, protocolManager?: any }) { + const { browser, spec, setScreenshotMetadata, screenshots, projectRoot, shouldLaunchNewTab, onError, protocolManager } = options const warnings = {} const browserOpts: OpenProjectLaunchOpts = { + protocolManager, projectRoot, shouldLaunchNewTab, onError, @@ -443,7 +457,7 @@ function listenForProjectEnd (project, exit): Bluebird { }) } -async function waitForBrowserToConnect (options: { project: Project, socketId: string, onError: (err: Error) => void, spec: SpecWithRelativeRoot, isFirstSpec: boolean, testingType: string, experimentalSingleTabRunMode: boolean, browser: Browser, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, webSecurity: boolean, videoRecording?: VideoRecording }) { +async function waitForBrowserToConnect (options: { project: Project, socketId: string, onError: (err: Error) => void, spec: SpecWithRelativeRoot, isFirstSpec: boolean, testingType: string, experimentalSingleTabRunMode: boolean, browser: Browser, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, webSecurity: boolean, videoRecording?: VideoRecording, protocolManager?: any }) { if (globalThis.CY_TEST_MOCK?.waitForBrowserToConnect) return Promise.resolve() const { project, socketId, onError, spec } = options @@ -692,10 +706,10 @@ function screenshotMetadata (data, resp) { } } -async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, headed: boolean, outputPath: string, specs: SpecWithRelativeRoot[], specPattern: string | RegExp | string[], beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, runUrl?: string, parallel?: boolean, group?: string, tag?: string, autoCancelAfterFailures?: number | false, testingType: TestingType, quiet: boolean, project: Project, onError: (err: Error) => void, exit: boolean, socketId: string, webSecurity: boolean, projectRoot: string } & Pick) { +async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, headed: boolean, outputPath: string, specs: SpecWithRelativeRoot[], specPattern: string | RegExp | string[], beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, runUrl?: string, parallel?: boolean, group?: string, tag?: string, autoCancelAfterFailures?: number | false, testingType: TestingType, quiet: boolean, project: Project, onError: (err: Error) => void, exit: boolean, socketId: string, webSecurity: boolean, projectRoot: string, protocolManager?: any } & Pick) { if (globalThis.CY_TEST_MOCK?.runSpecs) return globalThis.CY_TEST_MOCK.runSpecs - const { config, browser, sys, headed, outputPath, specs, specPattern, beforeSpecRun, afterSpecRun, runUrl, parallel, group, tag, autoCancelAfterFailures } = options + const { config, browser, sys, headed, outputPath, specs, specPattern, beforeSpecRun, afterSpecRun, runUrl, parallel, group, tag, autoCancelAfterFailures, protocolManager } = options const isHeadless = !headed @@ -723,7 +737,7 @@ async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, hea printResults.displaySpecHeader(spec.relativeToCommonRoot, index + 1, length, estimated) } - const { results } = await runSpec(config, spec, options, estimated, isFirstSpec, index === length - 1) + const { results } = await runSpec(config, spec, options, estimated, isFirstSpec, index === length - 1, protocolManager) isFirstSpec = false @@ -754,6 +768,7 @@ async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, hea runEachSpec, afterSpecRun, beforeSpecRun, + protocolManager, }) const results: CypressCommandLine.CypressRunResult = { @@ -822,7 +837,7 @@ async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, hea return results } -async function runSpec (config, spec: SpecWithRelativeRoot, options: { project: Project, browser: Browser, onError: (err: Error) => void, config: Cfg, quiet: boolean, exit: boolean, testingType: TestingType, socketId: string, webSecurity: boolean, projectRoot: string } & Pick, estimated, isFirstSpec, isLastSpec) { +async function runSpec (config, spec: SpecWithRelativeRoot, options: { project: Project, browser: Browser, onError: (err: Error) => void, config: Cfg, quiet: boolean, exit: boolean, testingType: TestingType, socketId: string, webSecurity: boolean, projectRoot: string } & Pick, estimated, isFirstSpec, isLastSpec, protocolManager) { const { project, browser, onError } = options const { isHeadless } = browser @@ -889,6 +904,7 @@ async function runSpec (config, spec: SpecWithRelativeRoot, options: { project: isFirstSpec, experimentalSingleTabRunMode: config.experimentalSingleTabRunMode, shouldLaunchNewTab: !isFirstSpec, + protocolManager, }), ]) @@ -910,6 +926,12 @@ async function ready (options: { projectRoot: string, record: boolean, key: stri const { projectRoot, record, key, ciBuildId, parallel, group, browser: browserName, tag, testingType, socketId, autoCancelAfterFailures } = options + let protocolManager: ProtocolManager | undefined + + if (record) { + protocolManager = new ProtocolManager() + } + assert(socketId) // this needs to be a closure over `exitEarly` and not a reference @@ -928,7 +950,10 @@ async function ready (options: { projectRoot: string, record: boolean, key: stri // TODO: refactor this so we don't need to extend options options.browsers = browsers - const { project, projectId, config, configFile } = await createAndOpenProject(options) + const { project, projectId, config, configFile } = await createAndOpenProject({ + ...options, + protocolManager, + }) debug('project created and opened with config %o', config) @@ -977,7 +1002,7 @@ async function ready (options: { projectRoot: string, record: boolean, key: stri chromePolicyCheck.run(onWarning) } - async function runAllSpecs ({ beforeSpecRun, afterSpecRun, runUrl, parallel }: { beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, runUrl?: string, parallel?: boolean}) { + async function runAllSpecs ({ beforeSpecRun, afterSpecRun, runUrl, parallel }: { beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, runUrl?: string, parallel?: boolean }) { const results = await runSpecs({ autoCancelAfterFailures, beforeSpecRun, @@ -1007,6 +1032,7 @@ async function ready (options: { projectRoot: string, record: boolean, key: stri testingType: options.testingType, exit: options.exit, webSecurity: options.webSecurity, + protocolManager, }) if (!options.quiet) { @@ -1040,6 +1066,7 @@ async function ready (options: { projectRoot: string, record: boolean, key: stri runAllSpecs, onError, quiet: options.quiet, + protocolManager, }) } diff --git a/packages/server/lib/open_project.ts b/packages/server/lib/open_project.ts index ad79bf2e1d81..408d4cf7f932 100644 --- a/packages/server/lib/open_project.ts +++ b/packages/server/lib/open_project.ts @@ -294,7 +294,7 @@ export class OpenProject { try { await this.projectBase.initializeConfig() - await this.projectBase.open() + await this.projectBase.open(options.protocolManager) } catch (err: any) { if (err.isCypressErr && err.portInUse) { errors.throwErr(err.type, err.port) diff --git a/packages/server/lib/project-base.ts b/packages/server/lib/project-base.ts index 85da6bfac22c..cb039c9d9c5f 100644 --- a/packages/server/lib/project-base.ts +++ b/packages/server/lib/project-base.ts @@ -20,7 +20,7 @@ import { SocketE2E } from './socket-e2e' import { ensureProp } from './util/class-helpers' import system from './util/system' -import type { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ReceivedCypressOptions, ResolvedConfigurationOptions, TestingType, VideoRecording } from '@packages/types' +import type { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ReceivedCypressOptions, ResolvedConfigurationOptions, TestingType, VideoRecording, ProtocolManager } from '@packages/types' import { DataContext, getCtx } from '@packages/data-context' import { createHmac } from 'crypto' @@ -143,7 +143,7 @@ export class ProjectBase extends EE { : new ServerCt() as TServer } - async open () { + async open (protocolManager?: ProtocolManager) { debug('opening project instance %s', this.projectRoot) debug('project open options %o', this.options) @@ -162,6 +162,7 @@ export class ProjectBase extends EE { shouldCorrelatePreRequests: this.shouldCorrelatePreRequests, testingType: this.testingType, SocketCtor: this.testingType === 'e2e' ? SocketE2E : SocketCt, + protocolManager, }) this.ctx.setAppServerPort(port) diff --git a/packages/server/lib/server-base.ts b/packages/server/lib/server-base.ts index 8676881d9527..cb13c3316c7d 100644 --- a/packages/server/lib/server-base.ts +++ b/packages/server/lib/server-base.ts @@ -29,7 +29,7 @@ import type { Browser } from '@packages/server/lib/browsers/types' import { InitializeRoutes, createCommonRoutes } from './routes' import { createRoutesE2E } from './routes-e2e' import { createRoutesCT } from './routes-ct' -import type { FoundSpec } from '@packages/types' +import type { FoundSpec, ProtocolManager } from '@packages/types' import type { Server as WebSocketServer } from 'ws' import { RemoteStates } from './remote_states' import { cookieJar, SerializableAutomationCookie } from './util/cookies' @@ -109,6 +109,7 @@ export interface OpenServerOptions { getCurrentBrowser: () => Browser getSpec: () => FoundSpec | null shouldCorrelatePreRequests: () => boolean + protocolManager?: ProtocolManager } export abstract class ServerBase { @@ -207,6 +208,7 @@ export abstract class ServerBase { testingType, SocketCtor, exit, + protocolManager, }: OpenServerOptions) { debug('server open') @@ -222,7 +224,7 @@ export abstract class ServerBase { target: config.baseUrl && testingType === 'component' ? config.baseUrl : undefined, }) - this._socket = new SocketCtor(config) as TSocket + this._socket = new SocketCtor(config, protocolManager) as TSocket clientCertificates.loadClientCertificateConfig(config) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 0d82385e3605..0be5a9668d3f 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -26,7 +26,7 @@ import runEvents from './plugins/run_events' // eslint-disable-next-line no-duplicate-imports import type { Socket } from '@packages/socket' -import type { RunState, CachedTestState } from '@packages/types' +import type { RunState, CachedTestState, ProtocolManager } from '@packages/types' import { cors } from '@packages/network' import memory from './browsers/memory' @@ -70,12 +70,14 @@ export class SocketBase { protected supportsRunEvents: boolean protected ended: boolean protected _io?: socketIo.SocketIOServer + protected protocolManager: any localBus: EventEmitter - constructor (config: Record) { + constructor (config: Record, protocolManager?: ProtocolManager) { this.supportsRunEvents = config.isTextTerminal || config.experimentalInteractiveRunEvents this.ended = false this.localBus = new EventEmitter() + this.protocolManager = protocolManager } protected ensureProp = ensureProp @@ -473,6 +475,8 @@ export class SocketBase { return memory.endProfiling() case 'check:memory:pressure': return memory.checkMemoryPressure({ ...args[0], automation }) + case 'protocol:test:before:run:async': + return this.protocolManager.beforeTest(args[0], args[1]) default: throw new Error(`You requested a backend event we cannot handle: ${eventName}`) } @@ -563,6 +567,7 @@ export class SocketBase { reporterEvents.forEach((event) => { socket.on(event, (data) => { + debug('reporter event %o', { event, data }) this.toRunner(event, data) }) }) diff --git a/packages/server/lib/socket-ct.ts b/packages/server/lib/socket-ct.ts index 2e91a42b0a22..ac088f005347 100644 --- a/packages/server/lib/socket-ct.ts +++ b/packages/server/lib/socket-ct.ts @@ -5,14 +5,15 @@ import dfd from 'p-defer' import type { Socket } from '@packages/socket' import type { DestroyableHttpServer } from '@packages/server/lib/util/server_destroy' import assert from 'assert' +import type { ProtocolManager } from '@packages/types' const debug = Debug('cypress:server:socket-ct') export class SocketCt extends SocketBase { #destroyAutPromise?: dfd.DeferredPromise - constructor (config: Record) { - super(config) + constructor (config: Record, protocolManager?: ProtocolManager) { + super(config, protocolManager) // should we use this option at all for component testing 😕? if (config.watchForFileChanges) { diff --git a/packages/server/lib/socket-e2e.ts b/packages/server/lib/socket-e2e.ts index af1dec4ea351..57ad99cab5ae 100644 --- a/packages/server/lib/socket-e2e.ts +++ b/packages/server/lib/socket-e2e.ts @@ -4,7 +4,7 @@ import { SocketBase } from './socket-base' import { fs } from './util/fs' import type { DestroyableHttpServer } from './util/server_destroy' import * as studio from './studio' -import type { FoundSpec } from '@packages/types' +import type { FoundSpec, ProtocolManager } from '@packages/types' const debug = Debug('cypress:server:socket-e2e') @@ -15,8 +15,8 @@ const isSpecialSpec = (name) => { export class SocketE2E extends SocketBase { private testFilePath: string | null - constructor (config: Record) { - super(config) + constructor (config: Record, protocolManager?: ProtocolManager) { + super(config, protocolManager) this.testFilePath = null diff --git a/packages/server/package.json b/packages/server/package.json index 387a1de0be86..81d6e91e51e4 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -127,6 +127,7 @@ "underscore.string": "3.3.6", "url-parse": "1.5.9", "uuid": "8.3.2", + "vm2": "3.9.14", "webpack-virtual-modules": "0.5.0", "widest-line": "3.1.0" }, diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index c6a7b3de5163..5ba54eda0e31 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -41,3 +41,5 @@ export * from './modeOptions' export * from './git' export * from './video' + +export * from './protocol' diff --git a/packages/types/src/protocol.ts b/packages/types/src/protocol.ts new file mode 100644 index 000000000000..f8098ef5478a --- /dev/null +++ b/packages/types/src/protocol.ts @@ -0,0 +1,8 @@ +// TODO: This is basic for now but will evolve as we progress with the protocol work + +export interface ProtocolManager { + connectToBrowser(options: any): void + beforeSpec(spec: any): void + afterSpec(): void + beforeTest(attr: any, test: any): void +} diff --git a/packages/types/src/server.ts b/packages/types/src/server.ts index 8dcd8e330501..fc7740e89882 100644 --- a/packages/types/src/server.ts +++ b/packages/types/src/server.ts @@ -2,6 +2,7 @@ import type { FoundBrowser } from './browser' import type { ReceivedCypressOptions } from './config' import type { PlatformName } from './platform' import type { RunModeVideoApi } from './video' +import type { ProtocolManager } from './protocol' export type OpenProjectLaunchOpts = { projectRoot: string @@ -10,6 +11,7 @@ export type OpenProjectLaunchOpts = { videoApi?: RunModeVideoApi onWarning: (err: Error) => void onError: (err: Error) => void + protocolManager?: any } export type BrowserLaunchOpts = { @@ -21,6 +23,7 @@ export type BrowserLaunchOpts = { onBrowserClose?: (...args: unknown[]) => void onBrowserOpen?: (...args: unknown[]) => void relaunchBrowser?: () => Promise + protocolManager?: any } & Partial // TODO: remove the `Partial` here by making it impossible for openProject.launch to be called w/o OpenProjectLaunchOpts & Pick @@ -90,6 +93,9 @@ export interface OpenProjectLaunchOptions { onChange?: WebSocketOptionsCallback onError?: (err: Error) => void + // Manager used to communicate Cypress lifecycle events to the protocol + protocolManager?: ProtocolManager + [key: string]: any } diff --git a/yarn.lock b/yarn.lock index f87b23fec610..90c4f23092e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7993,10 +7993,10 @@ acorn@^7.0.0, acorn@^7.1.1, acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1: - version "8.8.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" - integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== +acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0, acorn@^8.7.1: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== address@^1.0.1: version "1.1.2" @@ -29960,10 +29960,13 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -vm2@^3.9.3: - version "3.9.5" - resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.5.tgz#5288044860b4bbace443101fcd3bddb2a0aa2496" - integrity sha512-LuCAHZN75H9tdrAiLFf030oW7nJV5xwNMuk1ymOZwopmuK3d2H4L1Kv4+GFHgarKiLfXXLFU+7LDABHnwOkWng== +vm2@3.9.14, vm2@^3.9.3: + version "3.9.14" + resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.14.tgz#964042b474cf1e6e4f475a39144773cdb9deb734" + integrity sha512-HgvPHYHeQy8+QhzlFryvSteA4uQLBCOub02mgqdR+0bN/akRZ48TGB1v0aCv7ksyc0HXx16AZtMHKS38alc6TA== + dependencies: + acorn "^8.7.0" + acorn-walk "^8.2.0" void-elements@^3.1.0: version "3.1.0" From 294c6dcb1a50fc67fc84a0481dbbb5634dc5ede1 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Thu, 9 Mar 2023 17:11:08 -0600 Subject: [PATCH 02/18] renaming --- packages/server/lib/cloud/protocol.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/lib/cloud/protocol.ts b/packages/server/lib/cloud/protocol.ts index 09a8e595d20f..9efdd5511de5 100644 --- a/packages/server/lib/cloud/protocol.ts +++ b/packages/server/lib/cloud/protocol.ts @@ -22,9 +22,9 @@ const setupProtocol = async () => { sandbox: { Debug, CDP }, }) - const { Capture } = vm.run(script) + const { AppCaptureProtocol } = vm.run(script) - return new Capture() + return new AppCaptureProtocol() } class ProtocolManagerImpl implements ProtocolManager { From e37638a52b03fe37739fdcacea18899b53ae6940 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Thu, 9 Mar 2023 21:10:26 -0600 Subject: [PATCH 03/18] slight refactoring --- packages/server/lib/cloud/protocol.ts | 49 +++++++++++++++++---------- packages/types/src/protocol.ts | 2 ++ 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/packages/server/lib/cloud/protocol.ts b/packages/server/lib/cloud/protocol.ts index 9efdd5511de5..8293c034d61c 100644 --- a/packages/server/lib/cloud/protocol.ts +++ b/packages/server/lib/cloud/protocol.ts @@ -4,58 +4,73 @@ import Debug from 'debug' import CDP from 'chrome-remote-interface' import type { ProtocolManager } from '@packages/types' +interface AppCaptureProtocolInterface { + setupProtocol (url?: string): Promise + connectToBrowser (options: any): void + beforeSpec (spec: any): void + afterSpec (): void + beforeTest (attr: any, test: any): void +} + const debug = Debug('cypress:server:protocol') -const setupProtocol = async () => { - let script: string +const setupProtocol = async (url?: string): Promise => { + let script: string | undefined // TODO: We will need to remove this option in production if (process.env.CYPRESS_LOCAL_PROTOCOL_PATH) { script = await fs.readFile(process.env.CYPRESS_LOCAL_PROTOCOL_PATH, 'utf8') - } else { + } else if (url) { // TODO: Download the protocol script from the cloud - script = '' } - const vm = new NodeVM({ - console: 'inherit', - sandbox: { Debug, CDP }, - }) + if (script) { + const vm = new NodeVM({ + console: 'inherit', + sandbox: { Debug, CDP }, + }) + + const { AppCaptureProtocol } = vm.run(script) - const { AppCaptureProtocol } = vm.run(script) + return new AppCaptureProtocol() + } - return new AppCaptureProtocol() + return } class ProtocolManagerImpl implements ProtocolManager { - private protocol: any + private protocol: AppCaptureProtocolInterface | undefined + + protocolEnabled (): boolean { + return !!this.protocol + } - async setupProtocol () { + async setupProtocol (url?: string) { debug('setting up protocol') - this.protocol = await setupProtocol() + this.protocol = await setupProtocol(url) } connectToBrowser (options) { debug('connecting to browser for new spec') - this.protocol.connectToBrowser(options) + this.protocol?.connectToBrowser(options) } beforeSpec (spec) { debug('initializing new spec %O', spec.relative) - this.protocol.beforeSpec(spec) + this.protocol?.beforeSpec(spec) // Initialize DB here } afterSpec () { debug('after spec') - this.protocol.afterSpec() + this.protocol?.afterSpec() } beforeTest (attr, test) { debug('initialize new test %O', test.title) - this.protocol.beforeTest(attr, test) + this.protocol?.beforeTest(attr, test) } } diff --git a/packages/types/src/protocol.ts b/packages/types/src/protocol.ts index f8098ef5478a..720ba9f776d5 100644 --- a/packages/types/src/protocol.ts +++ b/packages/types/src/protocol.ts @@ -1,6 +1,8 @@ // TODO: This is basic for now but will evolve as we progress with the protocol work export interface ProtocolManager { + setupProtocol(url?: string): Promise + protocolEnabled(): boolean connectToBrowser(options: any): void beforeSpec(spec: any): void afterSpec(): void From 6de648e7007ce4c30d5bfcf65ec057b9984aa5aa Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Thu, 9 Mar 2023 21:25:45 -0600 Subject: [PATCH 04/18] slight refactoring --- packages/app/src/runner/event-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index 18450cd91019..51501d603a4a 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -587,7 +587,7 @@ export class EventManager { this.localBus.emit('script:error', err) }) - Cypress.on('test:before:run:async', async (args) => { + Cypress.on('test:before:run:async', async (...args) => { const [attr, test] = args this.reporterBus.emit('test:before:run:async', test) From 806a811e714e853b03efe9d1958a0aae62b0820a Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Thu, 9 Mar 2023 21:41:48 -0600 Subject: [PATCH 05/18] slight refactoring --- packages/server/lib/socket-base.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 0be5a9668d3f..1a29e1816ff1 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -70,7 +70,7 @@ export class SocketBase { protected supportsRunEvents: boolean protected ended: boolean protected _io?: socketIo.SocketIOServer - protected protocolManager: any + protected protocolManager?: ProtocolManager localBus: EventEmitter constructor (config: Record, protocolManager?: ProtocolManager) { @@ -476,7 +476,7 @@ export class SocketBase { case 'check:memory:pressure': return memory.checkMemoryPressure({ ...args[0], automation }) case 'protocol:test:before:run:async': - return this.protocolManager.beforeTest(args[0], args[1]) + return this.protocolManager?.beforeTest(args[0], args[1]) default: throw new Error(`You requested a backend event we cannot handle: ${eventName}`) } From b7a34dac76c5d2a166343d4cc615b13198adc0c3 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Thu, 9 Mar 2023 22:16:13 -0600 Subject: [PATCH 06/18] slight refactoring --- packages/app/src/runner/event-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index 51501d603a4a..0767e7da3961 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -590,7 +590,7 @@ export class EventManager { Cypress.on('test:before:run:async', async (...args) => { const [attr, test] = args - this.reporterBus.emit('test:before:run:async', test) + this.reporterBus.emit('test:before:run:async', attr) this.studioStore.interceptTest(test) From dd6d89e175897c789bfa78930e25d6426fef2224 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Thu, 9 Mar 2023 22:54:40 -0600 Subject: [PATCH 07/18] fix tests --- packages/app/src/runner/event-manager.ts | 2 +- packages/server/lib/cloud/protocol.ts | 8 ++++---- packages/server/lib/socket-base.ts | 2 +- packages/types/src/protocol.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index 0767e7da3961..787710fae33e 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -602,7 +602,7 @@ export class EventManager { }) } - await Cypress.backend('protocol:test:before:run:async', attr, test) + await Cypress.backend('protocol:test:before:run:async', { id: test.id, title: test.title, wallClockStartedAt: test.wallClockStartedAt.getTime() }) Cypress.primaryOriginCommunicator.toAllSpecBridges('test:before:run:async', ...args) }) diff --git a/packages/server/lib/cloud/protocol.ts b/packages/server/lib/cloud/protocol.ts index 8293c034d61c..401a57e1ba89 100644 --- a/packages/server/lib/cloud/protocol.ts +++ b/packages/server/lib/cloud/protocol.ts @@ -9,7 +9,7 @@ interface AppCaptureProtocolInterface { connectToBrowser (options: any): void beforeSpec (spec: any): void afterSpec (): void - beforeTest (attr: any, test: any): void + beforeTest (test: any): void } const debug = Debug('cypress:server:protocol') @@ -68,9 +68,9 @@ class ProtocolManagerImpl implements ProtocolManager { this.protocol?.afterSpec() } - beforeTest (attr, test) { - debug('initialize new test %O', test.title) - this.protocol?.beforeTest(attr, test) + beforeTest (test) { + debug('initialize new test %O', test) + this.protocol?.beforeTest(test) } } diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 1a29e1816ff1..e4c962ac4b15 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -476,7 +476,7 @@ export class SocketBase { case 'check:memory:pressure': return memory.checkMemoryPressure({ ...args[0], automation }) case 'protocol:test:before:run:async': - return this.protocolManager?.beforeTest(args[0], args[1]) + return this.protocolManager?.beforeTest(args[0]) default: throw new Error(`You requested a backend event we cannot handle: ${eventName}`) } diff --git a/packages/types/src/protocol.ts b/packages/types/src/protocol.ts index 720ba9f776d5..39ac5a6ba019 100644 --- a/packages/types/src/protocol.ts +++ b/packages/types/src/protocol.ts @@ -6,5 +6,5 @@ export interface ProtocolManager { connectToBrowser(options: any): void beforeSpec(spec: any): void afterSpec(): void - beforeTest(attr: any, test: any): void + beforeTest(test: any): void } From bdb0a2cada3bfc86ec136a8078915efb9e83a9a6 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 10 Mar 2023 09:08:42 -0600 Subject: [PATCH 08/18] fix tests --- packages/server/test/integration/cypress_spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/server/test/integration/cypress_spec.js b/packages/server/test/integration/cypress_spec.js index 581c912b512d..4f190c332c4b 100644 --- a/packages/server/test/integration/cypress_spec.js +++ b/packages/server/test/integration/cypress_spec.js @@ -82,6 +82,7 @@ const ELECTRON_BROWSER = { path: '', version: '99.101.1234', majorVersion: 99, + protocolManager: undefined, } const previousCwd = process.cwd() @@ -191,6 +192,7 @@ describe('lib/cypress', () => { // to make sure our Electron browser mock object passes validation during tests sinon.stub(process, 'versions').value({ + ...process.versions, chrome: ELECTRON_BROWSER.version, electron: '123.45.6789', }) From 04b70684b5bb18d52b739d30560bbd0f8997a19d Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 10 Mar 2023 10:29:58 -0600 Subject: [PATCH 09/18] fix tests --- packages/server/lib/cloud/api.ts | 2 +- packages/server/test/integration/cypress_spec.js | 4 +++- packages/server/test/unit/modes/record_spec.js | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts index 635779310616..4ed583ebab8e 100644 --- a/packages/server/lib/cloud/api.ts +++ b/packages/server/lib/cloud/api.ts @@ -333,7 +333,7 @@ module.exports = { }) .then(async (result) => { // TODO: Get url for the protocol code and pass it down to download - await options.protocolManager.setupProtocol() + await options.protocolManager?.setupProtocol() return result }) diff --git a/packages/server/test/integration/cypress_spec.js b/packages/server/test/integration/cypress_spec.js index 4f190c332c4b..6ae95d5132c1 100644 --- a/packages/server/test/integration/cypress_spec.js +++ b/packages/server/test/integration/cypress_spec.js @@ -47,6 +47,8 @@ const { getCtx, clearCtx, setCtx, makeDataContext } = require(`../../lib/makeDat const { BrowserCriClient } = require(`../../lib/browsers/browser-cri-client`) const { cloudRecommendationMessage } = require('../../lib/util/print-run') +const processVersions = process.versions + const TYPICAL_BROWSERS = [ { name: 'chrome', @@ -192,7 +194,7 @@ describe('lib/cypress', () => { // to make sure our Electron browser mock object passes validation during tests sinon.stub(process, 'versions').value({ - ...process.versions, + ...processVersions, chrome: ELECTRON_BROWSER.version, electron: '123.45.6789', }) diff --git a/packages/server/test/unit/modes/record_spec.js b/packages/server/test/unit/modes/record_spec.js index dd08db98a9a5..1a30aed44945 100644 --- a/packages/server/test/unit/modes/record_spec.js +++ b/packages/server/test/unit/modes/record_spec.js @@ -339,6 +339,7 @@ describe('lib/modes/record', () => { }, tags: ['nightly', 'develop'], autoCancelAfterFailures: 4, + protocolManager: undefined, }) }) }) From 986a2d8de783b354baef26b74040c0de34a75826 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 10 Mar 2023 13:25:43 -0600 Subject: [PATCH 10/18] fix tests --- packages/server/test/integration/cypress_spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/server/test/integration/cypress_spec.js b/packages/server/test/integration/cypress_spec.js index 6ae95d5132c1..233759a0bc41 100644 --- a/packages/server/test/integration/cypress_spec.js +++ b/packages/server/test/integration/cypress_spec.js @@ -84,7 +84,6 @@ const ELECTRON_BROWSER = { path: '', version: '99.101.1234', majorVersion: 99, - protocolManager: undefined, } const previousCwd = process.cwd() @@ -180,7 +179,7 @@ describe('lib/cypress', () => { sinon.stub(videoCapture, 'start').resolves({}) sinon.stub(electronApp, 'isRunning').returns(true) sinon.stub(extension, 'setHostAndPath').resolves() - sinon.stub(detect, 'detect').resolves(TYPICAL_BROWSERS) + sinon.stub(detect, 'detect').resolves([...TYPICAL_BROWSERS]) sinon.stub(process, 'exit') sinon.stub(ServerE2E.prototype, 'reset') sinon.stub(errors, 'warning') From 2ed841c2566305a2416b521e9e603910bc157382 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 10 Mar 2023 13:48:03 -0600 Subject: [PATCH 11/18] fix tests --- packages/server/lib/modes/run.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/lib/modes/run.ts b/packages/server/lib/modes/run.ts index 934211931a90..3ffc70ed549a 100644 --- a/packages/server/lib/modes/run.ts +++ b/packages/server/lib/modes/run.ts @@ -737,7 +737,7 @@ async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, hea printResults.displaySpecHeader(spec.relativeToCommonRoot, index + 1, length, estimated) } - const { results } = await runSpec(config, spec, options, estimated, isFirstSpec, index === length - 1, protocolManager) + const { results } = await runSpec(config, spec, options, estimated, isFirstSpec, index === length - 1) isFirstSpec = false @@ -837,7 +837,7 @@ async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, hea return results } -async function runSpec (config, spec: SpecWithRelativeRoot, options: { project: Project, browser: Browser, onError: (err: Error) => void, config: Cfg, quiet: boolean, exit: boolean, testingType: TestingType, socketId: string, webSecurity: boolean, projectRoot: string } & Pick, estimated, isFirstSpec, isLastSpec, protocolManager) { +async function runSpec (config, spec: SpecWithRelativeRoot, options: { project: Project, browser: Browser, onError: (err: Error) => void, config: Cfg, quiet: boolean, exit: boolean, testingType: TestingType, socketId: string, webSecurity: boolean, projectRoot: string, protocolManager?: ProtocolManager } & Pick, estimated, isFirstSpec, isLastSpec) { const { project, browser, onError } = options const { isHeadless } = browser @@ -904,7 +904,7 @@ async function runSpec (config, spec: SpecWithRelativeRoot, options: { project: isFirstSpec, experimentalSingleTabRunMode: config.experimentalSingleTabRunMode, shouldLaunchNewTab: !isFirstSpec, - protocolManager, + protocolManager: options.protocolManager, }), ]) From 4f73de7d7c4162e9cd43dbff03b3fdb1e860f10f Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 10 Mar 2023 14:45:48 -0600 Subject: [PATCH 12/18] pr comment --- packages/server/lib/cloud/api.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts index 4ed583ebab8e..11e9e90b5bb9 100644 --- a/packages/server/lib/cloud/api.ts +++ b/packages/server/lib/cloud/api.ts @@ -18,6 +18,7 @@ import * as enc from './encryption' import getEnvInformationForProjectRoot from './environment' import type { OptionsWithUrl } from 'request-promise' +import type { ProtocolManager } from '@packages/types' const THIRTY_SECONDS = humanInterval('30 seconds') const SIXTY_SECONDS = humanInterval('60 seconds') const TWO_MINUTES = humanInterval('2 minutes') @@ -241,7 +242,7 @@ export type CreateRunOptions = { tags: string[] testingType: 'e2e' | 'component' timeout?: number - protocolManager?: any + protocolManager?: ProtocolManager } let preflightResult = { From fcb6c6f9833d27a6958eb0a8dbde8449fabc6447 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 10 Mar 2023 15:27:17 -0600 Subject: [PATCH 13/18] pr comments --- packages/server/lib/cloud/protocol.ts | 12 +++++++----- packages/server/lib/modes/run.ts | 8 ++++---- packages/types/src/protocol.ts | 8 +++++--- packages/types/src/server.ts | 4 ++-- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/server/lib/cloud/protocol.ts b/packages/server/lib/cloud/protocol.ts index 401a57e1ba89..12af7cc6d683 100644 --- a/packages/server/lib/cloud/protocol.ts +++ b/packages/server/lib/cloud/protocol.ts @@ -2,14 +2,16 @@ import fs from 'fs-extra' import { NodeVM } from 'vm2' import Debug from 'debug' import CDP from 'chrome-remote-interface' -import type { ProtocolManager } from '@packages/types' +import type { ProtocolManager, SpecFile } from '@packages/types' + +// TODO: This is basic for now but will evolve as we progress with the protocol work interface AppCaptureProtocolInterface { setupProtocol (url?: string): Promise - connectToBrowser (options: any): void - beforeSpec (spec: any): void + connectToBrowser (options: { target: string, host: number, port: number }): void + beforeSpec (spec: SpecFile & { instanceId: string }): void afterSpec (): void - beforeTest (test: any): void + beforeTest (test: { id: string, title: string, wallClockStartedAt: number }): void } const debug = Debug('cypress:server:protocol') @@ -46,7 +48,7 @@ class ProtocolManagerImpl implements ProtocolManager { } async setupProtocol (url?: string) { - debug('setting up protocol') + debug('setting up protocol via url %s', url) this.protocol = await setupProtocol(url) } diff --git a/packages/server/lib/modes/run.ts b/packages/server/lib/modes/run.ts index 3ffc70ed549a..5cd8a07d94e8 100644 --- a/packages/server/lib/modes/run.ts +++ b/packages/server/lib/modes/run.ts @@ -56,7 +56,7 @@ const relativeSpecPattern = (projectRoot, pattern) => { return pattern.map((x) => x.replace(`${projectRoot}/`, '')) } -const iterateThroughSpecs = function (options: { specs: SpecFile[], runEachSpec: RunEachSpec, beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, config: Cfg, protocolManager?: any }) { +const iterateThroughSpecs = function (options: { specs: SpecFile[], runEachSpec: RunEachSpec, beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, config: Cfg, protocolManager?: ProtocolManager }) { const { specs, runEachSpec, beforeSpecRun, afterSpecRun, config, protocolManager } = options const serial = () => { @@ -358,7 +358,7 @@ async function postProcessRecording (options: { quiet: boolean, videoCompression return continueProcessing(onProgress) } -function launchBrowser (options: { browser: Browser, spec: SpecWithRelativeRoot, setScreenshotMetadata: SetScreenshotMetadata, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, onError: (err: Error) => void, videoRecording?: VideoRecording, protocolManager?: any }) { +function launchBrowser (options: { browser: Browser, spec: SpecWithRelativeRoot, setScreenshotMetadata: SetScreenshotMetadata, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, onError: (err: Error) => void, videoRecording?: VideoRecording, protocolManager?: ProtocolManager }) { const { browser, spec, setScreenshotMetadata, screenshots, projectRoot, shouldLaunchNewTab, onError, protocolManager } = options const warnings = {} @@ -457,7 +457,7 @@ function listenForProjectEnd (project, exit): Bluebird { }) } -async function waitForBrowserToConnect (options: { project: Project, socketId: string, onError: (err: Error) => void, spec: SpecWithRelativeRoot, isFirstSpec: boolean, testingType: string, experimentalSingleTabRunMode: boolean, browser: Browser, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, webSecurity: boolean, videoRecording?: VideoRecording, protocolManager?: any }) { +async function waitForBrowserToConnect (options: { project: Project, socketId: string, onError: (err: Error) => void, spec: SpecWithRelativeRoot, isFirstSpec: boolean, testingType: string, experimentalSingleTabRunMode: boolean, browser: Browser, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, webSecurity: boolean, videoRecording?: VideoRecording, protocolManager?: ProtocolManager }) { if (globalThis.CY_TEST_MOCK?.waitForBrowserToConnect) return Promise.resolve() const { project, socketId, onError, spec } = options @@ -706,7 +706,7 @@ function screenshotMetadata (data, resp) { } } -async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, headed: boolean, outputPath: string, specs: SpecWithRelativeRoot[], specPattern: string | RegExp | string[], beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, runUrl?: string, parallel?: boolean, group?: string, tag?: string, autoCancelAfterFailures?: number | false, testingType: TestingType, quiet: boolean, project: Project, onError: (err: Error) => void, exit: boolean, socketId: string, webSecurity: boolean, projectRoot: string, protocolManager?: any } & Pick) { +async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, headed: boolean, outputPath: string, specs: SpecWithRelativeRoot[], specPattern: string | RegExp | string[], beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, runUrl?: string, parallel?: boolean, group?: string, tag?: string, autoCancelAfterFailures?: number | false, testingType: TestingType, quiet: boolean, project: Project, onError: (err: Error) => void, exit: boolean, socketId: string, webSecurity: boolean, projectRoot: string, protocolManager?: ProtocolManager } & Pick) { if (globalThis.CY_TEST_MOCK?.runSpecs) return globalThis.CY_TEST_MOCK.runSpecs const { config, browser, sys, headed, outputPath, specs, specPattern, beforeSpecRun, afterSpecRun, runUrl, parallel, group, tag, autoCancelAfterFailures, protocolManager } = options diff --git a/packages/types/src/protocol.ts b/packages/types/src/protocol.ts index 39ac5a6ba019..96cb19ae5f49 100644 --- a/packages/types/src/protocol.ts +++ b/packages/types/src/protocol.ts @@ -1,10 +1,12 @@ +import type { SpecFile } from '.' + // TODO: This is basic for now but will evolve as we progress with the protocol work export interface ProtocolManager { setupProtocol(url?: string): Promise protocolEnabled(): boolean - connectToBrowser(options: any): void - beforeSpec(spec: any): void + connectToBrowser(options: { target: string, host: number, port: number }): void + beforeSpec(spec: SpecFile & { instanceId: string }): void afterSpec(): void - beforeTest(test: any): void + beforeTest(test: { id: string, title: string, wallClockStartedAt: number }): void } diff --git a/packages/types/src/server.ts b/packages/types/src/server.ts index fc7740e89882..3b3e7bab1148 100644 --- a/packages/types/src/server.ts +++ b/packages/types/src/server.ts @@ -11,7 +11,7 @@ export type OpenProjectLaunchOpts = { videoApi?: RunModeVideoApi onWarning: (err: Error) => void onError: (err: Error) => void - protocolManager?: any + protocolManager?: ProtocolManager } export type BrowserLaunchOpts = { @@ -23,7 +23,7 @@ export type BrowserLaunchOpts = { onBrowserClose?: (...args: unknown[]) => void onBrowserOpen?: (...args: unknown[]) => void relaunchBrowser?: () => Promise - protocolManager?: any + protocolManager?: ProtocolManager } & Partial // TODO: remove the `Partial` here by making it impossible for openProject.launch to be called w/o OpenProjectLaunchOpts & Pick From e80b891d109b74fc302b1224a127bd082ee3540c Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 10 Mar 2023 16:40:49 -0600 Subject: [PATCH 14/18] Refactoring and brief documentation --- guides/protocol-development.md | 11 +++++++++++ packages/server/lib/cloud/protocol.ts | 14 +++----------- packages/types/src/protocol.ts | 13 ++++++++----- 3 files changed, 22 insertions(+), 16 deletions(-) create mode 100644 guides/protocol-development.md diff --git a/guides/protocol-development.md b/guides/protocol-development.md new file mode 100644 index 000000000000..9febe1c5a1e7 --- /dev/null +++ b/guides/protocol-development.md @@ -0,0 +1,11 @@ +# Protocol Development + +Under normal circumstances, we retrieve from the cloud the code that is used to capture and communicate test data. In order to develop the code locally, developers will: + +* Clone the `cypress-services` repo + * Run `yarn` + * Run `yarn watch` in `packages/app-capture-protocol` +* Set `CYPRESS_LOCAL_PROTOCOL_PATH` to the path to `cypress-services/packages/app-capture-protocol/dist/index.js` +* Clone the `cypress` repo + * Run `yarn` + * Execute `cypress run` on a project in record mode diff --git a/packages/server/lib/cloud/protocol.ts b/packages/server/lib/cloud/protocol.ts index 12af7cc6d683..b62479918ba0 100644 --- a/packages/server/lib/cloud/protocol.ts +++ b/packages/server/lib/cloud/protocol.ts @@ -2,17 +2,9 @@ import fs from 'fs-extra' import { NodeVM } from 'vm2' import Debug from 'debug' import CDP from 'chrome-remote-interface' -import type { ProtocolManager, SpecFile } from '@packages/types' +import type { ProtocolManager, AppCaptureProtocolInterface } from '@packages/types' -// TODO: This is basic for now but will evolve as we progress with the protocol work - -interface AppCaptureProtocolInterface { - setupProtocol (url?: string): Promise - connectToBrowser (options: { target: string, host: number, port: number }): void - beforeSpec (spec: SpecFile & { instanceId: string }): void - afterSpec (): void - beforeTest (test: { id: string, title: string, wallClockStartedAt: number }): void -} +// TODO: This is basic for now but will evolve as we progress with the protocol wor const debug = Debug('cypress:server:protocol') @@ -59,7 +51,7 @@ class ProtocolManagerImpl implements ProtocolManager { } beforeSpec (spec) { - debug('initializing new spec %O', spec.relative) + debug('initializing new spec %O', spec) this.protocol?.beforeSpec(spec) // Initialize DB here diff --git a/packages/types/src/protocol.ts b/packages/types/src/protocol.ts index 96cb19ae5f49..c0eede1ba0bc 100644 --- a/packages/types/src/protocol.ts +++ b/packages/types/src/protocol.ts @@ -2,11 +2,14 @@ import type { SpecFile } from '.' // TODO: This is basic for now but will evolve as we progress with the protocol work -export interface ProtocolManager { +export interface AppCaptureProtocolInterface { + connectToBrowser (options: { target: string, host: string, port: number }): void + beforeSpec (spec: SpecFile & { instanceId: string }): void + afterSpec (): void + beforeTest (test: { id: string, title: string, wallClockStartedAt: number }): void +} + +export interface ProtocolManager extends AppCaptureProtocolInterface { setupProtocol(url?: string): Promise protocolEnabled(): boolean - connectToBrowser(options: { target: string, host: number, port: number }): void - beforeSpec(spec: SpecFile & { instanceId: string }): void - afterSpec(): void - beforeTest(test: { id: string, title: string, wallClockStartedAt: number }): void } From f3a9ebda8eae659ced40a0386d8d3ede368a14d0 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 13 Mar 2023 08:44:57 -0500 Subject: [PATCH 15/18] Apply suggestions from code review Co-authored-by: Matt Schile --- guides/protocol-development.md | 5 ++--- packages/types/src/server.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/guides/protocol-development.md b/guides/protocol-development.md index 9febe1c5a1e7..db5d088935ee 100644 --- a/guides/protocol-development.md +++ b/guides/protocol-development.md @@ -1,11 +1,10 @@ # Protocol Development -Under normal circumstances, we retrieve from the cloud the code that is used to capture and communicate test data. In order to develop the code locally, developers will: +In production, the capture code used to capture and communicate test data will be retrieved from the Cloud. However, in order to develop the capture code locally, developers will: * Clone the `cypress-services` repo * Run `yarn` * Run `yarn watch` in `packages/app-capture-protocol` -* Set `CYPRESS_LOCAL_PROTOCOL_PATH` to the path to `cypress-services/packages/app-capture-protocol/dist/index.js` * Clone the `cypress` repo * Run `yarn` - * Execute `cypress run` on a project in record mode + * Execute `CYPRESS_LOCAL_PROTOCOL_PATH=path/to/cypress-services/packages/app-capture-protocol/dist/index.js CYPRESS_INTERNAL_ENV=staging yarn cypress:run --record --key --project ` on a project in record mode diff --git a/packages/types/src/server.ts b/packages/types/src/server.ts index 3b3e7bab1148..51a33e196809 100644 --- a/packages/types/src/server.ts +++ b/packages/types/src/server.ts @@ -93,7 +93,7 @@ export interface OpenProjectLaunchOptions { onChange?: WebSocketOptionsCallback onError?: (err: Error) => void - // Manager used to communicate Cypress lifecycle events to the protocol + // Manager used to communicate with the Cloud protocol protocolManager?: ProtocolManager [key: string]: any From 918fbef8751e0044927b72ed11d233002b7a3050 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 13 Mar 2023 08:44:33 -0500 Subject: [PATCH 16/18] PR comments --- packages/server/lib/cloud/api.ts | 2 +- packages/server/lib/cloud/protocol.ts | 6 +++--- packages/types/src/protocol.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/server/lib/cloud/api.ts b/packages/server/lib/cloud/api.ts index 11e9e90b5bb9..3a40e31be772 100644 --- a/packages/server/lib/cloud/api.ts +++ b/packages/server/lib/cloud/api.ts @@ -333,7 +333,7 @@ module.exports = { }) }) .then(async (result) => { - // TODO: Get url for the protocol code and pass it down to download + // TODO(protocol): Get url for the protocol code and pass it down to download await options.protocolManager?.setupProtocol() return result diff --git a/packages/server/lib/cloud/protocol.ts b/packages/server/lib/cloud/protocol.ts index b62479918ba0..3b468f5f75ee 100644 --- a/packages/server/lib/cloud/protocol.ts +++ b/packages/server/lib/cloud/protocol.ts @@ -4,18 +4,18 @@ import Debug from 'debug' import CDP from 'chrome-remote-interface' import type { ProtocolManager, AppCaptureProtocolInterface } from '@packages/types' -// TODO: This is basic for now but will evolve as we progress with the protocol wor +// TODO(protocol): This is basic for now but will evolve as we progress with the protocol wor const debug = Debug('cypress:server:protocol') const setupProtocol = async (url?: string): Promise => { let script: string | undefined - // TODO: We will need to remove this option in production + // TODO(protocol): We will need to remove this option in production if (process.env.CYPRESS_LOCAL_PROTOCOL_PATH) { script = await fs.readFile(process.env.CYPRESS_LOCAL_PROTOCOL_PATH, 'utf8') } else if (url) { - // TODO: Download the protocol script from the cloud + // TODO(protocol): Download the protocol script from the cloud } if (script) { diff --git a/packages/types/src/protocol.ts b/packages/types/src/protocol.ts index c0eede1ba0bc..be30c10d82b2 100644 --- a/packages/types/src/protocol.ts +++ b/packages/types/src/protocol.ts @@ -1,6 +1,6 @@ import type { SpecFile } from '.' -// TODO: This is basic for now but will evolve as we progress with the protocol work +// TODO(protocol): This is basic for now but will evolve as we progress with the protocol work export interface AppCaptureProtocolInterface { connectToBrowser (options: { target: string, host: string, port: number }): void From 63c4df0a781b2a883ea9cc1b39edee0e9df57f8b Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 13 Mar 2023 09:04:33 -0500 Subject: [PATCH 17/18] add a couple of basic tests --- .../server/test/unit/browsers/chrome_spec.js | 13 +++++++++ packages/server/test/unit/cloud/api_spec.js | 28 ++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/server/test/unit/browsers/chrome_spec.js b/packages/server/test/unit/browsers/chrome_spec.js index ebf80b997c83..5952c7b5b005 100644 --- a/packages/server/test/unit/browsers/chrome_spec.js +++ b/packages/server/test/unit/browsers/chrome_spec.js @@ -541,10 +541,13 @@ describe('lib/browsers/chrome', () => { const pageCriClient = { send: sinon.stub().resolves(), on: sinon.stub(), + targetId: '1234', } const browserCriClient = { currentlyAttachedTarget: pageCriClient, + host: 'http://localhost', + port: 1234, } const automation = { @@ -555,6 +558,10 @@ describe('lib/browsers/chrome', () => { kill: sinon.stub().returns(), } + const protocolManager = { + connectToBrowser: sinon.stub().resolves(), + } + let onInitializeNewBrowserTabCalled = false const options = { ...openOpts, @@ -565,6 +572,7 @@ describe('lib/browsers/chrome', () => { onInitializeNewBrowserTab: () => { onInitializeNewBrowserTabCalled = true }, + protocolManager, } sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient) @@ -580,6 +588,11 @@ describe('lib/browsers/chrome', () => { expect(chrome._navigateUsingCRI).to.be.called expect(chrome._handleDownloads).to.be.called expect(onInitializeNewBrowserTabCalled).to.be.true + expect(protocolManager.connectToBrowser).to.be.calledWith({ + host: 'http://localhost', + port: 1234, + target: '1234', + }) }) }) diff --git a/packages/server/test/unit/cloud/api_spec.js b/packages/server/test/unit/cloud/api_spec.js index e9bdfbf0cb17..5cd5c91535b0 100644 --- a/packages/server/test/unit/cloud/api_spec.js +++ b/packages/server/test/unit/cloud/api_spec.js @@ -525,6 +525,9 @@ describe('lib/cloud/api', () => { context('.createRun', () => { beforeEach(function () { + this.protocolManager = { + setupProtocol: sinon.stub(), + }, this.buildProps = { group: null, parallel: null, @@ -563,9 +566,13 @@ describe('lib/cloud/api', () => { runId: 'new-run-id-123', }) - return api.createRun(this.buildProps) + return api.createRun({ + ...this.buildProps, + protocolManager: this.protocolManager, + }) .then((ret) => { expect(ret).to.deep.eq({ runId: 'new-run-id-123' }) + expect(this.protocolManager.setupProtocol).to.be.called }) }) @@ -595,9 +602,13 @@ describe('lib/cloud/api', () => { })) })) - return api.createRun(this.buildProps) + return api.createRun({ + ...this.buildProps, + protocolManager: this.protocolManager, + }) .then((ret) => { expect(ret).to.deep.eq({ runId: 'new-run-id-123' }) + expect(this.protocolManager.setupProtocol).to.be.called }) }) @@ -613,7 +624,10 @@ describe('lib/cloud/api', () => { }, }) - return api.createRun(this.buildProps) + return api.createRun({ + ...this.buildProps, + protocolManager: this.protocolManager, + }) .then(() => { throw new Error('should have thrown here') }).catch((err) => { @@ -628,6 +642,8 @@ describe('lib/cloud/api', () => { } }\ `) + + expect(this.protocolManager.setupProtocol).not.to.be.called }) }) @@ -667,11 +683,15 @@ describe('lib/cloud/api', () => { .post('/runs', this.buildProps) .reply(500, {}) - return api.createRun(this.buildProps) + return api.createRun({ + ...this.buildProps, + protocolManager: this.protocolManager, + }) .then(() => { throw new Error('should have thrown here') }).catch((err) => { expect(err.isApiError).to.be.true + expect(this.protocolManager.setupProtocol).not.to.be.called }) }) From 8f974cb0e74178c34ef8c4f4b673a8ce6b8d6e87 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 13 Mar 2023 11:07:43 -0500 Subject: [PATCH 18/18] empty commit