Merge branch 'develop' into 66e8896b66-master-into-develop

This commit is contained in:
Barthélémy Ledoux
2021-08-12 10:13:33 -05:00
committed by GitHub
39 changed files with 1609 additions and 908 deletions

View File

@@ -1 +1 @@
14.16.0
14.17.0

View File

@@ -8,7 +8,7 @@ branches:
# https://www.appveyor.com/docs/lang/nodejs-iojs/
environment:
# use matching version of Node.js
nodejs_version: "14.16.0"
nodejs_version: "14.17.0"
# encode secure variables which will NOT be used
# in pull requests
# https://www.appveyor.com/docs/build-configuration/#secure-variables

View File

@@ -1,4 +1,4 @@
{
"chrome:beta": "93.0.4577.18",
"chrome:stable": "92.0.4515.107"
"chrome:beta": "93.0.4577.25",
"chrome:stable": "92.0.4515.131"
}

View File

@@ -49,7 +49,7 @@ executors:
# the Docker image with Cypress dependencies and Chrome browser
cy-doc:
docker:
- image: cypress/browsers:node14.16.0-chrome90-ff88
- image: cypress/browsers:node14.17.0-chrome91-ff89
# by default, we use "small" to save on CI costs. bump on a per-job basis if needed.
resource_class: small
environment:
@@ -58,7 +58,7 @@ executors:
# Docker image with non-root "node" user
non-root-docker-user:
docker:
- image: cypress/browsers:node14.16.0-chrome90-ff88
- image: cypress/browsers:node14.17.0-chrome91-ff89
user: node
environment:
PLATFORM: linux
@@ -1126,6 +1126,7 @@ jobs:
runner-integration-tests-electron:
<<: *defaults
resource_class: medium
parallelism: 2
steps:
- run-runner-integration-tests:

View File

@@ -196,7 +196,7 @@
"yarn-deduplicate": "3.1.0"
},
"engines": {
"node": ">=14.16.0",
"node": ">=14.17.0",
"yarn": ">=1.17.3"
},
"productName": "Cypress",

View File

@@ -2127,6 +2127,61 @@ describe('network stubbing', { retries: 2 }, function () {
cy.get('#request').click()
cy.get('#result').should('contain', 'client')
})
it('works with reply', () => {
cy.intercept({
method: 'POST',
times: 1,
url: '/post-only',
},
(req) => {
req.reply('stubbed data')
}).as('interceptor')
cy.visit('fixtures/request.html')
cy.get('#request').click()
cy.get('#result').should('contain', 'stubbed data')
cy.get('#request').click()
cy.get('#result').should('contain', 'client')
})
it('works with reply and fallthrough', () => {
let times = 0
cy.intercept({
method: 'POST',
times: 3,
url: '/post-only',
},
(req) => {
req.reply(`${req.body === 'foo' ? 'foo' : 'nothing'} stubbed data ${times++}`)
})
cy.intercept({
method: 'POST',
times: 2,
url: '/post-only',
},
(req) => {
req.body = 'foo'
})
cy.visit('fixtures/request.html')
cy.get('#request').click()
cy.get('#result').should('contain', 'foo stubbed data 0')
cy.get('#request').click()
cy.get('#result').should('contain', 'foo stubbed data 1')
cy.get('#request').click()
cy.get('#result').should('contain', 'nothing stubbed data 2')
cy.get('#request').click()
cy.get('#result').should('contain', 'client')
})
})
})
})
@@ -2628,6 +2683,31 @@ describe('network stubbing', { retries: 2 }, function () {
.wait('@get')
})
// https://github.com/cypress-io/cypress/issues/17084
it('does not overwrite the json-related content-type header', () => {
cy.intercept('/json-content-type', (req) => {
req.on('response', (res) => {
res.send({
statusCode: 500,
headers: {
'content-type': 'application/problem+json',
'access-control-allow-origin': '*',
},
body: {
status: 500,
title: 'Internal Server Error',
},
})
})
})
.then(() => {
return fetch('/json-content-type')
.then((res) => {
expect(res.headers.get('content-type')).to.eq('application/problem+json')
})
})
})
context('body parsing', function () {
[
'application/json',

View File

@@ -735,7 +735,7 @@ describe('src/cy/commands/waiting', () => {
cy.wait('@foo', '@bar')
})
it('throws when passed caallback function', (done) => {
it('throws when passed callback function', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.eq('`cy.wait()` was passed invalid arguments. You cannot pass a function. If you would like to wait on the result of a `cy.wait()`, use `cy.then()`.')
expect(err.docsUrl).to.eq('https://on.cypress.io/wait')

View File

@@ -0,0 +1,196 @@
import _ from 'lodash'
import $Command from '../../../src/cypress/command'
import { create } from '../../../src/cypress/command_queue'
const createCommand = (props = {}) => {
return $Command.create(_.extend({
name: 'get',
args: ['#foo'],
type: 'parent',
chainerId: _.uniqueId('ch'),
userInvocationStack: '',
injected: false,
fn () {},
}, props))
}
const log = (props = {}) => {
return Cypress.log(_.extend({
name: _.uniqueId('l'),
}, props))
}
describe('src/cypress/command_queue', () => {
let queue
const state = () => {}
const timeouts = { timeout () {} }
const stability = { whenStable () {} }
const cleanup = () => {}
const fail = () => {}
const isCy = () => {}
beforeEach(() => {
queue = create(state, timeouts, stability, cleanup, fail, isCy)
queue.add(createCommand({
name: 'get',
logs: [log({ name: 'l1', alias: 'alias-1' }), log({ name: 'l2', alias: 'alias-1' })],
}))
queue.add(createCommand({
name: 'find',
logs: [log({ name: 'l3', alias: 'alias-2' })],
}))
queue.add(createCommand({
name: 'click',
logs: [log({ name: 'l4', alias: 'alias-1' }), log({ name: 'l5', alias: 'alias-3' })],
}))
})
context('#logs', () => {
it('returns a flat list of logs from the commands', () => {
const logs = queue.logs()
expect(_.invokeMap(logs, 'get', 'name')).to.eql(['l1', 'l2', 'l3', 'l4', 'l5'])
})
it('returns a filtered list of logs if filter is provided', () => {
const logs = queue.logs({ alias: 'alias-1' })
expect(_.invokeMap(logs, 'get', 'name')).to.eql(['l1', 'l2', 'l4'])
})
})
context('#get', () => {
it('returns list of commands', () => {
const commands = queue.get()
expect(_.invokeMap(commands, 'get', 'name')).to.eql(['get', 'find', 'click'])
})
})
context('#names', () => {
it('returns list of command names', () => {
const names = queue.names()
expect(names).to.eql(['get', 'find', 'click'])
})
})
context('#insert', () => {
it('inserts command into queue at index', () => {
queue.insert(1, createCommand({ name: 'eq' }))
expect(queue.names()).to.eql(['get', 'eq', 'find', 'click'])
})
it('returns the command', () => {
const command = createCommand({ name: 'eq' })
const result = queue.insert(1, command)
expect(result).to.equal(command)
})
it('resets the next and prev commands', () => {
const command = queue.insert(1, createCommand({ name: 'eq' }))
const prev = queue.at(0)
const next = queue.at(2)
expect(command.get('prev')).to.equal(prev)
expect(command.get('next')).to.equal(next)
expect(prev.get('next')).to.equal(command)
expect(next.get('prev')).to.equal(command)
})
it('works with start boundary index', () => {
const command = queue.insert(0, createCommand({ name: 'eq' }))
const next = queue.at(1)
expect(queue.names()).to.eql(['eq', 'get', 'find', 'click'])
expect(command.get('prev')).to.be.undefined
expect(command.get('next')).to.equal(next)
expect(next.get('prev')).to.equal(command)
})
it('works with end boundary index', () => {
const command = queue.insert(3, createCommand({ name: 'eq' }))
const prev = queue.at(2)
expect(queue.names()).to.eql(['get', 'find', 'click', 'eq'])
expect(command.get('prev')).to.equal(prev)
expect(command.get('next')).to.be.undefined
expect(prev.get('next')).to.equal(command)
})
})
context('#slice', () => {
it('returns commands from the index', () => {
const commands = queue.slice(1)
expect(_.invokeMap(commands, 'get', 'name')).to.eql(['find', 'click'])
})
})
context('#at', () => {
it('returns command at index', () => {
const command = queue.at(1)
expect(command.get('name')).to.equal('find')
})
})
context('#find', () => {
it('returns command that matches attributes', () => {
const command = queue.find({ name: 'click' })
expect(command.get('name')).to.equal('click')
})
})
context('#reset', () => {
it('resets the queue stopped state', () => {
queue.reset()
expect(queue.stopped).to.be.false
})
})
context('#clear', () => {
it('removes all commands from queue', () => {
queue.stop()
queue.clear()
expect(queue.get().length).to.equal(0)
})
})
context('#stop', () => {
it('stops the queue', () => {
queue.stop()
expect(queue.stopped).to.be.true
})
})
context('.length', () => {
it('is the number of commands in the queue', () => {
expect(queue.length).to.equal(3)
queue.insert(0, createCommand({ name: 'eq' }))
expect(queue.length).to.equal(4)
})
})
context('.stopped', () => {
it('is true when queue is stopped', () => {
queue.stop()
expect(queue.stopped).to.true
})
it('is false when queue is not stopped', () => {
expect(queue.stopped).to.false
})
})
})

View File

@@ -0,0 +1,204 @@
import Bluebird from 'bluebird'
import { create } from '../../../src/util/queue'
const ids = (queueables) => queueables.map((q) => q.id)
describe('src/util/queue', () => {
let queue
beforeEach(() => {
queue = create([
{ id: '1' },
{ id: '2' },
{ id: '3' },
])
})
context('#get', () => {
it('returns list of queueable data', () => {
const queueables = queue.get()
expect(ids(queueables)).to.eql(['1', '2', '3'])
})
})
context('#add', () => {
it('adds queueable to end of queue', () => {
queue.add({ id: '4' })
queue.add({ id: '5' })
expect(ids(queue.get())).to.eql(['1', '2', '3', '4', '5'])
})
})
context('#insert', () => {
it('inserts queueable into queue at index', () => {
queue.insert(1, { id: '4' })
expect(ids(queue.get())).to.eql(['1', '4', '2', '3'])
})
it('returns the queueable', () => {
const queueable = { id: '4' }
const result = queue.insert(1, queueable)
expect(result).to.equal(queueable)
})
it('works with start boundary index', () => {
queue.insert(0, { id: '4' })
expect(ids(queue.get())).to.eql(['4', '1', '2', '3'])
})
it('works with end boundary index', () => {
queue.insert(3, { id: '4' })
expect(ids(queue.get())).to.eql(['1', '2', '3', '4'])
})
it('throws when index is negative', () => {
expect(() => {
queue.insert(-1, { id: '4' })
})
.to.throw('queue.insert must be called with a valid index - the index (-1) is out of bounds')
})
it('throws when index is out of bounds', () => {
expect(() => {
queue.insert(4, { id: '4' })
})
.to.throw('queue.insert must be called with a valid index - the index (4) is out of bounds')
})
})
context('#slice', () => {
it('returns queueables data from the index', () => {
const queueables = queue.slice(1)
expect(ids(queueables)).to.eql(['2', '3'])
})
})
context('#at', () => {
it('returns queueable data at index', () => {
const queueable = queue.at(1)
expect(queueable.id).to.equal('2')
})
})
context('#clear', () => {
it('removes all queueables from queue', () => {
queue.clear()
expect(queue.get().length).to.equal(0)
})
})
context('#reset', () => {
it('resets queue.stopped to false', () => {
queue.stop()
queue.reset()
expect(queue.stopped).to.false
})
})
context('#stop', () => {
it('sets queue.stopped to true', () => {
queue.stop()
expect(queue.stopped).to.true
})
})
context('#run', () => {
let props
beforeEach(() => {
props = {
onRun: cy.stub(),
onError: cy.stub(),
onFinish: cy.stub(),
}
})
it('runs the onRun function', () => {
return queue.run(props).promise.then(() => {
expect(props.onRun).to.be.called
})
})
it('returns the promise and the cancel and reject functions', () => {
const result = queue.run(props)
expect(result.promise).to.be.an.instanceOf(Bluebird)
expect(result.cancel).to.be.a('function')
expect(result.reject).to.be.a('function')
})
it('calls onError if onRun errors', () => {
const expectedErr = new Error('onRun failed')
props.onRun.throws(expectedErr)
return queue.run(props).promise.then(() => {
expect(props.onError).to.be.calledWith(expectedErr)
})
})
it('calls onError when outer promise is rejected', () => {
const expectedErr = new Error('rejected')
// hold up running with a never-resolving promise
// giving us time to reject the outer promise
props.onRun = () => {
return new Promise(() => {})
}
const { promise, reject } = queue.run(props)
reject(expectedErr)
return promise.then(() => {
expect(props.onError).to.be.calledWith(expectedErr)
})
})
it('calls onFinish if it succeeds', () => {
return queue.run(props).promise.then(() => {
expect(props.onFinish).to.be.called
})
})
it('calls onFinish if it fails', () => {
props.onRun.throws(new Error('fails'))
return queue.run(props).promise.then(() => {
expect(props.onFinish).to.be.called
})
})
})
context('.length', () => {
it('is the number of queueables in the queue', () => {
expect(queue.length).to.equal(3)
queue.insert(0, { id: '4' })
expect(queue.length).to.equal(4)
})
})
context('.stopped', () => {
it('is true when queue is stopped', () => {
queue.stop()
expect(queue.stopped).to.true
})
it('is false when queue is not stopped', () => {
expect(queue.stopped).to.false
})
})
})

View File

@@ -76,7 +76,7 @@ const create = function (Cypress, cy) {
const assertions = []
// grab the rest of the queue'd commands
for (let cmd of cy.queue.slice(index).get()) {
for (let cmd of cy.queue.slice(index)) {
// don't break on utilities, just skip over them
if (cmd.is('utility')) {
continue

View File

@@ -1,6 +1,7 @@
const _ = require('lodash')
const Promise = require('bluebird')
const $Command = require('../../cypress/command')
const $dom = require('../../dom')
const $errUtils = require('../../cypress/error_utils')
@@ -24,11 +25,11 @@ module.exports = (Commands, Cypress, cy, state) => {
if (state('current').get('injected')) {
const restoreCmdIndex = state('index') + 1
cy.queue.splice(restoreCmdIndex, 0, {
cy.queue.insert(restoreCmdIndex, $Command.create({
args: [state('subject')],
name: 'log-restore',
fn: (subject) => subject,
})
}))
state('index', restoreCmdIndex)
}

View File

@@ -1,6 +1,7 @@
const _ = require('lodash')
const Promise = require('bluebird')
const $Command = require('../../cypress/command')
const $dom = require('../../dom')
const $elements = require('../../dom/elements')
const $errUtils = require('../../cypress/error_utils')
@@ -635,11 +636,11 @@ module.exports = (Commands, Cypress, cy, state) => {
// commands inside within() callback and commands chained to it.
const restoreCmdIndex = state('index') + 1
cy.queue.splice(restoreCmdIndex, 0, {
cy.queue.insert(restoreCmdIndex, $Command.create({
args: [subject],
name: 'within-restore',
fn: (subject) => subject,
})
}))
state('index', restoreCmdIndex)

View File

@@ -686,7 +686,12 @@ export default function (Commands, Cypress, cy) {
}
cy.state('onCommandFailed', (err, queue, next) => {
const index = _.findIndex(queue.commands, (v: any) => _commandToResume && v.attributes.chainerId === _commandToResume.chainerId)
const index = _.findIndex(queue.get(), (command: any) => {
return (
_commandToResume
&& command.attributes.chainerId === _commandToResume.chainerId
)
})
// attach codeframe and cleanse the stack trace since we will not hit the cy.fail callback
// if this is the first time validate fails

View File

@@ -77,7 +77,17 @@ export const onResponse: HandlerFn<CyHttpMessages.IncomingResponse> = async (Cyp
// arguments to res.send() are merged with the existing response
const _staticResponse = _.defaults({}, staticResponse, _.pick(res, STATIC_RESPONSE_KEYS))
_.defaults(_staticResponse.headers, res.headers)
_staticResponse.headers = _.defaults({}, _staticResponse.headers, res.headers)
// https://github.com/cypress-io/cypress/issues/17084
// When a user didn't provide content-type,
// and they provided body as an object,
// we remove the content-type provided by the server
if (!staticResponse.headers || !staticResponse.headers['content-type']) {
if (typeof _staticResponse.body === 'object') {
delete _staticResponse.headers['content-type']
}
}
sendStaticResponse(requestId, _staticResponse)

View File

@@ -5,6 +5,9 @@ import {
BackendStaticResponseWithArrayBuffer,
FixtureOpts,
} from '@packages/net-stubbing/lib/types'
import {
caseInsensitiveHas,
} from '@packages/net-stubbing/lib/util'
import * as $errUtils from '../../cypress/error_utils'
// user-facing StaticResponse only
@@ -112,7 +115,16 @@ export function getBackendStaticResponse (staticResponse: Readonly<StaticRespons
backendStaticResponse.body = staticResponse.body
} else {
backendStaticResponse.body = JSON.stringify(staticResponse.body)
_.set(backendStaticResponse, 'headers.content-type', 'application/json')
// There are various json-related MIME types. We cannot simply set it as `application/json`.
// @see https://www.iana.org/assignments/media-types/media-types.xhtml
if (
!backendStaticResponse.headers ||
(backendStaticResponse.headers &&
!caseInsensitiveHas(backendStaticResponse.headers, 'content-type'))
) {
_.set(backendStaticResponse, 'headers.content-type', 'application/json')
}
}
}

View File

@@ -1,4 +1,5 @@
const _ = require('lodash')
const utils = require('./utils')
class $Command {
constructor (obj = {}) {
@@ -124,6 +125,10 @@ class $Command {
}
static create (obj) {
if (utils.isInstanceOf(obj, $Command)) {
return obj
}
return new $Command(obj)
}
}

View File

@@ -1,117 +0,0 @@
const _ = require('lodash')
const utils = require('./utils')
const $Command = require('./command')
class $CommandQueue {
constructor (cmds = []) {
this.commands = cmds
}
logs (filters) {
let logs = _.flatten(this.invokeMap('get', 'logs'))
if (filters) {
const matchesFilters = _.matches(filters)
logs = _.filter(logs, (log) => {
return matchesFilters(log.get())
})
}
return logs
}
add (obj) {
if (utils.isInstanceOf(obj, $Command)) {
return obj
}
return $Command.create(obj)
}
get () {
return this.commands
}
names () {
return this.invokeMap('get', 'name')
}
splice (start, end, obj) {
const cmd = this.add(obj)
this.commands.splice(start, end, cmd)
const prev = this.at(start - 1)
const next = this.at(start + 1)
if (prev) {
prev.set('next', cmd)
cmd.set('prev', prev)
}
if (next) {
next.set('prev', cmd)
cmd.set('next', next)
}
return cmd
}
slice (...args) {
const cmds = this.commands.slice.apply(this.commands, args)
return $CommandQueue.create(cmds)
}
at (index) {
return this.commands[index]
}
_filterByAttrs (attrs, method) {
const matchesAttrs = _.matches(attrs)
return _[method](this.commands, (command) => {
return matchesAttrs(command.attributes)
})
}
filter (attrs) {
return this._filterByAttrs(attrs, 'filter')
}
find (attrs) {
return this._filterByAttrs(attrs, 'find')
}
toJSON () {
return this.invokeMap('toJSON')
}
reset () {
this.commands.splice(0, this.commands.length)
return this
}
static create (cmds) {
return new $CommandQueue(cmds)
}
}
Object.defineProperty($CommandQueue.prototype, 'length', {
get () {
return this.commands.length
},
})
// mixin lodash methods
_.each(['invokeMap', 'map', 'first', 'reduce', 'reject', 'last', 'indexOf', 'each'], (method) => {
return $CommandQueue.prototype[method] = function (...args) {
args.unshift(this.commands)
return _[method].apply(_, args)
}
})
module.exports = $CommandQueue

View File

@@ -0,0 +1,394 @@
import _ from 'lodash'
import $ from 'jquery'
import Bluebird from 'bluebird'
import Debug from 'debug'
import { create as createQueue } from '../util/queue'
import $dom from '../dom'
import $utils from './utils'
import * as $errUtils from './error_utils'
const debugErrors = Debug('cypress:driver:errors')
interface Command {
get(key: string): any
get(): any
set(key: string, value: any): any
set(options: any): any
attributes: object
finishLogs(): void
}
const __stackReplacementMarker = (fn, ctx, args) => {
return fn.apply(ctx, args)
}
const commandRunningFailed = (Cypress, state, err) => {
// allow for our own custom onFail function
if (err.onFail) {
err.onFail(err)
// clean up this onFail callback after it's been called
delete err.onFail
return
}
const current = state('current')
return Cypress.log({
end: true,
snapshot: true,
error: err,
consoleProps () {
if (!current) return
const consoleProps = {}
const prev = current.get('prev')
if (current.get('type') === 'parent' || !prev) return
// if type isn't parent then we know its dual or child
// and we can add Applied To if there is a prev command
// and it is a parent
consoleProps['Applied To'] = $dom.isElement(prev.get('subject')) ?
$dom.getElements(prev.get('subject')) :
prev.get('subject')
return consoleProps
},
})
}
export const create = (state, timeouts, stability, cleanup, fail, isCy) => {
const queue = createQueue()
const { get, slice, at, reset, clear, stop } = queue
const logs = (filter) => {
let logs = _.flatten(_.invokeMap(queue.get(), 'get', 'logs'))
if (filter) {
const matchesFilter = _.matches(filter)
logs = _.filter(logs, (log) => {
return matchesFilter(log.get())
})
}
return logs
}
const names = () => {
return _.invokeMap(queue.get(), 'get', 'name')
}
const add = (command) => {
queue.add(command)
}
const insert = (index: number, command: Command) => {
queue.insert(index, command)
const prev = at(index - 1) as Command
const next = at(index + 1) as Command
if (prev) {
prev.set('next', command)
command.set('prev', prev)
}
if (next) {
next.set('prev', command)
command.set('next', next)
}
return command
}
const find = (attrs) => {
const matchesAttrs = _.matches(attrs)
return _.find(queue.get(), (command: Command) => {
return matchesAttrs(command.attributes)
})
}
const runCommand = (command: Command) => {
// bail here prior to creating a new promise
// because we could have stopped / canceled
// prior to ever making it through our first
// command
if (queue.stopped) {
return
}
state('current', command)
state('chainerId', command.get('chainerId'))
return stability.whenStable(() => {
state('nestedIndex', state('index'))
return command.get('args')
})
.then((args) => {
// store this if we enqueue new commands
// to check for promise violations
let ret
let enqueuedCmd
const commandEnqueued = (obj) => {
return enqueuedCmd = obj
}
// only check for command enqueing when none
// of our args are functions else commands
// like cy.then or cy.each would always fail
// since they return promises and queue more
// new commands
if ($utils.noArgsAreAFunction(args)) {
Cypress.once('command:enqueued', commandEnqueued)
}
// run the command's fn with runnable's context
try {
ret = __stackReplacementMarker(command.get('fn'), state('ctx'), args)
} catch (err) {
throw err
} finally {
// always remove this listener
Cypress.removeListener('command:enqueued', commandEnqueued)
}
state('commandIntermediateValue', ret)
// we cannot pass our cypress instance or our chainer
// back into bluebird else it will create a thenable
// which is never resolved
if (isCy(ret)) {
return null
}
if (!(!enqueuedCmd || !$utils.isPromiseLike(ret))) {
return $errUtils.throwErrByPath(
'miscellaneous.command_returned_promise_and_commands', {
args: {
current: command.get('name'),
called: enqueuedCmd.name,
},
},
)
}
if (!(!enqueuedCmd || !!_.isUndefined(ret))) {
ret = _.isFunction(ret) ?
ret.toString() :
$utils.stringify(ret)
// if we got a return value and we enqueued
// a new command and we didn't return cy
// or an undefined value then throw
return $errUtils.throwErrByPath(
'miscellaneous.returned_value_and_commands_from_custom_command', {
args: {
current: command.get('name'),
returned: ret,
},
},
)
}
return ret
}).then((subject) => {
state('commandIntermediateValue', undefined)
// we may be given a regular array here so
// we need to re-wrap the array in jquery
// if that's the case if the first item
// in this subject is a jquery element.
// we want to do this because in 3.1.2 there
// was a regression when wrapping an array of elements
const firstSubject = $utils.unwrapFirst(subject)
// if ret is a DOM element and its not an instance of our own jQuery
if (subject && $dom.isElement(firstSubject) && !$utils.isInstanceOf(subject, $)) {
// set it back to our own jquery object
// to prevent it from being passed downstream
// TODO: enable turning this off
// wrapSubjectsInJquery: false
// which will just pass subjects downstream
// without modifying them
subject = $dom.wrap(subject)
}
command.set({ subject })
// end / snapshot our logs
// if they need it
command.finishLogs()
// reset the nestedIndex back to null
state('nestedIndex', null)
// also reset recentlyReady back to null
state('recentlyReady', null)
// we're finished with the current command
// so set it back to null
state('current', null)
state('subject', subject)
return subject
})
}
const run = () => {
const next = () => {
// bail if we've been told to abort in case
// an old command continues to run after
if (queue.stopped) {
return
}
// start at 0 index if we dont have one
let index = state('index') || state('index', 0)
const command = at(index) as Command
// if the command should be skipped
// just bail and increment index
// and set the subject
if (command && command.get('skip')) {
// must set prev + next since other
// operations depend on this state being correct
command.set({
prev: at(index - 1) as Command,
next: at(index + 1) as Command,
})
state('index', index + 1)
state('subject', command.get('subject'))
return next()
}
// if we're at the very end
if (!command) {
// trigger queue is almost finished
Cypress.action('cy:command:queue:before:end')
// we need to wait after all commands have
// finished running if the application under
// test is no longer stable because we cannot
// move onto the next test until its finished
return stability.whenStable(() => {
Cypress.action('cy:command:queue:end')
return null
})
}
// store the previous timeout
const prevTimeout = timeouts.timeout()
// store the current runnable
const runnable = state('runnable')
Cypress.action('cy:command:start', command)
return runCommand(command)
.then(() => {
// each successful command invocation should
// always reset the timeout for the current runnable
// unless it already has a state. if it has a state
// and we reset the timeout again, it will always
// cause a timeout later no matter what. by this time
// mocha expects the test to be done
let fn
if (!runnable.state) {
timeouts.timeout(prevTimeout)
}
// mutate index by incrementing it
// this allows us to keep the proper index
// in between different hooks like before + beforeEach
// else run will be called again and index would start
// over at 0
index += 1
state('index', index)
Cypress.action('cy:command:end', command)
fn = state('onPaused')
if (fn) {
return new Bluebird((resolve) => {
return fn(resolve)
}).then(next)
}
return next()
})
}
const onError = (err: Error | string) => {
if (state('onCommandFailed')) {
return state('onCommandFailed')(err, queue, next)
}
debugErrors('caught error in promise chain: %o', err)
// since this failed this means that a specific command failed
// and we should highlight it in red or insert a new command
if (_.isObject(err)) {
// @ts-ignore
err.name = err.name || 'CypressError'
}
commandRunningFailed(Cypress, state, err)
return fail(err)
}
const { promise, reject, cancel } = queue.run({
onRun: next,
onError,
onFinish: cleanup,
})
state('promise', promise)
state('reject', reject)
state('cancel', () => {
cancel()
Cypress.action('cy:canceled')
})
return promise
}
return {
logs,
names,
add,
insert,
find,
run,
get,
slice,
at,
reset,
clear,
stop,
get length () {
return queue.length
},
get stopped () {
return queue.stopped
},
}
}

View File

@@ -1,6 +1,5 @@
/* eslint-disable prefer-rest-params */
const _ = require('lodash')
const $ = require('jquery')
const Promise = require('bluebird')
const debugErrors = require('debug')('cypress:driver:errors')
@@ -27,20 +26,13 @@ const $Retries = require('../cy/retries')
const $Stability = require('../cy/stability')
const $selection = require('../dom/selection')
const $Snapshots = require('../cy/snapshots')
const $Command = require('./command')
const $CommandQueue = require('./command_queue')
const $VideoRecorder = require('../cy/video-recorder')
const $TestConfigOverrides = require('../cy/testConfigOverrides')
const { registerFetch } = require('unfetch')
const noArgsAreAFunction = (args) => {
return !_.some(args, _.isFunction)
}
const isPromiseLike = (ret) => {
return ret && _.isFunction(ret.then)
}
const returnedFalse = (result) => {
return result === false
}
@@ -121,61 +113,16 @@ const setTopOnError = function (Cypress, cy) {
top.__alreadySetErrorHandlers__ = true
}
const commandRunningFailed = (Cypress, state, err) => {
// allow for our own custom onFail function
if (err.onFail) {
err.onFail(err)
// clean up this onFail callback after it's been called
delete err.onFail
return
}
const current = state('current')
return Cypress.log({
end: true,
snapshot: true,
error: err,
consoleProps () {
if (!current) return
const obj = {}
const prev = current.get('prev')
// if type isnt parent then we know its dual or child
// and we can add Applied To if there is a prev command
// and it is a parent
if (current.get('type') !== 'parent' && prev) {
const ret = $dom.isElement(prev.get('subject')) ?
$dom.getElements(prev.get('subject'))
:
prev.get('subject')
obj['Applied To'] = ret
return obj
}
},
})
}
// NOTE: this makes the cy object an instance
// TODO: refactor the 'create' method below into this class
class $Cy {}
const create = function (specWindow, Cypress, Cookies, state, config, log) {
let cy = new $Cy()
let stopped = false
const commandFns = {}
state('specWindow', specWindow)
const isStopped = () => {
return stopped
}
const onFinishAssertions = function () {
return assertions.finishAssertions.apply(window, arguments)
}
@@ -196,11 +143,10 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
return $dom.query(selector, context)
}
const queue = $CommandQueue.create()
$VideoRecorder.create(Cypress)
const timeouts = $Timeouts.create(state)
const stability = $Stability.create(Cypress, state)
const retries = $Retries.create(Cypress, state, timeouts.timeout, timeouts.clearTimeout, stability.whenStable, onFinishAssertions)
const assertions = $Assertions.create(Cypress, cy)
@@ -221,6 +167,10 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
const snapshots = $Snapshots.create($$, state)
const testConfigOverrides = $TestConfigOverrides.create()
const isStopped = () => {
return queue.stopped
}
const isCy = (val) => {
return (val === cy) || $utils.isInstanceOf(val, $Chainer)
}
@@ -354,7 +304,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
const enqueue = function (obj) {
// if we have a nestedIndex it means we're processing
// nested commands and need to splice them into the
// nested commands and need to insert them into the
// index past the current index as opposed to
// pushing them to the end we also dont want to
// reset the run defer because splicing means we're
@@ -365,22 +315,21 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
// we had a bug that would bomb on custom commands when it was the
// first command. this was due to nestedIndex being undefined at that
// time. so we have to ensure to check that its any kind of number (even 0)
// in order to know to splice into the existing array.
// in order to know to insert it into the existing array.
let nestedIndex = state('nestedIndex')
// if this is a number then we know
// we're about to splice this into our commands
// and need to reset next + increment the index
// if this is a number, then we know we're about to insert this
// into our commands and need to reset next + increment the index
if (_.isNumber(nestedIndex)) {
state('nestedIndex', (nestedIndex += 1))
}
// we look at whether or not nestedIndex is a number, because if it
// is then we need to splice inside of our commands, else just push
// is then we need to insert inside of our commands, else just push
// it onto the end of the queu
const index = _.isNumber(nestedIndex) ? nestedIndex : queue.length
queue.splice(index, 0, obj)
queue.insert(index, $Command.create(obj))
return Cypress.action('cy:command:enqueued', obj)
}
@@ -401,298 +350,6 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
return getCommandsUntilFirstParentOrValidSubject(command.get('prev'), memo)
}
const runCommand = function (command) {
// bail here prior to creating a new promise
// because we could have stopped / canceled
// prior to ever making it through our first
// command
if (stopped) {
return
}
state('current', command)
state('chainerId', command.get('chainerId'))
return stability.whenStable(() => {
// TODO: handle this event
// @trigger "invoke:start", command
state('nestedIndex', state('index'))
return command.get('args')
})
.then((args) => {
// store this if we enqueue new commands
// to check for promise violations
let ret
let enqueuedCmd = null
const commandEnqueued = (obj) => {
return enqueuedCmd = obj
}
// only check for command enqueing when none
// of our args are functions else commands
// like cy.then or cy.each would always fail
// since they return promises and queue more
// new commands
if (noArgsAreAFunction(args)) {
Cypress.once('command:enqueued', commandEnqueued)
}
// run the command's fn with runnable's context
try {
ret = __stackReplacementMarker(command.get('fn'), state('ctx'), args)
} catch (err) {
throw err
} finally {
// always remove this listener
Cypress.removeListener('command:enqueued', commandEnqueued)
}
state('commandIntermediateValue', ret)
// we cannot pass our cypress instance or our chainer
// back into bluebird else it will create a thenable
// which is never resolved
if (isCy(ret)) {
return null
}
if (!(!enqueuedCmd || !isPromiseLike(ret))) {
return $errUtils.throwErrByPath(
'miscellaneous.command_returned_promise_and_commands', {
args: {
current: command.get('name'),
called: enqueuedCmd.name,
},
},
)
}
if (!(!enqueuedCmd || !!_.isUndefined(ret))) {
// TODO: clean this up in the utility function
// to conditionally stringify functions
ret = _.isFunction(ret) ?
ret.toString()
:
$utils.stringify(ret)
// if we got a return value and we enqueued
// a new command and we didn't return cy
// or an undefined value then throw
return $errUtils.throwErrByPath(
'miscellaneous.returned_value_and_commands_from_custom_command', {
args: {
current: command.get('name'),
returned: ret,
},
},
)
}
return ret
}).then((subject) => {
state('commandIntermediateValue', undefined)
// we may be given a regular array here so
// we need to re-wrap the array in jquery
// if that's the case if the first item
// in this subject is a jquery element.
// we want to do this because in 3.1.2 there
// was a regression when wrapping an array of elements
const firstSubject = $utils.unwrapFirst(subject)
// if ret is a DOM element and its not an instance of our own jQuery
if (subject && $dom.isElement(firstSubject) && !$utils.isInstanceOf(subject, $)) {
// set it back to our own jquery object
// to prevent it from being passed downstream
// TODO: enable turning this off
// wrapSubjectsInJquery: false
// which will just pass subjects downstream
// without modifying them
subject = $dom.wrap(subject)
}
command.set({ subject })
// end / snapshot our logs
// if they need it
command.finishLogs()
// reset the nestedIndex back to null
state('nestedIndex', null)
// also reset recentlyReady back to null
state('recentlyReady', null)
// we're finished with the current command
// so set it back to null
state('current', null)
state('subject', subject)
return subject
})
}
const run = function () {
const next = function () {
// bail if we've been told to abort in case
// an old command continues to run after
if (stopped) {
return
}
// start at 0 index if we dont have one
let index = state('index') || state('index', 0)
const command = queue.at(index)
// if the command should be skipped
// just bail and increment index
// and set the subject
// TODO DRY THIS LOGIC UP
if (command && command.get('skip')) {
// must set prev + next since other
// operations depend on this state being correct
command.set({ prev: queue.at(index - 1), next: queue.at(index + 1) })
state('index', index + 1)
state('subject', command.get('subject'))
return next()
}
// if we're at the very end
if (!command) {
// trigger queue is almost finished
Cypress.action('cy:command:queue:before:end')
// we need to wait after all commands have
// finished running if the application under
// test is no longer stable because we cannot
// move onto the next test until its finished
return stability.whenStable(() => {
Cypress.action('cy:command:queue:end')
return null
})
}
// store the previous timeout
const prevTimeout = timeouts.timeout()
// store the current runnable
const runnable = state('runnable')
Cypress.action('cy:command:start', command)
return runCommand(command)
.then(() => {
// each successful command invocation should
// always reset the timeout for the current runnable
// unless it already has a state. if it has a state
// and we reset the timeout again, it will always
// cause a timeout later no matter what. by this time
// mocha expects the test to be done
let fn
if (!runnable.state) {
timeouts.timeout(prevTimeout)
}
// mutate index by incrementing it
// this allows us to keep the proper index
// in between different hooks like before + beforeEach
// else run will be called again and index would start
// over at 0
state('index', (index += 1))
Cypress.action('cy:command:end', command)
fn = state('onPaused')
if (fn) {
return new Promise((resolve) => {
return fn(resolve)
}).then(next)
}
return next()
})
}
let inner = null
// this ends up being the parent promise wrapper
const promise = new Promise((resolve, reject) => {
// bubble out the inner promise
// we must use a resolve(null) here
// so the outer promise is first defined
// else this will kick off the 'next' call
// too soon and end up running commands prior
// to promise being defined
inner = Promise
.resolve(null)
.then(next)
.then(resolve)
.catch(reject)
// can't use onCancel argument here because
// its called asynchronously
// when we manually reject our outer promise we
// have to immediately cancel the inner one else
// it won't be notified and its callbacks will
// continue to be invoked
// normally we don't have to do this because rejections
// come from the inner promise and bubble out to our outer
//
// but when we manually reject the outer promise we
// have to go in the opposite direction from outer -> inner
const rejectOuterAndCancelInner = function (err) {
inner.cancel()
return reject(err)
}
state('resolve', resolve)
state('reject', rejectOuterAndCancelInner)
})
.catch((err) => {
if (state('onCommandFailed')) {
return state('onCommandFailed')(err, queue, next)
}
debugErrors('caught error in promise chain: %o', err)
// since this failed this means that a
// specific command failed and we should
// highlight it in red or insert a new command
err.name = err.name || 'CypressError'
commandRunningFailed(Cypress, state, err)
return fail(err)
})
.finally(cleanup)
// cancel both promises
const cancel = function () {
promise.cancel()
inner.cancel()
// notify the world
return Cypress.action('cy:canceled')
}
state('cancel', cancel)
state('promise', promise)
// return this outer bluebird promise
return promise
}
const removeSubject = () => {
return state('subject', undefined)
}
@@ -736,7 +393,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
}
const doneEarly = function () {
stopped = true
queue.stop()
// we only need to worry about doneEarly when
// it comes from a manual event such as stopping
@@ -795,7 +452,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
let rets
stopped = true
queue.stop()
if (typeof err === 'string') {
err = new Error(err)
@@ -875,6 +532,8 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
return finish(err)
}
const queue = $CommandQueue.create(state, timeouts, stability, cleanup, fail, isCy)
_.extend(cy, {
id: _.uniqueId('cy'),
@@ -1032,7 +691,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
stop () {
// don't do anything if we've already stopped
if (stopped) {
if (queue.stopped) {
return
}
@@ -1040,8 +699,6 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
},
reset (attrs, test) {
stopped = false
const s = state()
const backup = {
@@ -1059,6 +716,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
state(backup)
queue.reset()
queue.clear()
timers.reset()
testConfigOverrides.restoreAndSetTestConfigOverrides(test, Cypress.config, Cypress.env)
@@ -1130,7 +788,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
const current = state('current')
// if this is a custom promise
if (isPromiseLike(ret) && noArgsAreAFunction(current.get('args'))) {
if ($utils.isPromiseLike(ret) && $utils.noArgsAreAFunction(current.get('args'))) {
$errUtils.throwErrByPath(
'miscellaneous.command_returned_promise_and_commands', {
args: {
@@ -1149,7 +807,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
warnMixingPromisesAndCommands()
}
run()
queue.run()
}
return chain
@@ -1326,7 +984,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
setRunnable (runnable, hookId) {
// when we're setting a new runnable
// prepare to run again!
stopped = false
queue.reset()
// reset the promise again
state('promise', undefined)
@@ -1397,7 +1055,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
if (ret &&
(queue.length > currentLength) &&
(!isCy(ret)) &&
(!isPromiseLike(ret))) {
(!$utils.isPromiseLike(ret))) {
// TODO: clean this up in the utility function
// to conditionally stringify functions
ret = _.isFunction(ret) ?
@@ -1423,7 +1081,7 @@ const create = function (specWindow, Cypress, Cookies, state, config, log) {
}
// if we returned a promise like object
if ((!isCy(ret)) && isPromiseLike(ret)) {
if ((!isCy(ret)) && $utils.isPromiseLike(ret)) {
// indicate we've returned a custom promise
state('returnedCustomPromise', true)

View File

@@ -399,4 +399,12 @@ module.exports = {
return String.fromCharCode(`0x${p1}`)
}))
},
noArgsAreAFunction (args) {
return !_.some(args, _.isFunction)
},
isPromiseLike (ret) {
return ret && _.isFunction(ret.then)
},
}

View File

@@ -0,0 +1,114 @@
import Bluebird from 'bluebird'
interface QueueRunProps {
onRun: () => Bluebird<any> | Promise<any>
onError: (err: Error) => void
onFinish: () => void
}
export const create = <T>(queueables: T[] = []) => {
let stopped = false
const get = (): T[] => {
return queueables
}
const add = (queueable: T) => {
queueables.push(queueable)
}
const insert = (index: number, queueable: T) => {
if (index < 0 || index > queueables.length) {
throw new Error(`queue.insert must be called with a valid index - the index (${index}) is out of bounds`)
}
queueables.splice(index, 0, queueable)
return queueable
}
const slice = (index: number) => {
return queueables.slice(index)
}
const at = (index: number): T => {
return get()[index]
}
const reset = () => {
stopped = false
}
const clear = () => {
queueables.length = 0
}
const stop = () => {
stopped = true
}
const run = ({ onRun, onError, onFinish }: QueueRunProps) => {
let inner
let rejectOuterAndCancelInner
// this ends up being the parent promise wrapper
const promise = new Bluebird((resolve, reject) => {
// bubble out the inner promise. we must use a resolve(null) here
// so the outer promise is first defined else this will kick off
// the 'next' call too soon and end up running commands prior to
// the promise being defined
inner = Bluebird
.resolve(null)
.then(onRun)
.then(resolve)
.catch(reject)
// can't use onCancel argument here because it's called asynchronously.
// when we manually reject our outer promise we have to immediately
// cancel the inner one else it won't be notified and its callbacks
// will continue to be invoked. normally we don't have to do this
// because rejections come from the inner promise and bubble out to
// our outer, but when we manually reject the outer promise, we
// have to go in the opposite direction from outer -> inner
rejectOuterAndCancelInner = (err) => {
inner.cancel()
reject(err)
}
})
.catch(onError)
.finally(onFinish)
const cancel = () => {
promise.cancel()
inner.cancel()
}
return {
promise,
cancel,
// wrapped to ensure `rejectOuterAndCancelInner` is assigned
// before reject is called
reject: (err) => rejectOuterAndCancelInner(err),
}
}
return {
get,
add,
insert,
slice,
at,
reset,
clear,
stop,
run,
get length () {
return queueables.length
},
get stopped () {
return stopped
},
}
}

View File

@@ -24,7 +24,7 @@
"minimist": "1.2.5"
},
"devDependencies": {
"electron": "12.0.0-beta.14",
"electron": "13.1.7",
"execa": "4.1.0",
"mocha": "3.5.3"
},

View File

@@ -20,7 +20,6 @@ const {
const generateKeyPairAsync = Promise.promisify(pki.rsa.generateKeyPair)
const ipAddressRe = /^[\d\.]+$/
const asterisksRe = /\*/g
const CAattrs = [{
name: 'commonName',
@@ -119,6 +118,10 @@ const ServerExtensions = [{
name: 'subjectKeyIdentifier',
}]
function hostnameToFilename (hostname) {
return hostname.replace(/\*/g, '_')
}
class CA {
constructor (caFolder) {
if (!caFolder) {
@@ -167,9 +170,9 @@ class CA {
this.CAkeys = keys
return Promise.all([
fs.outputFileAsync(path.join(this.certsFolder, 'ca.pem'), pki.certificateToPem(cert)),
fs.outputFileAsync(path.join(this.keysFolder, 'ca.private.key'), pki.privateKeyToPem(keys.privateKey)),
fs.outputFileAsync(path.join(this.keysFolder, 'ca.public.key'), pki.publicKeyToPem(keys.publicKey)),
fs.outputFileAsync(this.getCACertPath(), pki.certificateToPem(cert)),
fs.outputFileAsync(this.getCAPrivateKeyPath(), pki.privateKeyToPem(keys.privateKey)),
fs.outputFileAsync(this.getCAPublicKeyPath(), pki.publicKeyToPem(keys.publicKey)),
this.writeCAVersion(),
])
})
@@ -177,9 +180,9 @@ class CA {
loadCA () {
return Promise.props({
certPEM: fs.readFileAsync(path.join(this.certsFolder, 'ca.pem'), 'utf-8'),
keyPrivatePEM: fs.readFileAsync(path.join(this.keysFolder, 'ca.private.key'), 'utf-8'),
keyPublicPEM: fs.readFileAsync(path.join(this.keysFolder, 'ca.public.key'), 'utf-8'),
certPEM: fs.readFileAsync(this.getCACertPath(), 'utf-8'),
keyPrivatePEM: fs.readFileAsync(this.getCAPrivateKeyPath(), 'utf-8'),
keyPublicPEM: fs.readFileAsync(this.getCAPublicKeyPath(), 'utf-8'),
})
.then((results) => {
this.CAcert = pki.certificateFromPem(results.certPEM)
@@ -231,29 +234,59 @@ class CA {
const keyPrivatePem = pki.privateKeyToPem(keysServer.privateKey)
const keyPublicPem = pki.publicKeyToPem(keysServer.publicKey)
const dest = mainHost.replace(asterisksRe, '_')
const baseFilename = hostnameToFilename(mainHost)
return Promise.all([
fs.outputFileAsync(path.join(this.certsFolder, `${dest}.pem`), certPem),
fs.outputFileAsync(path.join(this.keysFolder, `${dest}.key`), keyPrivatePem),
fs.outputFileAsync(path.join(this.keysFolder, `${dest}.public.key`), keyPublicPem),
fs.outputFileAsync(this.getCertPath(baseFilename), certPem),
fs.outputFileAsync(this.getPrivateKeyPath(baseFilename), keyPrivatePem),
fs.outputFileAsync(this.getPublicKeyPath(baseFilename), keyPublicPem),
])
.return([certPem, keyPrivatePem])
}
getCertificateKeysForHostname (hostname) {
const dest = hostname.replace(asterisksRe, '_')
clearDataForHostname (hostname) {
const baseFilename = hostnameToFilename(hostname)
return Promise.all([
fs.readFileAsync(path.join(this.certsFolder, `${dest}.pem`)),
fs.readFileAsync(path.join(this.keysFolder, `${dest}.key`)),
fs.remove(this.getCertPath(baseFilename)),
fs.remove(this.getPrivateKeyPath(baseFilename)),
fs.remove(this.getPublicKeyPath(baseFilename)),
])
}
getCertificateKeysForHostname (hostname) {
const baseFilename = hostnameToFilename(hostname)
return Promise.all([
fs.readFileAsync(this.getCertPath(baseFilename)),
fs.readFileAsync(this.getPrivateKeyPath(baseFilename)),
])
}
getPrivateKeyPath (baseFilename) {
return path.join(this.keysFolder, `${baseFilename}.key`)
}
getPublicKeyPath (baseFilename) {
return path.join(this.keysFolder, `${baseFilename}.public.key`)
}
getCertPath (baseFilename) {
return path.join(this.certsFolder, `${baseFilename}.pem`)
}
getCACertPath () {
return path.join(this.certsFolder, 'ca.pem')
}
getCAPrivateKeyPath () {
return path.join(this.keysFolder, 'ca.private.key')
}
getCAPublicKeyPath () {
return path.join(this.keysFolder, 'ca.public.key')
}
getCAVersionPath () {
return path.join(this.baseCAFolder, 'ca_version.txt')
}
@@ -286,7 +319,7 @@ class CA {
static create (caFolder) {
const ca = new CA(caFolder)
return fs.statAsync(path.join(ca.certsFolder, 'ca.pem'))
return fs.statAsync(ca.getCACertPath())
.bind(ca)
.then(ca.assertMinimumCAVersion)
.tapCatch(ca.removeAll)

View File

@@ -183,6 +183,15 @@ class Server {
}
return this._getPortFor(hostname)
.catch(async (err) => {
debug('Error adding context, deleting certs and regenning %o', { hostname, err })
// files on disk can be corrupted, so try again
// @see https://github.com/cypress-io/cypress/issues/8705
await this._ca.clearDataForHostname(hostname)
return this._getPortFor(hostname)
})
.then((port) => {
sslServers[hostname] = { port }

View File

@@ -9,6 +9,7 @@ const Promise = require('bluebird')
const proxy = require('../helpers/proxy')
const httpServer = require('../helpers/http_server')
const httpsServer = require('../helpers/https_server')
const fs = require('fs').promises
describe('Proxy', () => {
beforeEach(function () {
@@ -149,6 +150,44 @@ describe('Proxy', () => {
})
})
// @see https://github.com/cypress-io/cypress/issues/8705
it('handles errors with reusing existing certificates', async function () {
await this.proxy._ca.removeAll()
proxy.reset()
const genSpy = this.sandbox.spy(this.proxy, '_generateMissingCertificates')
await request({
strictSSL: false,
url: 'https://localhost:8443/',
proxy: 'http://localhost:3333',
})
proxy.reset()
expect(genSpy).to.be.calledWith('localhost').and.calledOnce
const privateKeyPath = this.proxy._ca.getPrivateKeyPath('localhost')
const key = (await fs.readFile(privateKeyPath)).toString().trim()
expect(key).to.match(/^-----BEGIN RSA PRIVATE KEY-----/)
.and.match(/-----END RSA PRIVATE KEY-----$/)
await fs.writeFile(privateKeyPath, 'some random garbage')
await request({
strictSSL: false,
url: 'https://localhost:8443/',
proxy: 'http://localhost:3333',
})
expect(genSpy).to.always.have.been.calledWith('localhost').and.calledTwice
const key2 = (await fs.readFile(privateKeyPath)).toString().trim()
expect(key2).to.match(/^-----BEGIN RSA PRIVATE KEY-----/)
.and.match(/-----END RSA PRIVATE KEY-----$/)
})
// https://github.com/cypress-io/cypress/issues/771
it('generates certs and can proxy requests for HTTPS requests to IPs', function () {
this.sandbox.spy(this.proxy, '_generateMissingCertificates')
@@ -204,7 +243,6 @@ describe('Proxy', () => {
})
})
// TODO
context('with an upstream proxy', () => {
beforeEach(function () {
// PROXY vars should override npm_config vars, so set them to cause failures if they are used

View File

@@ -153,6 +153,18 @@ export class InterceptedRequest {
data,
}
// https://github.com/cypress-io/cypress/issues/17139
// Routes should be counted before they're sent.
if (eventName === 'before:request') {
const route = this.matchingRoutes.find(({ id }) => id === subscription.routeId) as BackendRoute
route.matches++
if (route.routeMatcher.times && route.matches >= route.routeMatcher.times) {
route.disabled = true
}
}
const _emit = () => emit(this.socket, eventName, eventFrame)
if (!subscription.await) {
@@ -176,7 +188,7 @@ export class InterceptedRequest {
}
}
for (const { routeId, subscriptions, immediateStaticResponse } of this.subscriptionsByRoute) {
for (const { subscriptions, immediateStaticResponse } of this.subscriptionsByRoute) {
for (const subscription of subscriptions) {
await handleSubscription(subscription)
@@ -186,14 +198,6 @@ export class InterceptedRequest {
}
if (eventName === 'before:request') {
const route = this.matchingRoutes.find(({ id }) => id === routeId) as BackendRoute
route.matches++
if (route.routeMatcher.times && route.matches >= route.routeMatcher.times) {
route.disabled = true
}
if (immediateStaticResponse) {
await sendStaticResponse(this, immediateStaticResponse)

View File

@@ -16,6 +16,7 @@ import ThrottleStream from 'throttle'
import MimeTypes from 'mime-types'
import { CypressIncomingRequest } from '@packages/proxy'
import { InterceptedRequest } from './intercepted-request'
import { caseInsensitiveGet, caseInsensitiveHas } from '../util'
// TODO: move this into net-stubbing once cy.route is removed
import { parseContentType } from '@packages/server/lib/controllers/xhrs'
@@ -79,24 +80,6 @@ function _getFakeClientResponse (opts: {
return clientResponse
}
const caseInsensitiveGet = function (obj, lowercaseProperty) {
for (let key of Object.keys(obj)) {
if (key.toLowerCase() === lowercaseProperty) {
return obj[key]
}
}
}
const caseInsensitiveHas = function (obj, lowercaseProperty) {
for (let key of Object.keys(obj)) {
if (key.toLowerCase() === lowercaseProperty) {
return true
}
}
return false
}
export function setDefaultHeaders (req: CypressIncomingRequest, res: IncomingMessage) {
const setDefaultHeader = (lowercaseHeader: string, defaultValueFn: () => string) => {
if (!caseInsensitiveHas(res.headers, lowercaseHeader)) {

View File

@@ -0,0 +1,17 @@
export const caseInsensitiveGet = function (obj, lowercaseProperty) {
for (let key of Object.keys(obj)) {
if (key.toLowerCase() === lowercaseProperty) {
return obj[key]
}
}
}
export const caseInsensitiveHas = function (obj, lowercaseProperty) {
for (let key of Object.keys(obj)) {
if (key.toLowerCase() === lowercaseProperty) {
return true
}
}
return false
}

View File

@@ -19,6 +19,11 @@ export type CyCookie = Pick<chrome.cookies.Cookie, 'name' | 'value' | 'expiratio
// https://developer.chrome.com/extensions/cookies#method-getAll
type CyCookieFilter = chrome.cookies.GetAllDetails
export const screencastOpts: cdp.Page.StartScreencastRequest = {
format: 'jpeg',
everyNthFrame: Number(process.env.CYPRESS_EVERY_NTH_FRAME || 5),
}
function convertSameSiteExtensionToCdp (str: CyCookie['sameSite']): cdp.Network.CookieSameSite | undefined {
return str ? ({
'no_restriction': 'None',

View File

@@ -11,7 +11,7 @@ import { launch } from '@packages/launcher'
import appData from '../util/app_data'
import { fs } from '../util/fs'
import { CdpAutomation } from './cdp_automation'
import { CdpAutomation, screencastOpts } from './cdp_automation'
import * as CriClient from './cri-client'
import * as protocol from './protocol'
import utils from './utils'
@@ -273,9 +273,7 @@ const _maybeRecordVideo = async function (client, options) {
client.send('Page.screencastFrameAck', { sessionId: meta.sessionId })
})
await client.send('Page.startScreencast', {
format: 'jpeg',
})
await client.send('Page.startScreencast', screencastOpts)
return client
}

View File

@@ -5,7 +5,7 @@ const Bluebird = require('bluebird')
const debug = require('debug')('cypress:server:browsers:electron')
const menu = require('../gui/menu')
const Windows = require('../gui/windows')
const { CdpAutomation } = require('./cdp_automation')
const { CdpAutomation, screencastOpts } = require('./cdp_automation')
const savedState = require('../saved_state')
const utils = require('./utils')
const errors = require('../errors')
@@ -62,7 +62,7 @@ const _getAutomation = function (win, options, parent) {
return fn(message, data)
}
await sendCommand('Page.startScreencast')
await sendCommand('Page.startScreencast', screencastOpts)
const ret = await fn(message, data)
@@ -104,9 +104,7 @@ const _maybeRecordVideo = function (webContents, options) {
}
})
await webContents.debugger.sendCommand('Page.startScreencast', {
format: 'jpeg',
})
await webContents.debugger.sendCommand('Page.startScreencast', screencastOpts)
}
}

View File

@@ -39,7 +39,7 @@ import { checkSupportFile } from './project_utils'
// TODO: Figure out how to type this better.
type ReceivedCypressOptions =
Partial<Pick<Cypress.RuntimeConfigOptions, 'hosts' | 'projectName' | 'clientRoute' | 'devServerPublicPathRoute' | 'namespace' | 'report' | 'socketIoCookie' | 'configFile' | 'isTextTerminal' | 'isNewProject' | 'proxyUrl' | 'browsers'>>
& Partial<Pick<Cypress.ResolvedConfigOptions, 'supportFolder' | 'experimentalSourceRewriting' | 'fixturesFolder' | 'reporter' | 'reporterOptions' | 'screenshotsFolder' | 'pluginsFile' | 'supportFile' | 'integrationFolder' | 'baseUrl' | 'viewportHeight' | 'viewportWidth' | 'port' | 'experimentalInteractiveRunEvents'>>
& Partial<Pick<Cypress.ResolvedConfigOptions, 'chromeWebSecurity' | 'supportFolder' | 'experimentalSourceRewriting' | 'fixturesFolder' | 'reporter' | 'reporterOptions' | 'screenshotsFolder' | 'pluginsFile' | 'supportFile' | 'integrationFolder' | 'baseUrl' | 'viewportHeight' | 'viewportWidth' | 'port' | 'experimentalInteractiveRunEvents'>>
export interface Cfg extends ReceivedCypressOptions {
projectRoot: string
@@ -675,7 +675,7 @@ export class ProjectBase<TServer extends ServerE2E | ServerCt> extends EE {
if (theCfg.browsers) {
theCfg.browsers = theCfg.browsers?.map((browser) => {
if (browser.family === 'chromium') {
if (browser.family === 'chromium' || theCfg.chromeWebSecurity) {
return browser
}

View File

@@ -1,348 +0,0 @@
const _ = require('lodash')
const utils = require('fluent-ffmpeg/lib/utils')
const debug = require('debug')('cypress:server:video')
const ffmpeg = require('fluent-ffmpeg')
const stream = require('stream')
const Promise = require('bluebird')
const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path
const BlackHoleStream = require('black-hole-stream')
const { fs } = require('./util/fs')
// extra verbose logs for logging individual frames
const debugFrames = require('debug')('cypress-verbose:server:video:frames')
debug('using ffmpeg from %s', ffmpegPath)
ffmpeg.setFfmpegPath(ffmpegPath)
const deferredPromise = function () {
let reject
let resolve = (reject = null)
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve
reject = _reject
})
return { promise, resolve, reject }
}
module.exports = {
generateFfmpegChaptersConfig (tests) {
if (!tests) {
return null
}
const configString = tests.map((test) => {
return test.attempts.map((attempt, i) => {
const { videoTimestamp, wallClockDuration } = attempt
let title = test.title ? test.title.join(' ') : ''
if (i > 0) {
title += `attempt ${i}`
}
return [
'[CHAPTER]',
'TIMEBASE=1/1000',
`START=${videoTimestamp - wallClockDuration}`,
`END=${videoTimestamp}`,
`title=${title}`,
].join('\n')
}).join('\n')
}).join('\n')
return `;FFMETADATA1\n${configString}`
},
getMsFromDuration (duration) {
return utils.timemarkToSeconds(duration) * 1000
},
getCodecData (src) {
return new Promise((resolve, reject) => {
return ffmpeg()
.on('stderr', (stderr) => {
return debug('get codecData stderr log %o', { message: stderr })
}).on('codecData', resolve)
.input(src)
.format('null')
.output(new BlackHoleStream())
.run()
}).tap((data) => {
return debug('codecData %o', {
src,
data,
})
}).tapCatch((err) => {
return debug('getting codecData failed', { err })
})
},
getChapters (fileName) {
return new Promise((resolve, reject) => {
ffmpeg.ffprobe(fileName, ['-show_chapters'], (err, metadata) => {
if (err) {
return reject(err)
}
resolve(metadata)
})
})
},
copy (src, dest) {
debug('copying from %s to %s', src, dest)
return fs
.copyAsync(src, dest, { overwrite: true })
.catch({ code: 'ENOENT' }, () => {})
},
// dont yell about ENOENT errors
start (name, options = {}) {
const pt = stream.PassThrough()
const ended = deferredPromise()
let done = false
let wantsWrite = true
let skippedChunksCount = 0
let writtenChunksCount = 0
_.defaults(options, {
onError () {},
})
const endVideoCapture = function (waitForMoreChunksTimeout = 3000) {
debugFrames('frames written:', writtenChunksCount)
// in some cases (webm) ffmpeg will crash if fewer than 2 buffers are
// written to the stream, so we don't end capture until we get at least 2
if (writtenChunksCount < 2) {
return new Promise((resolve) => {
pt.once('data', resolve)
})
.then(endVideoCapture)
.timeout(waitForMoreChunksTimeout)
}
done = true
pt.end()
// return the ended promise which will eventually
// get resolve or rejected
return ended.promise
}
const lengths = {}
const writeVideoFrame = function (data) {
// make sure we haven't ended
// our stream yet because paint
// events can linger beyond
// finishing the actual video
if (done) {
return
}
// when `data` is empty, it is sent as an empty Buffer (`<Buffer >`)
// which can crash the process. this can happen if there are
// errors in the video capture process, which are handled later
// on, so just skip empty frames here.
// @see https://github.com/cypress-io/cypress/pull/6818
if (_.isEmpty(data)) {
debugFrames('empty chunk received %o', data)
return
}
if (options.webmInput) {
if (lengths[data.length]) {
// this prevents multiple chunks of webm metadata from being written to the stream
// which would crash ffmpeg
debugFrames('duplicate length frame received:', data.length)
return
}
lengths[data.length] = true
}
writtenChunksCount++
debugFrames('writing video frame')
if (wantsWrite) {
if (!(wantsWrite = pt.write(data))) {
return pt.once('drain', () => {
debugFrames('video stream drained')
wantsWrite = true
})
}
} else {
skippedChunksCount += 1
return debugFrames('skipping video frame %o', { skipped: skippedChunksCount })
}
}
const startCapturing = () => {
return new Promise((resolve) => {
const cmd = ffmpeg({
source: pt,
priority: 20,
})
.videoCodec('libx264')
.outputOptions('-preset ultrafast')
.on('start', (command) => {
debug('capture started %o', { command })
return resolve({
cmd,
startedVideoCapture: new Date,
})
}).on('codecData', (data) => {
return debug('capture codec data: %o', data)
}).on('stderr', (stderr) => {
return debug('capture stderr log %o', { message: stderr })
}).on('error', (err, stdout, stderr) => {
debug('capture errored: %o', { error: err.message, stdout, stderr })
// bubble errors up
options.onError(err, stdout, stderr)
// reject the ended promise
return ended.reject(err)
}).on('end', () => {
debug('capture ended')
return ended.resolve()
})
// this is to prevent the error "invalid data input" error
// when input frames have an odd resolution
.videoFilters(`crop='floor(in_w/2)*2:floor(in_h/2)*2'`)
if (options.webmInput) {
cmd
.inputFormat('webm')
// assume 18 fps. This number comes from manual measurement of avg fps coming from firefox.
// TODO: replace this with the 'vfr' option below when dropped frames issue is fixed.
.inputFPS(18)
// 'vsync vfr' (variable framerate) works perfectly but fails on top page navigation
// since video timestamp resets to 0, timestamps already written will be dropped
// .outputOption('-vsync vfr')
} else {
cmd
.inputFormat('image2pipe')
.inputOptions('-use_wallclock_as_timestamps 1')
}
return cmd.save(name)
})
}
return startCapturing()
.then(({ cmd, startedVideoCapture }) => {
return {
_pt: pt,
cmd,
endVideoCapture,
writeVideoFrame,
startedVideoCapture,
}
})
},
async process (name, cname, videoCompression, ffmpegchaptersConfig, onProgress = function () {}) {
const metaFileName = `${name}.meta`
const maybeGenerateMetaFile = Promise.method(() => {
if (!ffmpegchaptersConfig) {
return false
}
// Writing the metadata to filesystem is necessary because fluent-ffmpeg is just a wrapper of ffmpeg command.
return fs.writeFile(metaFileName, ffmpegchaptersConfig).then(() => true)
})
const addChaptersMeta = await maybeGenerateMetaFile()
let total = null
return new Promise((resolve, reject) => {
debug('processing video from %s to %s video compression %o',
name, cname, videoCompression)
const command = ffmpeg()
const outputOptions = [
'-preset fast',
`-crf ${videoCompression}`,
]
if (addChaptersMeta) {
command.input(metaFileName)
outputOptions.push('-map_metadata 1')
}
command.input(name)
.videoCodec('libx264')
.outputOptions(outputOptions)
// .videoFilters("crop='floor(in_w/2)*2:floor(in_h/2)*2'")
.on('start', (command) => {
debug('compression started %o', { command })
})
.on('codecData', (data) => {
debug('compression codec data: %o', data)
total = utils.timemarkToSeconds(data.duration)
})
.on('stderr', (stderr) => {
debug('compression stderr log %o', { message: stderr })
})
.on('progress', (progress) => {
// bail if we dont have total yet
if (!total) {
return
}
debug('compression progress: %o', progress)
const progressed = utils.timemarkToSeconds(progress.timemark)
const percent = progressed / total
if (percent < 1) {
return onProgress(percent)
}
})
.on('error', (err, stdout, stderr) => {
debug('compression errored: %o', { error: err.message, stdout, stderr })
return reject(err)
})
.on('end', () => {
debug('compression ended')
// we are done progressing
onProgress(1)
// rename and obliterate the original
return fs.moveAsync(cname, name, {
overwrite: true,
})
.then(() => {
if (addChaptersMeta) {
return fs.unlink(metaFileName)
}
})
.then(() => {
return resolve()
})
}).save(cname)
})
},
}

View File

@@ -0,0 +1,362 @@
import _ from 'lodash'
import utils from 'fluent-ffmpeg/lib/utils'
import Debug from 'debug'
import ffmpeg from 'fluent-ffmpeg'
import stream from 'stream'
import Bluebird from 'bluebird'
import { path as ffmpegPath } from '@ffmpeg-installer/ffmpeg'
import BlackHoleStream from 'black-hole-stream'
import { fs } from './util/fs'
const debug = Debug('cypress:server:video')
// extra verbose logs for logging individual frames
const debugFrames = Debug('cypress-verbose:server:video:frames')
debug('using ffmpeg from %s', ffmpegPath)
ffmpeg.setFfmpegPath(ffmpegPath)
const deferredPromise = function () {
let reject
let resolve
const promise = new Bluebird((_resolve, _reject) => {
resolve = _resolve
reject = _reject
})
return { promise, resolve, reject }
}
export function generateFfmpegChaptersConfig (tests) {
if (!tests) {
return null
}
const configString = tests.map((test) => {
return test.attempts.map((attempt, i) => {
const { videoTimestamp, wallClockDuration } = attempt
let title = test.title ? test.title.join(' ') : ''
if (i > 0) {
title += `attempt ${i}`
}
return [
'[CHAPTER]',
'TIMEBASE=1/1000',
`START=${videoTimestamp - wallClockDuration}`,
`END=${videoTimestamp}`,
`title=${title}`,
].join('\n')
}).join('\n')
}).join('\n')
return `;FFMETADATA1\n${configString}`
}
export function getMsFromDuration (duration) {
return utils.timemarkToSeconds(duration) * 1000
}
export function getCodecData (src) {
return new Bluebird((resolve, reject) => {
return ffmpeg()
.on('stderr', (stderr) => {
return debug('get codecData stderr log %o', { message: stderr })
}).on('codecData', resolve)
.input(src)
.format('null')
.output(new BlackHoleStream())
.run()
}).tap((data) => {
return debug('codecData %o', {
src,
data,
})
}).tapCatch((err) => {
return debug('getting codecData failed', { err })
})
}
export function getChapters (fileName) {
return new Bluebird((resolve, reject) => {
ffmpeg.ffprobe(fileName, ['-show_chapters'], (err, metadata) => {
if (err) {
return reject(err)
}
resolve(metadata)
})
})
}
export function copy (src, dest) {
debug('copying from %s to %s', src, dest)
return fs
.copy(src, dest, { overwrite: true })
.catch((err) => {
if (err.code === 'ENOENT') {
debug('caught ENOENT error on copy, ignoring %o', { src, dest, err })
return
}
throw err
})
}
type StartOptions = {
// If set, expect input frames as webm chunks.
webmInput?: boolean
// Callback for asynchronous errors in video processing/compression.
onError?: (err: Error, stdout: string, stderr: string) => void
}
export function start (name, options: StartOptions = {}) {
const pt = new stream.PassThrough()
const ended = deferredPromise()
let done = false
let wantsWrite = true
let skippedChunksCount = 0
let writtenChunksCount = 0
_.defaults(options, {
onError () {},
})
const endVideoCapture = function (waitForMoreChunksTimeout = 3000) {
debugFrames('frames written:', writtenChunksCount)
// in some cases (webm) ffmpeg will crash if fewer than 2 buffers are
// written to the stream, so we don't end capture until we get at least 2
if (writtenChunksCount < 2) {
return new Bluebird((resolve) => {
pt.once('data', resolve)
})
.then(() => endVideoCapture())
.timeout(waitForMoreChunksTimeout)
}
done = true
pt.end()
// return the ended promise which will eventually
// get resolve or rejected
return ended.promise
}
const lengths = {}
const writeVideoFrame = function (data) {
// make sure we haven't ended
// our stream yet because paint
// events can linger beyond
// finishing the actual video
if (done) {
return
}
// when `data` is empty, it is sent as an empty Buffer (`<Buffer >`)
// which can crash the process. this can happen if there are
// errors in the video capture process, which are handled later
// on, so just skip empty frames here.
// @see https://github.com/cypress-io/cypress/pull/6818
if (_.isEmpty(data)) {
debugFrames('empty chunk received %o', data)
return
}
if (options.webmInput) {
if (lengths[data.length]) {
// this prevents multiple chunks of webm metadata from being written to the stream
// which would crash ffmpeg
debugFrames('duplicate length frame received:', data.length)
return
}
lengths[data.length] = true
}
writtenChunksCount++
debugFrames('writing video frame')
if (wantsWrite) {
if (!(wantsWrite = pt.write(data))) {
return pt.once('drain', () => {
debugFrames('video stream drained')
wantsWrite = true
})
}
} else {
skippedChunksCount += 1
return debugFrames('skipping video frame %o', { skipped: skippedChunksCount })
}
}
const startCapturing = () => {
return new Bluebird((resolve) => {
const cmd = ffmpeg({
source: pt,
priority: 20,
})
.videoCodec('libx264')
.outputOptions('-preset ultrafast')
.on('start', (command) => {
debug('capture started %o', { command })
return resolve({
cmd,
startedVideoCapture: new Date,
})
}).on('codecData', (data) => {
return debug('capture codec data: %o', data)
}).on('stderr', (stderr) => {
return debug('capture stderr log %o', { message: stderr })
}).on('error', (err, stdout, stderr) => {
debug('capture errored: %o', { error: err.message, stdout, stderr })
// bubble errors up
options.onError?.(err, stdout, stderr)
// reject the ended promise
return ended.reject(err)
}).on('end', () => {
debug('capture ended')
return ended.resolve()
})
// this is to prevent the error "invalid data input" error
// when input frames have an odd resolution
.videoFilters(`crop='floor(in_w/2)*2:floor(in_h/2)*2'`)
if (options.webmInput) {
cmd
.inputFormat('webm')
// assume 18 fps. This number comes from manual measurement of avg fps coming from firefox.
// TODO: replace this with the 'vfr' option below when dropped frames issue is fixed.
.inputFPS(18)
// 'vsync vfr' (variable framerate) works perfectly but fails on top page navigation
// since video timestamp resets to 0, timestamps already written will be dropped
// .outputOption('-vsync vfr')
} else {
cmd
.inputFormat('image2pipe')
.inputOptions('-use_wallclock_as_timestamps 1')
}
return cmd.save(name)
})
}
return startCapturing()
.then(({ cmd, startedVideoCapture }: any) => {
return {
_pt: pt,
cmd,
endVideoCapture,
writeVideoFrame,
startedVideoCapture,
}
})
}
// Progress callback called with percentage `0 <= p <= 1` of compression progress.
type OnProgress = (p: number) => void
export async function process (name, cname, videoCompression, ffmpegchaptersConfig, onProgress: OnProgress = function () {}) {
const metaFileName = `${name}.meta`
const maybeGenerateMetaFile = Bluebird.method(() => {
if (!ffmpegchaptersConfig) {
return false
}
// Writing the metadata to filesystem is necessary because fluent-ffmpeg is just a wrapper of ffmpeg command.
return fs.writeFile(metaFileName, ffmpegchaptersConfig).then(() => true)
})
const addChaptersMeta = await maybeGenerateMetaFile()
let total = null
return new Bluebird((resolve, reject) => {
debug('processing video from %s to %s video compression %o',
name, cname, videoCompression)
const command = ffmpeg()
const outputOptions = [
'-preset fast',
`-crf ${videoCompression}`,
]
if (addChaptersMeta) {
command.input(metaFileName)
outputOptions.push('-map_metadata 1')
}
command.input(name)
.videoCodec('libx264')
.outputOptions(outputOptions)
// .videoFilters("crop='floor(in_w/2)*2:floor(in_h/2)*2'")
.on('start', (command) => {
debug('compression started %o', { command })
})
.on('codecData', (data) => {
debug('compression codec data: %o', data)
total = utils.timemarkToSeconds(data.duration)
})
.on('stderr', (stderr) => {
debug('compression stderr log %o', { message: stderr })
})
.on('progress', (progress) => {
// bail if we dont have total yet
if (!total) {
return
}
debug('compression progress: %o', progress)
const progressed = utils.timemarkToSeconds(progress.timemark)
// @ts-ignore
const percent = progressed / total
if (percent < 1) {
return onProgress(percent)
}
})
.on('error', (err, stdout, stderr) => {
debug('compression errored: %o', { error: err.message, stdout, stderr })
return reject(err)
})
.on('end', async () => {
debug('compression ended')
// we are done progressing
onProgress(1)
// rename and obliterate the original
await fs.move(cname, name, {
overwrite: true,
})
if (addChaptersMeta) {
await fs.unlink(metaFileName)
}
resolve()
}).save(cname)
})
}

View File

@@ -1,5 +1,5 @@
const { expect, sinon } = require('../spec_helper')
import videoCapture from '../../lib/video_capture'
import * as videoCapture from '../../lib/video_capture'
import path from 'path'
import fse from 'fs-extra'
import os from 'os'

View File

@@ -788,6 +788,9 @@ const e2e = {
// Emulate no typescript environment
CYPRESS_INTERNAL_NO_TYPESCRIPT: options.noTypeScript ? '1' : '0',
// disable frame skipping to make quick Chromium tests have matching snapshots/working video
CYPRESS_EVERY_NTH_FRAME: 1,
// force file watching for use with --no-exit
...(options.noExit ? { CYPRESS_INTERNAL_FORCE_FILEWATCH: '1' } : {}),
})

View File

@@ -255,6 +255,33 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
expect(cfg).ok
})
})
// https://github.com/cypress-io/cypress/issues/17614
it('only attaches warning to non-chrome browsers when chromeWebSecurity:true', async function () {
config.get.restore()
sinon.stub(config, 'get').returns({
integrationFolder,
browsers: [{ family: 'chromium', name: 'Canary' }, { family: 'some-other-family', name: 'some-other-name' }],
chromeWebSecurity: true,
})
await this.project.initializeConfig()
.then(() => {
const cfg = this.project.getConfig()
expect(cfg.chromeWebSecurity).eq(true)
expect(cfg.browsers).deep.eq([
{
family: 'chromium',
name: 'Canary',
},
{
family: 'some-other-family',
name: 'some-other-name',
},
])
})
})
})
context('#initializeConfig', function () {

View File

@@ -3,7 +3,7 @@ set e+x
echo "This script should be run from cypress's root"
name=cypress/browsers:node14.16.0-chrome90-ff88
name=cypress/browsers:node14.17.0-chrome91-ff89
echo "Pulling CI container $name"
docker pull $name

View File

@@ -17191,10 +17191,10 @@ electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.378, electron-to-chromi
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz#857e310ca00f0b75da4e1db6ff0e073cc4a91ddf"
integrity sha512-Mfz4FIB4FSvEwBpDfdipRIrwd6uo8gUDoRDF4QEYb4h4tSuI3ov594OrjU6on042UlFHouIJpClDODGkPcBSbg==
electron@12.0.0-beta.14:
version "12.0.0-beta.14"
resolved "https://registry.npmjs.org/electron/-/electron-12.0.0-beta.14.tgz#f8c40c7e479879c305e519380e710c0a357aa734"
integrity sha512-PYM+EepIEj9kLePXEb9gIxzZk5H4zM7LGg5iw60OHt+SYEECPNFJmPj3N6oHKu3W+KrCG7285Vgz2ZCp1u0kKA==
electron@13.1.7:
version "13.1.7"
resolved "https://registry.yarnpkg.com/electron/-/electron-13.1.7.tgz#7e17f5c93a8d182a2a486884fed3dc34ab101be9"
integrity sha512-sVfpP/0s6a82FK32LMuEe9L+aWZw15u3uYn9xUJArPjy4OZHteE6yM5871YCNXNiDnoCLQ5eqQWipiVgHsf8nQ==
dependencies:
"@electron/get" "^1.0.1"
"@types/node" "^14.6.2"