diff --git a/packages/launcher/lib/browsers.ts b/packages/launcher/lib/browsers.ts index 821cda74a7b4..e7b9b466edd4 100644 --- a/packages/launcher/lib/browsers.ts +++ b/packages/launcher/lib/browsers.ts @@ -97,6 +97,7 @@ export function launch ( browser: FoundBrowser, url: string, args: string[] = [], + opts: { pipeStdio?: boolean } = {}, ) { log('launching browser %o', { browser, url }) @@ -110,7 +111,15 @@ export function launch ( log('spawning browser with args %o', { args }) - const proc = cp.spawn(browser.path, args, { stdio: ['ignore', 'pipe', 'pipe'] }) + const stdio = ['ignore', 'pipe', 'pipe'] + + if (opts.pipeStdio) { + // also pipe stdio 3 and 4 for access to debugger protocol + stdio.push('pipe', 'pipe') + } + + // @ts-ignore + const proc = cp.spawn(browser.path, args, { stdio }) proc.stdout.on('data', (buf) => { log('%s stdout: %s', browser.name, String(buf).trim()) diff --git a/packages/server/__snapshots__/5_cdp_spec.ts.js b/packages/server/__snapshots__/5_cdp_spec.ts.js index 6c378afb74dc..82cd998b06f9 100644 --- a/packages/server/__snapshots__/5_cdp_spec.ts.js +++ b/packages/server/__snapshots__/5_cdp_spec.ts.js @@ -1,4 +1,4 @@ -exports['e2e cdp / handles disconnections as expected'] = ` +exports['e2e cdp / with TCP transport / handles disconnections as expected'] = ` ==================================================================================================== @@ -59,4 +59,67 @@ Error: connect ECONNREFUSED 127.0.0.1:7777 ✖ 1 of 1 failed (100%) XX:XX - - 1 - - -` \ No newline at end of file +` + +exports['e2e cdp / with stdio transport / falls back to connecting via tcp when stdio cannot be connected'] = ` + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (spec.ts) │ + │ Searched: cypress/integration/spec.ts │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: spec.ts (1 of 1) +Warning: Cypress failed to connect to Chrome via stdio after 1 second. Falling back to TCP... +Connecting to Chrome via TCP was successful, continuing with tests. + + + passes + ✓ passes + + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: spec.ts │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /XXX/XXX/XXX/cypress/videos/spec.ts.mp4 (X second) + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ spec.ts XX:XX 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✔ All specs passed! XX:XX 1 1 - - - + + +` diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index d4ad82ff5456..21aed30da4e0 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -13,6 +13,7 @@ import * as CriClient from './cri-client' import * as protocol from './protocol' import utils from './utils' import { Browser } from './types' +import errors from '../errors' // TODO: this is defined in `cypress-npm-api` but there is currently no way to get there type CypressConfiguration = any @@ -35,6 +36,9 @@ type ChromePreferences = { localState: object } +const staticCdpPort = Number(process.env.CYPRESS_REMOTE_DEBUGGING_PORT) +const stdioTimeoutMs = Number(process.env.CYPRESS_CDP_TARGET_TIMEOUT) || 60000 + const pathToExtension = extension.getPathToExtension() const pathToTheme = extension.getPathToTheme() @@ -173,9 +177,7 @@ const _writeChromePreferences = (userDir: string, originalPrefs: ChromePreferenc } const getRemoteDebuggingPort = async () => { - const port = Number(process.env.CYPRESS_REMOTE_DEBUGGING_PORT) - - return port || utils.getPort() + return staticCdpPort || utils.getPort() } /** @@ -243,19 +245,47 @@ const _disableRestorePagesPrompt = function (userDir) { .catch(() => { }) } +const useStdioCdp = (browser) => { + return ( + // CDP via stdio doesn't seem to work in browsers older than 72 + // @see https://github.com/cyrus-and/chrome-remote-interface/issues/381#issuecomment-445277683 + browser.majorVersion >= 72 + // allow users to force TCP by specifying a port in environment + && !staticCdpPort + ) +} + // After the browser has been opened, we can connect to // its remote interface via a websocket. -const _connectToChromeRemoteInterface = function (port, onError) { - // @ts-ignore - la(check.userPort(port), 'expected port number to connect CRI to', port) +const _connectToChromeRemoteInterface = function (browser, process, port, onError) { + const connectTcp = () => { + // @ts-ignore + la(check.userPort(port), 'expected port number to connect CRI to', port) + + debug('connecting to Chrome remote interface at random port %d', port) + + return protocol.getWsTargetFor(port) + .then((wsUrl) => { + debug('received wsUrl %s for port %d', wsUrl, port) + + return CriClient.create({ target: wsUrl }, onError) + }) + } + + if (!useStdioCdp(browser)) { + return connectTcp() + } + + return CriClient.create({ process }, onError) + .timeout(stdioTimeoutMs) + .catch(Bluebird.TimeoutError, async () => { + errors.warning('CDP_STDIO_TIMEOUT', browser.displayName, stdioTimeoutMs) - debug('connecting to Chrome remote interface at random port %d', port) + const client = await connectTcp() - return protocol.getWsTargetFor(port) - .then((wsUrl) => { - debug('received wsUrl %s for port %d', wsUrl, port) + errors.warning('CDP_FALLBACK_SUCCEEDED', browser.displayName) - return CriClient.create(wsUrl, onError) + return client }) } @@ -398,6 +428,10 @@ export = { args.push(`--remote-debugging-port=${port}`) args.push('--remote-debugging-address=127.0.0.1') + if (useStdioCdp(browser)) { + args.push('--remote-debugging-pipe') + } + return args }, @@ -453,14 +487,16 @@ export = { // first allows us to connect the remote interface, // start video recording and then // we will load the actual page - const launchedBrowser = await utils.launch(browser, 'about:blank', args) + const launchedBrowser = await utils.launch(browser, 'about:blank', args, { + pipeStdio: useStdioCdp(browser), + }) la(launchedBrowser, 'did not get launched browser instance') // SECOND connect to the Chrome remote interface // and when the connection is ready // navigate to the actual url - const criClient = await this._connectToChromeRemoteInterface(port, options.onError) + const criClient = await this._connectToChromeRemoteInterface(browser, launchedBrowser, port, options.onError) la(criClient, 'expected Chrome remote interface reference', criClient) diff --git a/packages/server/lib/browsers/cri-client.ts b/packages/server/lib/browsers/cri-client.ts index f776d79f4041..47546930688c 100644 --- a/packages/server/lib/browsers/cri-client.ts +++ b/packages/server/lib/browsers/cri-client.ts @@ -1,6 +1,7 @@ import Bluebird from 'bluebird' import debugModule from 'debug' import _ from 'lodash' +import { ChildProcess } from 'child_process' const chromeRemoteInterface = require('chrome-remote-interface') const errors = require('../errors') @@ -82,41 +83,40 @@ const getMajorMinorVersion = (version: string): Version => { const maybeDebugCdpMessages = (cri) => { if (debugVerboseReceive.enabled) { - cri._ws.on('message', (data) => { - data = _ - .chain(JSON.parse(data)) - .tap((data) => { - ([ - 'params.data', // screencast frame data - 'result.data', // screenshot data - ]).forEach((truncatablePath) => { - const str = _.get(data, truncatablePath) - - if (!_.isString(str)) { - return - } + const handleMessage = cri._handleMessage - _.set(data, truncatablePath, _.truncate(str, { - length: 100, - omission: `... [truncated string of total bytes: ${str.length}]`, - })) - }) + cri._handleMessage = (message) => { + const formatted = _.cloneDeep(message) + + ;([ + 'params.data', // screencast frame data + 'result.data', // screenshot data + ]).forEach((truncatablePath) => { + const str = _.get(formatted, truncatablePath) + + if (!_.isString(str)) { + return + } - return data + _.set(formatted, truncatablePath, _.truncate(str, { + length: 100, + omission: `... [truncated string of total bytes: ${str.length}]`, + })) }) - .value() - debugVerboseReceive('received CDP message %o', data) - }) + debugVerboseReceive('received CDP message %o', formatted) + + return handleMessage.call(cri, message) + } } if (debugVerboseSend.enabled) { - const send = cri._ws.send + const send = cri._send - cri._ws.send = (data, callback) => { + cri._send = (data, callback) => { debugVerboseSend('sending CDP command %o', JSON.parse(data)) - return send.call(cri._ws, data, callback) + return send.call(cri, data, callback) } } } @@ -132,17 +132,36 @@ export { chromeRemoteInterface } type DeferredPromise = { resolve: Function, reject: Function } -export const create = Bluebird.method((target: websocketUrl, onAsynchronousError: Function): Bluebird => { +type CreateOpts = { + target?: websocketUrl + process?: ChildProcess +} + +type Message = { + method: CRI.Command + params?: any + sessionId?: string +} + +export const create = Bluebird.method((opts: CreateOpts, onAsynchronousError: Function): Bluebird => { const subscriptions: {eventName: CRI.EventName, cb: Function}[] = [] - let enqueuedCommands: {command: CRI.Command, params: any, p: DeferredPromise }[] = [] + let enqueuedCommands: {message: Message, params: any, p: DeferredPromise }[] = [] let closed = false // has the user called .close on this? let connected = false // is this currently connected to CDP? let cri let client: CRIWrapper + let sessionId: string | undefined const reconnect = () => { + if (opts.process) { + // reconnecting doesn't make sense for stdio + onAsynchronousError(errors.get('CDP_STDIO_ERROR')) + + return + } + debug('disconnected, attempting to reconnect... %o', { closed }) connected = false @@ -159,7 +178,7 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError }) enqueuedCommands.forEach((cmd) => { - cri.send(cmd.command, cmd.params) + cri.sendRaw(cmd.message) .then(cmd.p.resolve, cmd.p.reject) }) @@ -173,10 +192,10 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError const connect = () => { cri?.close() - debug('connecting %o', { target }) + debug('connecting %o', opts) return chromeRemoteInterface({ - target, + ...opts, local: true, }) .then((newCri) => { @@ -190,6 +209,46 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError // @see https://github.com/cyrus-and/chrome-remote-interface/issues/72 cri._notifier.on('disconnect', reconnect) + + if (opts.process) { + // if using stdio, we need to find the target before declaring the connection complete + return findTarget() + } + + return + }) + } + + const findTarget = () => { + debug('finding CDP target...') + + return new Bluebird((resolve, reject) => { + const isAboutBlank = (target) => target.type === 'page' && target.url === 'about:blank' + + const attachToTarget = _.once(({ targetId }) => { + debug('attaching to target %o', { targetId }) + cri.send('Target.attachToTarget', { + targetId, + flatten: true, // enable selecting via sessionId + }).then((result) => { + debug('attached to target %o', result) + sessionId = result.sessionId + resolve() + }).catch(reject) + }) + + cri.send('Target.setDiscoverTargets', { discover: true }) + .then(() => { + cri.on('Target.targetCreated', (target) => { + if (isAboutBlank(target)) { + attachToTarget(target) + } + }) + + return cri.send('Target.getTargets') + .then(({ targetInfos }) => targetInfos.filter(isAboutBlank).map(attachToTarget)) + }) + .catch(reject) }) } @@ -219,14 +278,23 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError ensureMinimumProtocolVersion, getProtocolVersion, send: Bluebird.method((command: CRI.Command, params?: object) => { + const message: Message = { + method: command, + params, + } + + if (sessionId) { + message.sessionId = sessionId + } + const enqueue = () => { return new Bluebird((resolve, reject) => { - enqueuedCommands.push({ command, params, p: { resolve, reject } }) + enqueuedCommands.push({ message, params, p: { resolve, reject } }) }) } if (connected) { - return cri.send(command, params) + return cri.sendRaw(message) .catch((err) => { if (!WEBSOCKET_NOT_OPEN_RE.test(err.message)) { throw err diff --git a/packages/server/lib/errors.js b/packages/server/lib/errors.js index 0e7f68829341..63667bed410c 100644 --- a/packages/server/lib/errors.js +++ b/packages/server/lib/errors.js @@ -5,6 +5,7 @@ const chalk = require('chalk') const AU = require('ansi_up') const Promise = require('bluebird') const { stripIndent } = require('./util/strip_indent') +const humanTime = require('./util/human_time') const ansi_up = new AU.default @@ -851,6 +852,12 @@ const getMsgByType = function (type, arg1 = {}, arg2, arg3) { There was an error reconnecting to the Chrome DevTools protocol. Please restart the browser. ${arg1.stack}` + case 'CDP_STDIO_ERROR': + return 'The connection between Cypress and Chrome has unexpectedly ended. Please restart the browser.' + case 'CDP_STDIO_TIMEOUT': + return `Warning: Cypress failed to connect to ${arg1} via stdio after ${humanTime.long(arg2)}. Falling back to TCP...` + case 'CDP_FALLBACK_SUCCEEDED': + return `Connecting to ${arg1} via TCP was successful, continuing with tests.` case 'CDP_RETRYING_CONNECTION': return `Failed to connect to Chrome, retrying in 1 second (attempt ${chalk.yellow(arg1)}/62)` case 'DEPRECATED_BEFORE_BROWSER_LAUNCH_ARGS': diff --git a/packages/server/package.json b/packages/server/package.json index 79444d9b5a94..048c044a2e66 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -37,7 +37,7 @@ "chalk": "2.4.2", "check-more-types": "2.24.0", "chokidar": "3.2.2", - "chrome-remote-interface": "0.28.2", + "chrome-remote-interface": "cypress-io/chrome-remote-interface#147192810f29951cd96c5e406495e9b4d740ba95", "cli-table3": "0.5.1", "coffeescript": "1.12.7", "color-string": "1.5.4", diff --git a/packages/server/test/e2e/5_cdp_spec.ts b/packages/server/test/e2e/5_cdp_spec.ts index c80af4680e13..677c7d744ea6 100644 --- a/packages/server/test/e2e/5_cdp_spec.ts +++ b/packages/server/test/e2e/5_cdp_spec.ts @@ -4,36 +4,61 @@ import Fixtures from '../support/helpers/fixtures' describe('e2e cdp', function () { e2e.setup() - let restoreEnv: Function - beforeEach(() => { - restoreEnv = mockedEnv({ - CYPRESS_REMOTE_DEBUGGING_PORT: '7777', + context('with TCP transport', function () { + let restoreEnv: Function + + beforeEach(() => { + restoreEnv = mockedEnv({ + CYPRESS_REMOTE_DEBUGGING_PORT: '7777', + }) }) - }) - afterEach(() => { - restoreEnv() - }) + afterEach(() => { + restoreEnv() + }) + + // NOTE: this test takes almost a minute and is largely redundant with protocol_spec + e2e.it.skip('fails when remote debugging port cannot be connected to', { + project: Fixtures.projectPath('remote-debugging-port-removed'), + spec: 'spec.ts', + browser: 'chrome', + expectedExitCode: 1, + }) - // NOTE: this test takes almost a minute and is largely redundant with protocol_spec - e2e.it.skip('fails when remote debugging port cannot be connected to', { - project: Fixtures.projectPath('remote-debugging-port-removed'), - spec: 'spec.ts', - browser: 'chrome', - expectedExitCode: 1, + // https://github.com/cypress-io/cypress/issues/5685 + e2e.it('handles disconnections as expected', { + project: Fixtures.projectPath('remote-debugging-disconnect'), + spec: 'spec.ts', + browser: 'chrome', + expectedExitCode: 1, + snapshot: true, + onStdout: (stdout) => { + // the location of this warning is non-deterministic + return stdout.replace('The automation client disconnected. Cannot continue running tests.\n', '') + }, + }) }) - // https://github.com/cypress-io/cypress/issues/5685 - e2e.it('handles disconnections as expected', { - project: Fixtures.projectPath('remote-debugging-disconnect'), - spec: 'spec.ts', - browser: 'chrome', - expectedExitCode: 1, - snapshot: true, - onStdout: (stdout) => { - // the location of this warning is non-deterministic - return stdout.replace('The automation client disconnected. Cannot continue running tests.\n', '') - }, + // @see https://github.com/cypress-io/cypress/pull/14348 + context('with stdio transport', function () { + e2e.it('can run tests in chrome even with remote-debugging-port omitted', { + project: Fixtures.projectPath('remote-debugging-port-removed'), + spec: 'spec.ts', + browser: 'chrome', + expectedExitCode: 0, + }) + + e2e.it('falls back to connecting via tcp when stdio cannot be connected', { + project: Fixtures.projectPath('remote-debugging-port-removed'), + processEnv: { + CY_REMOVE_PIPE: '1', + CYPRESS_CDP_TARGET_TIMEOUT: '1000', + }, + spec: 'spec.ts', + browser: 'chrome', + expectedExitCode: 0, + snapshot: true, + }) }) }) diff --git a/packages/server/test/support/fixtures/projects/remote-debugging-port-removed/cypress/plugins.js b/packages/server/test/support/fixtures/projects/remote-debugging-port-removed/cypress/plugins.js index bcc725e90a00..244534b11503 100644 --- a/packages/server/test/support/fixtures/projects/remote-debugging-port-removed/cypress/plugins.js +++ b/packages/server/test/support/fixtures/projects/remote-debugging-port-removed/cypress/plugins.js @@ -4,8 +4,10 @@ module.exports = (on) => { on('before:browser:launch', (browser = {}, options) => { la(browser.family === 'chromium', 'this test can only be run with a chromium-family browser') - // remove debugging port so that the browser connection fails - const newArgs = options.args.filter((arg) => !arg.startsWith('--remote-debugging-port=')) + const cdpArg = process.env.CY_REMOVE_PIPE ? '--remote-debugging-pipe' : '--remote-debugging-port' + + // remove debugging pipe or port so that the browser connection fails + const newArgs = options.args.filter((arg) => !arg.startsWith(cdpArg)) la(newArgs.length === options.args.length - 1, 'exactly one argument should have been removed') diff --git a/packages/server/test/unit/browsers/cri-client_spec.ts b/packages/server/test/unit/browsers/cri-client_spec.ts index eeb45bcfb520..c584d85b7a4e 100644 --- a/packages/server/test/unit/browsers/cri-client_spec.ts +++ b/packages/server/test/unit/browsers/cri-client_spec.ts @@ -11,32 +11,39 @@ describe('lib/browsers/cri-client', function () { create: typeof create } let send: sinon.SinonStub + let sendRaw: sinon.SinonStub + let criStub: any let criImport: sinon.SinonStub let onError: sinon.SinonStub - let getClient: () => ReturnType + let getClient: (opts?: any) => ReturnType beforeEach(function () { sinon.stub(Bluebird, 'promisify').returnsArg(0) send = sinon.stub() + sendRaw = sinon.stub() onError = sinon.stub() + criStub = { + send, + sendRaw, + on: sinon.stub(), + close: sinon.stub(), + _notifier: new EventEmitter(), + } + criImport = sinon.stub() .withArgs({ target: DEBUGGER_URL, local: true, }) - .resolves({ - send, - close: sinon.stub(), - _notifier: new EventEmitter(), - }) + .resolves(criStub) criClient = proxyquire('../lib/browsers/cri-client', { 'chrome-remote-interface': criImport, }) - getClient = () => criClient.create(DEBUGGER_URL, onError) + getClient = (opts = { target: DEBUGGER_URL }) => criClient.create(opts, onError) }) context('.create', function () { @@ -46,19 +53,65 @@ describe('lib/browsers/cri-client', function () { expect(client.send).to.be.instanceOf(Function) }) + context('with process', function () { + let process: any + + beforeEach(function () { + process = { /** stubbed */} + + criImport.withArgs({ + process, + local: true, + }) + .resolves(criStub) + }) + + it('finds and attaches to target and persists sessionId', async function () { + const target = { + targetId: 'good', + type: 'page', + url: 'about:blank', + } + + const otherTarget = { + targetId: 'bad', + } + + send + .withArgs('Target.setDiscoverTargets').resolves() + .withArgs('Target.getTargets').resolves({ targetInfos: [otherTarget, target] }) + .withArgs('Target.attachToTarget', { targetId: 'good', flatten: true }).resolves({ sessionId: 'session-1' }) + + sendRaw.resolves() + + const client = await getClient({ process }) + + await client.send('Browser.getVersion') + + expect(sendRaw).to.be.calledWith({ + method: 'Browser.getVersion', + params: undefined, + sessionId: 'session-1', + }) + }) + }) + context('#send', function () { - it('calls cri.send with command and data', async function () { - send.resolves() + it('calls cri.sendRaw with command and data', async function () { + sendRaw.resolves() const client = await getClient() client.send('Browser.getVersion', { baz: 'quux' }) - expect(send).to.be.calledWith('Browser.getVersion', { baz: 'quux' }) + expect(sendRaw).to.be.calledWith({ + method: 'Browser.getVersion', + params: { baz: 'quux' }, + }) }) - it('rejects if cri.send rejects', async function () { + it('rejects if cri.sendRaw rejects', async function () { const err = new Error - send.rejects(err) + sendRaw.rejects(err) const client = await getClient() await expect(client.send('Browser.getVersion', { baz: 'quux' })) @@ -74,14 +127,14 @@ describe('lib/browsers/cri-client', function () { it(`with '${msg}'`, async function () { const err = new Error(msg) - send.onFirstCall().rejects(err) - send.onSecondCall().resolves() + sendRaw.onFirstCall().rejects(err) + sendRaw.onSecondCall().resolves() const client = await getClient() await client.send('Browser.getVersion', { baz: 'quux' }) - expect(send).to.be.calledTwice + expect(sendRaw).to.be.calledTwice }) }) }) @@ -90,7 +143,10 @@ describe('lib/browsers/cri-client', function () { context('#ensureMinimumProtocolVersion', function () { function withProtocolVersion (actual, test) { if (actual) { - send.withArgs('Browser.getVersion') + sendRaw.withArgs({ + method: 'Browser.getVersion', + params: undefined, + }) .resolves({ protocolVersion: actual }) } diff --git a/yarn.lock b/yarn.lock index 904530ec88e8..b766673032ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10366,14 +10366,6 @@ chrome-har-capturer@0.13.4: chrome-remote-interface "^0.25.7" commander "2.x.x" -chrome-remote-interface@0.28.2: - version "0.28.2" - resolved "https://registry.yarnpkg.com/chrome-remote-interface/-/chrome-remote-interface-0.28.2.tgz#6be3554d2c227ff07eb74baa7e5d4911da12a5a6" - integrity sha512-F7mjof7rWvRNsJqhVXuiFU/HWySCxTA9tzpLxUJxVfdLkljwFJ1aMp08AnwXRmmP7r12/doTDOMwaNhFCJsacw== - dependencies: - commander "2.11.x" - ws "^7.2.0" - chrome-remote-interface@^0.25.7: version "0.25.7" resolved "https://registry.yarnpkg.com/chrome-remote-interface/-/chrome-remote-interface-0.25.7.tgz#827e85fbef3cc561a9ef2404eb7eee355968c5bc" @@ -10382,6 +10374,13 @@ chrome-remote-interface@^0.25.7: commander "2.11.x" ws "3.3.x" +chrome-remote-interface@cypress-io/chrome-remote-interface#147192810f29951cd96c5e406495e9b4d740ba95: + version "0.28.2" + resolved "https://codeload.github.com/cypress-io/chrome-remote-interface/tar.gz/147192810f29951cd96c5e406495e9b4d740ba95" + dependencies: + commander "2.11.x" + ws "^7.2.0" + chrome-trace-event@^1.0.0, chrome-trace-event@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"