Merge branch 'develop' into v6.0-release

This commit is contained in:
Jennifer Shehane
2020-11-03 22:05:54 +06:30
committed by GitHub
19 changed files with 403 additions and 152 deletions
@@ -0,0 +1,71 @@
{
"integration": [
{
"name": "app_spec.coffee",
"absolute": "/user/project/cypress/integration/app_spec.coffee",
"relative": "cypress/integration/app_spec.coffee"
},
{
"name": "accounts/account_new_spec.coffee",
"absolute": "/user/project/cypress/integration/accounts/account_new_spec.coffee",
"relative": "cypress/integration/accounts/account_new_spec.coffee"
},
{
"name": "accounts/accounts_list_spec.coffee",
"absolute": "/user/project/cypress/integration/accounts/accounts_list_spec.coffee",
"relative": "cypress/integration/accounts/accounts_list_spec.coffee"
},
{
"name": "admin_users/admin_users_list_spec.coffee",
"absolute": "/user/project/cypress/integration/admin_users/admin_users_list_spec.coffee",
"relative": "cypress/integration/admin_users/admin_users_list_spec.coffee"
},
{
"name": "admin_users/admin.user/foo_list_spec.coffee",
"absolute": "/user/project/cypress/integration/admin_users/admin.user/foo_list_spec.coffee",
"relative": "cypress/integration/admin_users/admin.user/foo_list_spec.coffee"
}
],
"component": [
{
"name": "admin_users/admin/users/bar_list_spec.coffee",
"absolute": "/user/project/cypress/unit/admin_users/admin/users/bar_list_spec.coffee",
"relative": "cypress/unit/admin_users/admin/users/bar_list_spec.coffee"
},
{
"name": "admin_users/admin/users/all/admin/one_list_spec.coffee",
"absolute": "/user/project/cypress/unit/admin_users/admin/users/all/admin/one_list_spec.coffee",
"relative": "cypress/unit/admin_users/admin/users/all/admin/one_list_spec.coffee"
},
{
"name": "admin_users/admin/users/all/admin/two_list_spec.coffee",
"absolute": "/user/project/cypress/unit/admin_users/admin/users/all/admin/two_list_spec.coffee",
"relative": "cypress/unit/admin_users/admin/users/all/admin/two_list_spec.coffee"
},
{
"name": "admin_users/admin/users/all/admin/three_list_spec.coffee",
"absolute": "/user/project/cypress/unit/admin_users/admin/users/all/admin/three_list_spec.coffee",
"relative": "cypress/unit/admin_users/admin/users/all/admin/three_list_spec.coffee"
},
{
"name": "admin_users/admin/users/all/admin/four_list_spec.coffee",
"absolute": "/user/project/cypress/unit/admin_users/admin/users/all/admin/four_list_spec.coffee",
"relative": "cypress/unit/admin_users/admin/users/all/admin/four_list_spec.coffee"
},
{
"name": "admin_users/admin/users/all/admin/five_list_spec.coffee",
"absolute": "/user/project/cypress/unit/admin_users/admin/users/all/admin/five_list_spec.coffee",
"relative": "cypress/unit/admin_users/admin/users/all/admin/five_list_spec.coffee"
},
{
"name": "admin_users/admin/users/all/admin/six_list_spec.coffee",
"absolute": "/user/project/cypress/unit/admin_users/admin/users/all/admin/six_list_spec.coffee",
"relative": "cypress/unit/admin_users/admin/users/all/admin/six_list_spec.coffee"
},
{
"name": "admin_users/admin/users/all/admin/last_list_spec.coffee",
"absolute": "/user/project/cypress/unit/admin_users/admin/users/all/admin/last_list_spec.coffee",
"relative": "cypress/unit/admin_users/admin/users/all/admin/last_list_spec.coffee"
}
]
}
@@ -2,7 +2,7 @@ describe('Specs List', function () {
beforeEach(function () {
cy.fixture('user').as('user')
cy.fixture('config').as('config')
cy.fixture('specs').as('specs')
cy.fixture('specs_with_components').as('specs')
cy.fixture('specs_windows').as('specsWindows')
cy.visitIndex().then(function (win) {
@@ -11,7 +11,9 @@ describe('Specs List', function () {
this.win = win
this.ipc = win.App.ipc
this.numSpecs = this.specs.integration.length + this.specs.unit.length
expect(this.specs.integration.length, 'has integration tests').to.be.gt(0)
expect(this.specs.component.length, 'has component tests').to.be.gt(0)
this.numSpecs = this.specs.integration.length + this.specs.component.length
cy.stub(this.ipc, 'getOptions').resolves({ projectRoot: '/foo/bar' })
cy.stub(this.ipc, 'getCurrentUser').resolves(this.user)
@@ -190,24 +192,27 @@ describe('Specs List', function () {
})
context('run all specs', function () {
const runAllIntegrationSpecsLabel = 'Run 5 integration specs'
it('displays run all specs button', () => {
cy.contains('.all-tests', 'Run all specs').should('have.attr', 'title')
cy.contains('.all-tests', runAllIntegrationSpecsLabel)
.should('have.attr', 'title', 'Run integration specs together')
})
it('has play icon', () => {
cy.contains('.all-tests', 'Run all specs')
cy.contains('.all-tests', runAllIntegrationSpecsLabel)
.find('i').should('have.class', 'fa-play')
})
it('triggers browser launch on click of button', () => {
cy.contains('.all-tests', 'Run all specs').click()
cy.contains('.all-tests', runAllIntegrationSpecsLabel).click()
.find('.fa-dot-circle')
.then(function () {
const launchArgs = this.ipc.launchBrowser.lastCall.args
expect(launchArgs[0].browser.name, 'browser name').to.eq('chrome')
expect(launchArgs[0].spec.name, 'spec name').to.eq('All Specs')
expect(launchArgs[0].spec.name, 'spec name').to.eq('All Integration Specs')
expect(launchArgs[0].specFilter, 'spec filter').to.eq(null)
})
@@ -215,7 +220,7 @@ describe('Specs List', function () {
describe('all specs running in browser', function () {
beforeEach(() => {
cy.contains('.all-tests', 'Run all specs').as('allSpecs').click()
cy.contains('.all-tests', runAllIntegrationSpecsLabel).as('allSpecs').click()
})
it('updates spec icon', function () {
@@ -232,8 +237,9 @@ describe('Specs List', function () {
context('displays list of specs', function () {
it('lists main folders of specs', function () {
cy.get('.folder.level-0').should('have.length', 2)
cy.contains('.folder.level-0', 'integration')
cy.contains('.folder.level-0', 'unit')
cy.contains('.folder.level-0', 'component')
})
it('lists nested folders', () => {
@@ -405,11 +411,13 @@ describe('Specs List', function () {
})
describe('typing the filter', function () {
const runAllIntegrationSpecsLabel = 'Run 5 integration specs'
beforeEach(function () {
this.ipc.getSpecs.yields(null, this.specs)
this.openProject.resolve(this.config)
cy.contains('.all-tests', 'Run all specs')
cy.contains('.all-tests', runAllIntegrationSpecsLabel)
cy.get('.filter').type('new')
})
@@ -418,7 +426,7 @@ describe('Specs List', function () {
.should('have.length', 1)
.and('contain', 'account_new_spec.coffee')
cy.contains('.all-tests', 'Run 1 spec').click()
cy.contains('.all-tests', 'Run 1 integration spec').click()
.find('.fa-dot-circle')
.then(() => {
expect(this.ipc.launchBrowser).to.have.property('called').equal(true)
@@ -441,7 +449,7 @@ describe('Specs List', function () {
cy.get('.specs-list .file')
.should('have.length', this.numSpecs)
cy.contains('.all-tests', 'Run all specs')
cy.contains('.all-tests', runAllIntegrationSpecsLabel)
})
it('clears the filter if the user press ESC key', function () {
@@ -451,7 +459,7 @@ describe('Specs List', function () {
cy.get('.specs-list .file')
.should('have.length', this.numSpecs)
cy.contains('.all-tests', 'Run all specs')
cy.contains('.all-tests', runAllIntegrationSpecsLabel)
.find('.fa-play')
})
@@ -460,17 +468,13 @@ describe('Specs List', function () {
cy.get('.specs-list').should('not.exist')
cy.get('.empty-well').should('contain', 'No specs match your search: "foobarbaz"')
cy.contains('.all-tests', 'No specs')
})
it('disables run all tests if no results', function () {
it('removes run all tests buttons if no results', function () {
cy.get('.filter').clear().type('foobarbaz')
cy.contains('.all-tests', 'No specs').should('be.disabled').click({ force: true })
.then(function () {
expect(this.ipc.launchBrowser).to.have.property('called').equal(false)
})
// the "Run ... tests" buttons should be gone
cy.get('.all-tests').should('not.exist')
})
it('clears and focuses the filter field when clear search is clicked', function () {
@@ -491,7 +495,7 @@ describe('Specs List', function () {
})
it('does not update run button label while running', function () {
cy.contains('.all-tests', 'Run 1 spec').click()
cy.contains('.all-tests', 'Run 1 integration spec').click()
// mock opened browser and running tests
// to force "Stop" button to show up
cy.window().its('__project').then((project) => {
@@ -499,17 +503,17 @@ describe('Specs List', function () {
})
// the button has its its label reflect the running specs
cy.contains('.all-tests', 'Running 1 spec')
cy.contains('.all-tests', 'Running integration tests')
.should('have.class', 'active')
// the button has its label unchanged while the specs are running
cy.get('.filter').clear()
cy.contains('.all-tests', 'Running 1 spec')
cy.contains('.all-tests', 'Running integration tests')
.should('have.class', 'active')
// but once the project stops running tests, the button gets updated
cy.get('.close-browser').click()
cy.contains('.all-tests', 'Run all specs')
cy.contains('.all-tests', 'Run 5 integration specs')
.should('not.have.class', 'active')
})
})
@@ -527,7 +531,7 @@ describe('Specs List', function () {
this.openProject.resolve(this.config)
cy.get('.filter').should('have.value', 'app')
cy.contains('.all-tests', 'Run 1 spec')
cy.contains('.all-tests', 'Run 1 integration spec')
})
it('does not apply it for a different project', function () {
@@ -671,6 +675,57 @@ describe('Specs List', function () {
cy.get('@secondSpec').parent().should('have.class', 'active')
})
})
context('with component tests', function () {
beforeEach(function () {
this.ipc.getSpecs.yields(null, this.specs)
this.openProject.resolve(this.config)
})
it('shows separate run specs buttons', function () {
cy.get('.all-tests').should('have.length', 2)
cy.contains('.folder-name', 'integration tests')
.contains('.all-tests', 'Run 5 integration specs')
cy.contains('.folder-name', 'component tests')
.contains('.all-tests', 'Run 8 component specs')
})
it('runs all component tests together', function () {
cy.contains('.all-tests', 'Run 8 component specs').click()
// all other "Run .." buttons should disappear
cy.get('.all-tests').should('have.length', 1)
// and the label changes
cy.contains('.folder-name', 'component tests')
.contains('.all-tests', 'Running component tests').should('be.visible')
.and('have.class', 'active')
})
it('runs single component spec', function () {
cy.contains('bar_list_spec.coffee').click()
.parent()
.should('have.class', 'active')
// all other "Run .." buttons should disappear
cy.get('.all-tests').should('have.length', 1)
// and the label changes
cy.contains('.folder-name', 'component tests')
.contains('.all-tests', 'Running 1 spec').should('be.visible')
// the button does not get the class active, it stays with the file
.and('not.have.class', 'active')
})
it('filters all spec types using filter', function () {
cy.get('.filter').type('fo')
cy.contains('.all-tests', 'Run 1 integration spec')
cy.contains('.all-tests', 'Run 1 component spec')
cy.log('**clearing the search**')
cy.get('.filter').clear()
cy.contains('.all-tests', 'Run 5 integration specs')
cy.contains('.all-tests', 'Run 8 component specs')
})
})
})
describe('spec list updates', function () {
@@ -3,14 +3,16 @@ import { action, computed, observable } from 'mobx'
export default class Folder {
@observable path
@observable displayName
@observable specType
@observable isExpanded = true
@observable children = []
isFolder = true
constructor ({ path, displayName }) {
constructor ({ path, displayName, specType }) {
this.path = path
this.displayName = displayName
this.specType = specType || 'integration'
}
@computed get hasChildren () {
+2 -2
View File
@@ -8,7 +8,7 @@ export default class Spec {
@observable displayName
// TODO clarify the role of "type" vs "specType"
@observable type
@observable specType // integration | component
@observable specType // "integration" | "component"
@observable isChosen = false
constructor ({ path, name, absolute, relative, displayName, type, specType }) {
@@ -18,7 +18,7 @@ export default class Spec {
this.relative = relative
this.displayName = displayName
this.type = type
this.specType = specType
this.specType = specType || 'integration'
}
@computed get hasChildren () {
+68 -27
View File
@@ -1,3 +1,5 @@
// @ts-check
import cs from 'classnames'
import _ from 'lodash'
import React, { Component } from 'react'
@@ -8,7 +10,23 @@ import Tooltip from '@cypress/react-tooltip'
import FileOpener from './file-opener'
import ipc from '../lib/ipc'
import projectsApi from '../projects/projects-api'
import specsStore, { allSpecsSpec } from './specs-store'
import specsStore, { allIntegrationSpecsSpec, allComponentSpecsSpec } from './specs-store'
/**
* Returns a label text for a button.
* @param {boolean} areTestsAlreadyRunning To form the message "running" vs "run"
* @param {'integration'|'component'} specType Spec type should be included in the label
* @param {number} specsN Number of specs to run or already running
*/
const formRunButtonLabel = (areTestsAlreadyRunning, specType, specsN) => {
if (areTestsAlreadyRunning) {
return `Running ${specType} tests`
}
const label = specsN === 1 ? `Run 1 ${specType} spec` : `Run ${specsN} ${specType} specs`
return label
}
@observer
class SpecsList extends Component {
@@ -20,8 +38,10 @@ class SpecsList extends Component {
// is currently running
this.runAllSavedLabel = null
// @ts-ignore
if (window.Cypress) {
// expose project object for testing
// @ts-ignore
window.__project = this.props.project
}
}
@@ -31,6 +51,9 @@ class SpecsList extends Component {
const filteredSpecs = specsStore.getFilteredSpecs()
const integrationSpecsN = _.filter(filteredSpecs, { specType: 'integration' }).length
const componentSpecsN = _.filter(filteredSpecs, { specType: 'component' }).length
const hasSpecFilter = specsStore.filter
const numberOfShownSpecs = filteredSpecs.length
const hasNoSpecs = !hasSpecFilter && !numberOfShownSpecs
@@ -40,31 +63,10 @@ class SpecsList extends Component {
}
const areTestsRunning = this._areTestsRunning()
let runSpecsLabel = allSpecsSpec.displayName
let runButtonDisabled = false
if (areTestsRunning && this.runAllSavedLabel) {
runSpecsLabel = this.runAllSavedLabel
} else {
if (hasSpecFilter) {
if (numberOfShownSpecs < 1) {
runSpecsLabel = 'No specs'
runButtonDisabled = true
} else {
const specLabel = numberOfShownSpecs === 1 ? 'spec' : 'specs'
runSpecsLabel = `Run ${numberOfShownSpecs} ${specLabel}`
}
}
}
const runTestsButton = (<button onClick={this._selectSpec.bind(this, allSpecsSpec)}
disabled={runButtonDisabled}
title="Run all integration specs together"
className={cs('btn-link all-tests', { active: specsStore.isChosen(allSpecsSpec) })}>
<i className={`fa-fw ${this._allSpecsIcon()}`} />{' '}
{runSpecsLabel}
</button>)
// store in the component for ease of sharing with other methods
this.integrationLabel = formRunButtonLabel(areTestsRunning, 'integration', integrationSpecsN)
this.componentLabel = formRunButtonLabel(areTestsRunning, 'component', componentSpecsN)
return (
<div className='specs'>
@@ -91,7 +93,6 @@ class SpecsList extends Component {
<a className='clear-filter fas fa-times' onClick={this._clearFilter} />
</Tooltip>
</div>
{runTestsButton}
</header>
{this._specsList()}
</div>
@@ -162,9 +163,12 @@ class SpecsList extends Component {
_selectSpec (spec, e) {
e.preventDefault()
e.stopPropagation()
const { project } = this.props
this.selectedSpec = spec
if (spec.relative === '__all') {
if (specsStore.filter) {
const filteredSpecs = specsStore.getFilteredSpecs()
@@ -198,6 +202,38 @@ class SpecsList extends Component {
_folderContent (spec, nestingLevel) {
const isExpanded = spec.isExpanded
const specType = spec.specType || 'integration'
// only applied to the top level for "integration" and "component" specs
const getSpecRunButton = () => {
const word = this._areTestsRunning() ? 'Running' : 'Run'
let buttonText = spec.displayName === 'integration' ? this.integrationLabel : this.componentLabel
if (this._areTestsRunning()) {
// selected spec must be set
// only show the button matching current running spec type
if (spec.specType !== this.selectedSpec.specType) {
return <></>
}
if (this.selectedSpec.relative !== '__all') {
// we are only running 1 spec
buttonText = `${word} 1 spec`
}
}
const isActive = specType === 'integration'
? specsStore.isChosen(allIntegrationSpecsSpec)
: specsStore.isChosen(allComponentSpecsSpec)
const className = cs('btn-link all-tests', { active: isActive })
return (<button
className={className}
title={`${word} ${specType} specs together`}
onClick={this._selectSpec.bind(this,
spec.displayName === 'integration' ? allIntegrationSpecsSpec : allComponentSpecsSpec)
}><i className={`fa-fw ${this._allSpecsIcon()}`} />{' '}{buttonText}</button>)
}
return (
<li key={spec.path} className={`folder level-${nestingLevel} ${isExpanded ? 'folder-expanded' : 'folder-collapsed'}`}>
@@ -219,6 +255,7 @@ class SpecsList extends Component {
</> :
spec.displayName
}
{nestingLevel === 0 ? getSpecRunButton() : <></>}
</div>
{
isExpanded ?
@@ -231,6 +268,7 @@ class SpecsList extends Component {
}
</div>
</li>
)
}
@@ -241,8 +279,11 @@ class SpecsList extends Component {
relativeFile: spec.relative,
}
const isActive = specsStore.isChosen(spec)
const className = cs(`file level-${nestingLevel}`, { active: isActive })
return (
<li key={spec.path} className={cs(`file level-${nestingLevel}`, { active: specsStore.isChosen(spec) })}>
<li key={spec.path} className={className}>
<a href='#' onClick={this._selectSpec.bind(this, spec)} className="file-name-wrapper">
<div className="file-name">
<i className={`fa-fw ${this._specIcon(specsStore.isChosen(spec))}`} />
+37 -7
View File
@@ -8,15 +8,24 @@ import Folder from './folder-model'
const pathSeparatorRe = /[\\\/]/g
export const allSpecsSpec = new Spec({
name: 'All Specs',
export const allIntegrationSpecsSpec = new Spec({
name: 'All Integration Specs',
absolute: '__all',
relative: '__all',
displayName: 'Run all specs',
specType: 'integration',
})
export const allComponentSpecsSpec = new Spec({
name: 'All Component Specs',
absolute: '__all',
relative: '__all',
displayName: 'Run all component specs',
specType: 'component',
})
const formRelativePath = (spec) => {
return spec === allSpecsSpec ? spec.relative : path.join(spec.type, spec.name)
return spec.relative
}
const pathsEqual = (path1, path2) => {
@@ -63,7 +72,7 @@ export class SpecsStore {
@action setSpecs (specsByType) {
this._files = _.flatten(_.map(specsByType, (specs, type) => {
return _.map(specs, (spec) => {
return _.extend({}, spec, { type })
return _.extend({}, spec, { specType: type })
})
}))
@@ -75,7 +84,23 @@ export class SpecsStore {
}
@action setChosenSpecByRelativePath (relativePath) {
this.chosenSpecPath = relativePath
// find an actual spec using relative path
if (relativePath === allIntegrationSpecsSpec.relative) {
this.chosenSpecPath = relativePath
} else if (relativePath === allComponentSpecsSpec.relative) {
this.chosenSpecPath = relativePath
} else {
const foundSpec = this._files.find((file) => {
return file.relative.endsWith(relativePath)
})
if (foundSpec) {
this.chosenSpecPath = foundSpec.relative
} else {
// a problem: could not find chosen spec
this.chosenSpecPath = null
}
}
}
@action setExpandSpecFolder (spec, isExpanded) {
@@ -141,7 +166,7 @@ export class SpecsStore {
files = filterSpecs(this.filter, files)
const tree = _.reduce(files, (root, file) => {
const segments = [file.type].concat(file.name.split(pathSeparatorRe))
const segments = [file.specType].concat(file.name.split(pathSeparatorRe))
const segmentsPassed = []
let placeholder = root
@@ -150,7 +175,12 @@ export class SpecsStore {
segmentsPassed.push(segment)
const currentPath = path.join(...segmentsPassed)
const isCurrentAFile = i === segments.length - 1
const props = { path: currentPath, displayName: segment }
const props = {
path: currentPath,
displayName: segment,
specType: file.specType,
}
let existing = _.find(placeholder, (file) => {
return pathsEqual(file.path, currentPath)
+9 -5
View File
@@ -73,11 +73,9 @@ $max-nesting-level: 14;
}
.all-tests {
position: relative;
display: inline;
margin-left: auto;
font-size: 13px;
color: #637eb9;
padding: 5px 10px;
&:hover, &:focus {
color: #38589c;
@@ -163,8 +161,14 @@ $max-nesting-level: 14;
flex-direction: row;
i {
display: block;
margin-right: 5px;
font-size: 14px;
margin-right: 0;
}
.all-tests {
i {
font-size: 8px;
}
}
span {
@@ -497,19 +497,6 @@ describe('src/cy/commands/aliasing', () => {
.get('input:first').as('firstInput')
.get('@lastDiv')
})
it('throws when alias is missing \'@\' but matches an available alias', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.eq('Invalid alias: `getAny`.\nYou forgot the `@`. It should be written as: `@getAny`.')
done()
})
cy
.server()
.route('*', {}).as('getAny')
.wait('getAny').then(() => {})
})
})
})
})
@@ -1787,6 +1787,63 @@ describe('network stubbing', { retries: 2 }, function () {
})
})
context('with an intercepted request', function () {
it('can dynamically alias the request', function () {
cy.route2('/foo', (req) => {
req.alias = 'fromInterceptor'
})
.then(() => {
$.get('/foo')
})
.wait('@fromInterceptor')
})
it('can time out on a dynamic alias', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.contain('for the 1st request to the route')
done()
})
cy.route2('/foo', (req) => {
req.alias = 'fromInterceptor'
})
.wait('@fromInterceptor', { timeout: 100 })
})
it('dynamic aliases are fulfilled before route aliases', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.contain('for the 1st request to the route: `fromAs`')
done()
})
cy.route2('/foo', (req) => {
req.alias = 'fromInterceptor'
})
.as('fromAs')
.then(() => {
$.get('/foo')
})
.wait('@fromInterceptor')
// this will fail - dynamic aliasing maintains the existing wait semantics, including that each request can only be waited once
.wait('@fromAs', { timeout: 100 })
})
it('fulfills both dynamic aliases when two are defined', function () {
cy.route2('/foo', (req) => {
req.alias = 'fromInterceptor'
})
.route2('/foo', (req) => {
expect(req.alias).to.be.undefined
req.alias = 'fromInterceptor2'
})
.then(() => {
$.get('/foo')
})
.wait('@fromInterceptor')
.wait('@fromInterceptor2')
})
})
// @see https://github.com/cypress-io/cypress/issues/8695
context('yields request', function () {
it('when not intercepted', function () {
@@ -1842,47 +1899,5 @@ describe('network stubbing', { retries: 2 }, function () {
.then(testResponse('something different', done))
})
})
// NOTE: was undocumented in cy.route2, may not continue to support
// @see https://github.com/cypress-io/cypress/issues/7663
context.skip('indexed aliases', function () {
it('can wait for things that do not make sense but are technically true', function () {
cy.route2('/foo')
.as('foo.bar')
.then(() => {
$.get('/foo')
})
.wait('@foo.bar.1')
.wait('@foo.bar.1') // still only asserting on the 1st response
.wait('@foo.bar.request') // now waiting for the next request
})
it('can wait on the 3rd request using "alias.3"', function () {
cy.route2('/foo')
.as('foo.bar')
.then(() => {
_.times(3, () => {
$.get('/foo')
})
})
.wait('@foo.bar.3')
})
it('can timeout waiting on the 3rd request using "alias.3"', function (done) {
cy.on('fail', (err) => {
expect(err.message).to.contain('No response ever occurred.')
done()
})
cy.route2('/foo')
.as('foo.bar')
.then(() => {
_.times(2, () => {
$.get('/foo')
})
})
.wait('@foo.bar.3', { timeout: 100 })
})
})
})
})
+23 -2
View File
@@ -24,6 +24,13 @@ const throwErr = (arg) => {
}
module.exports = (Commands, Cypress, cy, state) => {
const isDynamicAliasingPossible = () => {
// dynamic aliasing is possible if cy.route2 is enabled and a route with dynamic interception has been defined
return Cypress.config('experimentalNetworkStubbing') && _.find(state('routes'), (route) => {
return _.isFunction(route.handler)
})
}
let userOptions = null
const waitNumber = (subject, ms, options) => {
@@ -107,7 +114,21 @@ module.exports = (Commands, Cypress, cy, state) => {
specifier = _.last(allParts)
}
const aliasObj = cy.getAlias(str, 'wait', log)
let aliasObj
try {
aliasObj = cy.getAlias(str, 'wait', log)
} catch (err) {
// before cy.route2, we could know when an alias did/did not exist, because they
// were declared synchronously. with cy.route2, req.alias can be used to dynamically
// create aliases, so we cannot know at wait-time if an alias exists or not
if (!isDynamicAliasingPossible()) {
throw err
}
// could be a dynamic alias
aliasObj = { alias: str.slice(1) }
}
if (!aliasObj) {
cy.aliasNotFoundFor(str, 'wait', log)
@@ -142,7 +163,7 @@ module.exports = (Commands, Cypress, cy, state) => {
log.set('referencesAlias', aliases)
}
if (!['route', 'route2'].includes(command.get('name'))) {
if (command && !['route', 'route2'].includes(command.get('name'))) {
$errUtils.throwErrByPath('wait.invalid_alias', {
onFail: options._log,
args: { alias },
@@ -56,6 +56,7 @@ export function registerEvents (Cypress: Cypress.Cypress) {
Cypress.on('test:before:run', () => {
// wipe out callbacks, requests, and routes when tests start
state('routes', {})
state('aliasedRequests', [])
})
Cypress.on('net:event', (eventName, frame: NetEventFrames.BaseHttp) => {
@@ -53,6 +53,8 @@ export const onRequestReceived: HandlerFn<NetEventFrames.HttpRequestReceived> =
id: requestId,
request: req,
state: 'Received',
requestWaited: false,
responseWaited: false,
}
const continueFrame: Partial<NetEventFrames.HttpRequestContinue> = {
@@ -111,7 +113,7 @@ export const onRequestReceived: HandlerFn<NetEventFrames.HttpRequestReceived> =
destroy () {
userReq.reply({
forceNetworkError: true,
})
}) // TODO: this misnomer is a holdover from XHR, should be numRequests
},
}
@@ -200,6 +202,15 @@ export const onRequestReceived: HandlerFn<NetEventFrames.HttpRequestReceived> =
resolved = true
})
.then(() => {
if (userReq.alias) {
Cypress.state('aliasedRequests').push({
alias: userReq.alias,
request: request as Request,
})
delete userReq.alias
}
if (!replyCalled) {
// handler function resolved without resolving request, pass on
continueFrame.tryNextRoute = true
@@ -7,33 +7,31 @@ import {
const RESPONSE_WAITED_STATES: RequestState[] = ['ResponseIntercepted', 'Complete']
export function waitForRoute (alias: string, state: Cypress.State, specifier: 'request' | 'response' | string): Request | null {
// if they didn't specify what to wait on, they want to wait on a response
if (!specifier) {
specifier = 'response'
function getPredicateForSpecifier (specifier: string): Partial<Request> {
if (specifier === 'request') {
return { requestWaited: false }
}
// 1. Get route with this alias.
// default to waiting on response
return { responseWaited: false }
}
export function waitForRoute (alias: string, state: Cypress.State, specifier: 'request' | 'response' | string): Request | null {
// 1. Create an array of known requests that have this alias.
// Start with request-level (req.alias = '...') aliases that could be a match.
const candidateRequests = _.filter(state('aliasedRequests'), { alias })
.map(({ request }) => request)
// Now add route-level (cy.route2(...).as()) aliased requests.
const route: Route = _.find(state('routes'), { alias })
if (!route) {
// TODO: once XHR stubbing is removed, this should throw
return null
if (route) {
Array.prototype.push.apply(candidateRequests, _.values(route.requests))
}
// 2. Find the first request without responseWaited/requestWaited/with the correct index
let i = 0
const request = _.find(route.requests, (request) => {
i++
switch (specifier) {
case 'request':
return !request.requestWaited
case 'response':
return !request.responseWaited
default:
return i === Number(specifier)
}
})
// 2. Find the first request without responseWaited/requestWaited
const predicate = getPredicateForSpecifier(specifier)
const request = _.find(candidateRequests, predicate) as Request | undefined
if (!request) {
return null
+6
View File
@@ -60,6 +60,7 @@ declare namespace Cypress {
interface State {
(k: '$autIframe', v?: JQuery<HTMLIFrameElement>): JQuery<HTMLIFrameElement> | undefined
(k: 'routes', v?: RouteMap): RouteMap
(k: 'aliasedRequests', v?: AliasedRequest[]): AliasedRequest[]
(k: 'document', v?: Document): Document
(k: 'window', v?: Window): Window
(k: string, v?: any): any
@@ -72,3 +73,8 @@ declare namespace Cypress {
document: Document
}
}
type AliasedRequest = {
alias: string
request: any
}
@@ -111,6 +111,11 @@ export namespace CyHttpMessages {
* not follow redirects before yielding the response (the 3xx redirect is yielded)
*/
followRedirect?: boolean
/**
* If set, `cy.wait` can be used to await the request/response cycle to complete for this
* request via `cy.wait('@alias')`.
*/
alias?: string
}
export interface IncomingHttpRequest extends IncomingRequest {
+5 -2
View File
@@ -52,6 +52,8 @@ module.exports = {
},
getSpecs (spec, config, extraOptions = {}) {
// when asking for all specs: spec = "__all"
// otherwise it is a relative spec filename like "integration/spec.js"
debug('get specs %o', { spec, extraOptions })
const convertSpecPath = (spec) => {
@@ -65,6 +67,7 @@ module.exports = {
}
const specFilter = _.get(extraOptions, 'specFilter')
const specTypeFilter = _.get(extraOptions, 'specType', 'integration')
debug('specFilter %o', { specFilter })
const specFilterContains = (spec) => {
@@ -87,9 +90,9 @@ module.exports = {
return debug('found __all specs %o', specs)
}))
.filter(specFilterFn)
.filter((spec) => {
.filter((foundSpec) => {
if (experimentalComponentTestingEnabled) {
return spec.specType === 'integration'
return foundSpec.specType === specTypeFilter
}
return true
+1
View File
@@ -49,6 +49,7 @@ module.exports = ({ app, config, getRemoteState, networkProxy, project, onError
app.get('/__cypress/iframes/*', (req, res) => {
const extraOptions = {
specFilter: _.get(project, 'spec.specFilter'),
specType: _.get(project, 'spec.specType', 'integration'),
}
debug('project %o', project)
+1 -1
View File
@@ -104,7 +104,7 @@
"squirrelly": "7.9.2",
"strip-ansi": "6.0.0",
"syntax-error": "1.4.0",
"systeminformation": "4.26.9",
"systeminformation": "4.27.11",
"term-size": "2.1.0",
"through": "2.3.8",
"tough-cookie": "4.0.0",
+4 -4
View File
@@ -30466,10 +30466,10 @@ syntax-error@1.4.0, syntax-error@^1.1.1:
dependencies:
acorn-node "^1.2.0"
systeminformation@4.26.9:
version "4.26.9"
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.26.9.tgz#ecc725a162c0c7d8d48226f97637a5f590042ffd"
integrity sha512-69MTIX9j//wnteJzQWuGL6FiMxlfS3wTWyhROI6pNUaQALYqJb3W9VtLVcEcKFOjO1vrGRgilJVFzJeRRi//Pg==
systeminformation@4.27.11:
version "4.27.11"
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.27.11.tgz#6dbe96e48091444f80dab6c05ee1901286826b60"
integrity sha512-U7bigXbOnsB8k1vNHS0Y13RCsRz5/UohiUmND+3mMUL6vfzrpbe/h4ZqewowB+B+tJNnmGFDj08Z8xGfYo45dQ==
table@4.0.2:
version "4.0.2"