mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-26 08:59:26 -05:00
Merge branch 'develop' into v6.0-release
This commit is contained in:
@@ -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 () {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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))}`} />
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user