mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-27 19:39:29 -06:00
feat: run only filtered specs (#8007)
Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
This commit is contained in:
1
cli/types/cypress.d.ts
vendored
1
cli/types/cypress.d.ts
vendored
@@ -218,6 +218,7 @@ declare namespace Cypress {
|
||||
name: string // "config_passing_spec.coffee"
|
||||
relative: string // "cypress/integration/config_passing_spec.coffee" or "__all" if clicked all specs button
|
||||
absolute: string
|
||||
specFilter?: string // optional spec filter used by the user
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -201,12 +201,15 @@ describe('Specs List', function () {
|
||||
|
||||
it('triggers browser launch on click of button', () => {
|
||||
cy.contains('.all-tests', 'Run all specs').click()
|
||||
.find('.fa-dot-circle')
|
||||
.then(function () {
|
||||
const launchArgs = this.ipc.launchBrowser.lastCall.args
|
||||
|
||||
expect(launchArgs[0].browser.name).to.eq('chrome')
|
||||
expect(launchArgs[0].browser.name, 'browser name').to.eq('chrome')
|
||||
|
||||
expect(launchArgs[0].spec.name).to.eq('All Specs')
|
||||
expect(launchArgs[0].spec.name, 'spec name').to.eq('All Specs')
|
||||
|
||||
expect(launchArgs[0].specFilter, 'spec filter').to.eq(null)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -406,13 +409,23 @@ describe('Specs List', function () {
|
||||
this.ipc.getSpecs.yields(null, this.specs)
|
||||
this.openProject.resolve(this.config)
|
||||
|
||||
cy.contains('.all-tests', 'Run all specs')
|
||||
cy.get('.filter').type('new')
|
||||
})
|
||||
|
||||
it('displays only matching spec', () => {
|
||||
it('displays only matching spec', function () {
|
||||
cy.get('.specs-list .file')
|
||||
.should('have.length', 1)
|
||||
.and('contain', 'account_new_spec.coffee')
|
||||
|
||||
cy.contains('.all-tests', 'Run 1 spec').click()
|
||||
.find('.fa-dot-circle')
|
||||
.then(() => {
|
||||
expect(this.ipc.launchBrowser).to.have.property('called').equal(true)
|
||||
const launchArgs = this.ipc.launchBrowser.lastCall.args
|
||||
|
||||
expect(launchArgs[0].specFilter, 'spec filter').to.eq('new')
|
||||
})
|
||||
})
|
||||
|
||||
it('only shows matching folders', () => {
|
||||
@@ -427,6 +440,8 @@ describe('Specs List', function () {
|
||||
|
||||
cy.get('.specs-list .file')
|
||||
.should('have.length', this.numSpecs)
|
||||
|
||||
cy.contains('.all-tests', 'Run all specs')
|
||||
})
|
||||
|
||||
it('clears the filter if the user press ESC key', function () {
|
||||
@@ -435,6 +450,9 @@ describe('Specs List', function () {
|
||||
|
||||
cy.get('.specs-list .file')
|
||||
.should('have.length', this.numSpecs)
|
||||
|
||||
cy.contains('.all-tests', 'Run all specs')
|
||||
.find('.fa-play')
|
||||
})
|
||||
|
||||
it('shows empty message if no results', function () {
|
||||
@@ -442,6 +460,17 @@ 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 () {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
it('clears and focuses the filter field when clear search is clicked', function () {
|
||||
@@ -460,6 +489,29 @@ describe('Specs List', function () {
|
||||
expect(JSON.parse(win.localStorage[`specsFilter-${this.config.projectId}-/foo/bar`])).to.equal('new')
|
||||
})
|
||||
})
|
||||
|
||||
it('does not update run button label while running', function () {
|
||||
cy.contains('.all-tests', 'Run 1 spec').click()
|
||||
// mock opened browser and running tests
|
||||
// to force "Stop" button to show up
|
||||
cy.window().its('__project').then((project) => {
|
||||
project.browserOpened()
|
||||
})
|
||||
|
||||
// the button has its its label reflect the running specs
|
||||
cy.contains('.all-tests', 'Running 1 spec')
|
||||
.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')
|
||||
.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')
|
||||
.should('not.have.class', 'active')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there\'s a saved filter', function () {
|
||||
@@ -475,6 +527,7 @@ describe('Specs List', function () {
|
||||
this.openProject.resolve(this.config)
|
||||
|
||||
cy.get('.filter').should('have.value', 'app')
|
||||
cy.contains('.all-tests', 'Run 1 spec')
|
||||
})
|
||||
|
||||
it('does not apply it for a different project', function () {
|
||||
@@ -525,7 +578,7 @@ describe('Specs List', function () {
|
||||
cy.get('@firstSpec')
|
||||
.click()
|
||||
.then(function () {
|
||||
expect(this.ipc.closeBrowser).to.be.called
|
||||
expect(this.ipc.closeBrowser).to.have.property('called', true)
|
||||
|
||||
const launchArgs = this.ipc.launchBrowser.lastCall.args
|
||||
|
||||
@@ -544,6 +597,12 @@ describe('Specs List', function () {
|
||||
.should('have.class', 'active')
|
||||
})
|
||||
|
||||
it('shows the running spec label', () => {
|
||||
cy.get('@firstSpec').click()
|
||||
cy.contains('.all-tests', 'Running 1 spec')
|
||||
.find('.fa-dot-circle')
|
||||
})
|
||||
|
||||
it('maintains active selection if specs change', function () {
|
||||
cy.get('@firstSpec').click().then(() => {
|
||||
this.ipc.getSpecs.yield(null, this.specs)
|
||||
@@ -682,7 +741,8 @@ describe('Specs List', function () {
|
||||
})
|
||||
|
||||
it('opens in preferred opener', function () {
|
||||
cy.get('@button').click().then(() => {
|
||||
cy.get('@button').click()
|
||||
.then(() => {
|
||||
expect(this.ipc.openFile).to.be.calledWith({
|
||||
where: this.availableEditors[4],
|
||||
file: '/user/project/cypress/integration/app_spec.coffee',
|
||||
|
||||
@@ -63,7 +63,8 @@ const addProject = (path) => {
|
||||
.return(project)
|
||||
}
|
||||
|
||||
const runSpec = (project, spec, browser) => {
|
||||
// TODO: refactor to take options object
|
||||
const runSpec = (project, spec, browser, specFilter) => {
|
||||
specsStore.setChosenSpec(spec)
|
||||
project.setChosenBrowser(browser)
|
||||
|
||||
@@ -75,6 +76,7 @@ const runSpec = (project, spec, browser) => {
|
||||
spec: spec.file,
|
||||
specType: spec.specType,
|
||||
relative: spec.relative,
|
||||
specFilter,
|
||||
}
|
||||
|
||||
ipc.launchBrowser(launchOptions, (err, data = {}) => {
|
||||
|
||||
@@ -15,12 +15,56 @@ class SpecsList extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.filterRef = React.createRef()
|
||||
// when the specs are running and the user changes the search filter
|
||||
// we still want to show the previous button label to reflect what
|
||||
// is currently running
|
||||
this.runAllSavedLabel = null
|
||||
|
||||
if (window.Cypress) {
|
||||
// expose project object for testing
|
||||
window.__project = this.props.project
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
if (specsStore.isLoading) return <Loader color='#888' scale={0.5}/>
|
||||
|
||||
if (!specsStore.filter && !specsStore.specs.length) return this._empty()
|
||||
const filteredSpecs = specsStore.getFilteredSpecs()
|
||||
|
||||
const hasSpecFilter = specsStore.filter
|
||||
const numberOfShownSpecs = filteredSpecs.length
|
||||
const hasNoSpecs = !hasSpecFilter && !numberOfShownSpecs
|
||||
|
||||
if (hasNoSpecs) {
|
||||
return this._empty()
|
||||
}
|
||||
|
||||
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>)
|
||||
|
||||
return (
|
||||
<div className='specs'>
|
||||
@@ -47,12 +91,7 @@ class SpecsList extends Component {
|
||||
<a className='clear-filter fas fa-times' onClick={this._clearFilter} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<a onClick={this._selectSpec.bind(this, allSpecsSpec)}
|
||||
title="Run all integration specs together"
|
||||
className={cs('all-tests', { active: specsStore.isChosen(allSpecsSpec) })}>
|
||||
<i className={`fa-fw ${this._allSpecsIcon(specsStore.isChosen(allSpecsSpec))}`} />{' '}
|
||||
{allSpecsSpec.displayName}
|
||||
</a>
|
||||
{runTestsButton}
|
||||
</header>
|
||||
{this._specsList()}
|
||||
</div>
|
||||
@@ -86,8 +125,17 @@ class SpecsList extends Component {
|
||||
return spec.hasChildren ? this._folderContent(spec, nestingLevel) : this._specContent(spec, nestingLevel)
|
||||
}
|
||||
|
||||
_allSpecsIcon (allSpecsChosen) {
|
||||
return allSpecsChosen ? 'far fa-dot-circle green' : 'fas fa-play'
|
||||
_allSpecsIcon () {
|
||||
return this._areTestsRunning() ? 'far fa-dot-circle green' : 'fas fa-play'
|
||||
}
|
||||
|
||||
_areTestsRunning () {
|
||||
if (!this.props.project) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.props.project.browserState === 'opening'
|
||||
|| this.props.project.browserState === 'opened'
|
||||
}
|
||||
|
||||
_specIcon (isChosen) {
|
||||
@@ -117,7 +165,21 @@ class SpecsList extends Component {
|
||||
|
||||
const { project } = this.props
|
||||
|
||||
return projectsApi.runSpec(project, spec, project.chosenBrowser)
|
||||
if (spec.relative === '__all') {
|
||||
if (specsStore.filter) {
|
||||
const filteredSpecs = specsStore.getFilteredSpecs()
|
||||
const numberOfShownSpecs = filteredSpecs.length
|
||||
|
||||
this.runAllSavedLabel = numberOfShownSpecs === 1
|
||||
? 'Running 1 spec' : `Running ${numberOfShownSpecs} specs`
|
||||
} else {
|
||||
this.runAllSavedLabel = 'Running all specs'
|
||||
}
|
||||
} else {
|
||||
this.runAllSavedLabel = 'Running 1 spec'
|
||||
}
|
||||
|
||||
return projectsApi.runSpec(project, spec, project.chosenBrowser, specsStore.filter)
|
||||
}
|
||||
|
||||
_setExpandRootFolder (specFolderPath, isExpanded, e) {
|
||||
|
||||
@@ -25,7 +25,27 @@ const pathsEqual = (path1, path2) => {
|
||||
return path1.replace(pathSeparatorRe, '') === path2.replace(pathSeparatorRe, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters give file objects by spec name substring
|
||||
*/
|
||||
const filterSpecs = (filter, files) => {
|
||||
if (!filter) {
|
||||
return files
|
||||
}
|
||||
|
||||
const filteredFiles = _.filter(files, (spec) => {
|
||||
return spec.name.toLowerCase().includes(filter.toLowerCase())
|
||||
})
|
||||
|
||||
return filteredFiles
|
||||
}
|
||||
|
||||
export class SpecsStore {
|
||||
/**
|
||||
* All spec files
|
||||
*
|
||||
* @memberof SpecsStore
|
||||
*/
|
||||
@observable _files = []
|
||||
@observable chosenSpecPath
|
||||
@observable error
|
||||
@@ -75,7 +95,9 @@ export class SpecsStore {
|
||||
}
|
||||
|
||||
@action setFilter (project, filter = null) {
|
||||
if (!filter) return this.clearFilter(project)
|
||||
if (!filter) {
|
||||
return this.clearFilter(project)
|
||||
}
|
||||
|
||||
localData.set(this.getSpecsFilterId(project), filter)
|
||||
|
||||
@@ -106,12 +128,17 @@ export class SpecsStore {
|
||||
return specOrFolder.children.some((child) => child.isFolder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only specs matching the current filter
|
||||
*
|
||||
* @memberof SpecsStore
|
||||
*/
|
||||
getFilteredSpecs () {
|
||||
return filterSpecs(this.filter, this._files)
|
||||
}
|
||||
|
||||
_tree (files) {
|
||||
if (this.filter) {
|
||||
files = _.filter(files, (spec) => {
|
||||
return spec.name.toLowerCase().includes(this.filter.toLowerCase())
|
||||
})
|
||||
}
|
||||
files = filterSpecs(this.filter, files)
|
||||
|
||||
const tree = _.reduce(files, (root, file) => {
|
||||
const segments = [file.type].concat(file.name.split(pathSeparatorRe))
|
||||
|
||||
@@ -17,6 +17,8 @@ $max-nesting-level: 14;
|
||||
height: 42px;
|
||||
background: #f5f5f5;
|
||||
border-bottom: 1px solid #ddd;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.search {
|
||||
@@ -24,7 +26,7 @@ $max-nesting-level: 14;
|
||||
margin-right: 15px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: calc(100% - 140px);
|
||||
width: calc(100% - 160px);
|
||||
|
||||
label {
|
||||
position: absolute;
|
||||
@@ -91,10 +93,14 @@ $max-nesting-level: 14;
|
||||
pointer-events: none;
|
||||
color: #4c4e63;
|
||||
text-decoration: none;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 8px;
|
||||
position: relative;
|
||||
|
||||
@@ -2,179 +2,240 @@ import { EventEmitter } from 'events'
|
||||
import { itHandlesFileOpening } from '../support/utils'
|
||||
|
||||
describe('controls', function () {
|
||||
beforeEach(function () {
|
||||
cy.fixture('runnables').as('runnables')
|
||||
context('all specs', function () {
|
||||
beforeEach(function () {
|
||||
cy.fixture('runnables').as('runnables')
|
||||
|
||||
this.runner = new EventEmitter()
|
||||
this.runner = new EventEmitter()
|
||||
|
||||
cy.visit('/dist').then((win) => {
|
||||
win.render({
|
||||
runner: this.runner,
|
||||
spec: {
|
||||
name: 'foo.js',
|
||||
relative: 'relative/path/to/foo.js',
|
||||
absolute: '/absolute/path/to/foo.js',
|
||||
},
|
||||
cy.visit('/dist').then((win) => {
|
||||
win.render({
|
||||
runner: this.runner,
|
||||
spec: {
|
||||
relative: '__all',
|
||||
name: '',
|
||||
absolute: '',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('.reporter').then(() => {
|
||||
this.runner.emit('runnables:ready', this.runnables)
|
||||
|
||||
this.runner.emit('reporter:start', {})
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('.reporter').then(() => {
|
||||
this.runner.emit('runnables:ready', this.runnables)
|
||||
|
||||
this.runner.emit('reporter:start', {})
|
||||
it('shows header', () => {
|
||||
cy.contains('.runnable-header', 'All Specs')
|
||||
})
|
||||
})
|
||||
|
||||
describe('tests', function () {
|
||||
context('filtered specs', function () {
|
||||
beforeEach(function () {
|
||||
this.passingTestTitle = this.runnables.suites[0].tests[0].title
|
||||
this.failingTestTitle = this.runnables.suites[0].tests[1].title
|
||||
})
|
||||
cy.fixture('runnables').as('runnables')
|
||||
|
||||
describe('expand and collapse', function () {
|
||||
it('is collapsed by default', function () {
|
||||
cy.contains(this.passingTestTitle)
|
||||
.parents('.collapsible').first()
|
||||
.should('not.have.class', 'is-open')
|
||||
.find('.collapsible-content')
|
||||
.should('not.be.visible')
|
||||
this.runner = new EventEmitter()
|
||||
|
||||
cy.visit('/dist').then((win) => {
|
||||
win.render({
|
||||
runner: this.runner,
|
||||
spec: {
|
||||
relative: '__all',
|
||||
name: '',
|
||||
absolute: '',
|
||||
specFilter: 'cof',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('expand/collapse test manually', function () {
|
||||
beforeEach(function () {
|
||||
cy.get('.reporter').then(() => {
|
||||
this.runner.emit('runnables:ready', this.runnables)
|
||||
|
||||
this.runner.emit('reporter:start', {})
|
||||
})
|
||||
})
|
||||
|
||||
it('shows header', () => {
|
||||
cy.contains('.runnable-header', 'Specs matching "cof"')
|
||||
})
|
||||
})
|
||||
|
||||
context('single spec', function () {
|
||||
beforeEach(function () {
|
||||
cy.fixture('runnables').as('runnables')
|
||||
|
||||
this.runner = new EventEmitter()
|
||||
|
||||
cy.visit('/dist').then((win) => {
|
||||
win.render({
|
||||
runner: this.runner,
|
||||
spec: {
|
||||
name: 'foo.js',
|
||||
relative: 'relative/path/to/foo.js',
|
||||
absolute: '/absolute/path/to/foo.js',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('.reporter').then(() => {
|
||||
this.runner.emit('runnables:ready', this.runnables)
|
||||
|
||||
this.runner.emit('reporter:start', {})
|
||||
})
|
||||
})
|
||||
|
||||
describe('tests', function () {
|
||||
beforeEach(function () {
|
||||
this.passingTestTitle = this.runnables.suites[0].tests[0].title
|
||||
this.failingTestTitle = this.runnables.suites[0].tests[1].title
|
||||
})
|
||||
|
||||
describe('expand and collapse', function () {
|
||||
it('is collapsed by default', function () {
|
||||
cy.contains(this.passingTestTitle)
|
||||
.parents('.collapsible').first().as('testWrapper')
|
||||
.parents('.collapsible').first()
|
||||
.should('not.have.class', 'is-open')
|
||||
.find('.collapsible-content')
|
||||
.should('not.be.visible')
|
||||
})
|
||||
|
||||
it('expands/collapses on click', function () {
|
||||
cy.contains(this.passingTestTitle)
|
||||
.click()
|
||||
describe('expand/collapse test manually', function () {
|
||||
beforeEach(function () {
|
||||
cy.contains(this.passingTestTitle)
|
||||
.parents('.collapsible').first().as('testWrapper')
|
||||
.should('not.have.class', 'is-open')
|
||||
.find('.collapsible-content')
|
||||
.should('not.be.visible')
|
||||
})
|
||||
|
||||
cy.get('@testWrapper')
|
||||
it('expands/collapses on click', function () {
|
||||
cy.contains(this.passingTestTitle)
|
||||
.click()
|
||||
|
||||
cy.get('@testWrapper')
|
||||
.should('have.class', 'is-open')
|
||||
.find('.collapsible-content').should('be.visible')
|
||||
|
||||
cy.contains(this.passingTestTitle)
|
||||
.click()
|
||||
|
||||
cy.get('@testWrapper')
|
||||
.should('not.have.class', 'is-open')
|
||||
.find('.collapsible-content').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('expands/collapses on enter', function () {
|
||||
cy.contains(this.passingTestTitle)
|
||||
.parents('.collapsible-header').first()
|
||||
.focus().type('{enter}')
|
||||
|
||||
cy.get('@testWrapper')
|
||||
.should('have.class', 'is-open')
|
||||
.find('.collapsible-content').should('be.visible')
|
||||
|
||||
cy.contains(this.passingTestTitle)
|
||||
.parents('.collapsible-header').first()
|
||||
.focus().type('{enter}')
|
||||
|
||||
cy.get('@testWrapper')
|
||||
.should('not.have.class', 'is-open')
|
||||
.find('.collapsible-content').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('expands/collapses on space', function () {
|
||||
cy.contains(this.passingTestTitle)
|
||||
.parents('.collapsible-header').first()
|
||||
.focus().type(' ')
|
||||
|
||||
cy.get('@testWrapper')
|
||||
.should('have.class', 'is-open')
|
||||
.find('.collapsible-content').should('be.visible')
|
||||
|
||||
cy.contains(this.passingTestTitle)
|
||||
.parents('.collapsible-header').first()
|
||||
.focus().type(' ')
|
||||
|
||||
cy.get('@testWrapper')
|
||||
.should('not.have.class', 'is-open')
|
||||
.find('.collapsible-content').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('failed tests', function () {
|
||||
it('expands automatically', function () {
|
||||
cy.contains(this.failingTestTitle)
|
||||
.parents('.collapsible').first()
|
||||
.should('have.class', 'is-open')
|
||||
.find('.collapsible-content').should('be.visible')
|
||||
|
||||
cy.contains(this.passingTestTitle)
|
||||
.click()
|
||||
|
||||
cy.get('@testWrapper')
|
||||
.should('not.have.class', 'is-open')
|
||||
.find('.collapsible-content').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('expands/collapses on enter', function () {
|
||||
cy.contains(this.passingTestTitle)
|
||||
.parents('.collapsible-header').first()
|
||||
.focus().type('{enter}')
|
||||
|
||||
cy.get('@testWrapper')
|
||||
.should('have.class', 'is-open')
|
||||
.find('.collapsible-content').should('be.visible')
|
||||
|
||||
cy.contains(this.passingTestTitle)
|
||||
.parents('.collapsible-header').first()
|
||||
.focus().type('{enter}')
|
||||
|
||||
cy.get('@testWrapper')
|
||||
.should('not.have.class', 'is-open')
|
||||
.find('.collapsible-content').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('expands/collapses on space', function () {
|
||||
cy.contains(this.passingTestTitle)
|
||||
.parents('.collapsible-header').first()
|
||||
.focus().type(' ')
|
||||
|
||||
cy.get('@testWrapper')
|
||||
.should('have.class', 'is-open')
|
||||
.find('.collapsible-content').should('be.visible')
|
||||
|
||||
cy.contains(this.passingTestTitle)
|
||||
.parents('.collapsible-header').first()
|
||||
.focus().type(' ')
|
||||
|
||||
cy.get('@testWrapper')
|
||||
.should('not.have.class', 'is-open')
|
||||
.find('.collapsible-content').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('failed tests', function () {
|
||||
it('expands automatically', function () {
|
||||
cy.contains(this.failingTestTitle)
|
||||
.parents('.collapsible').first()
|
||||
.should('have.class', 'is-open')
|
||||
.find('.collapsible-content')
|
||||
.should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('header', function () {
|
||||
it('displays', function () {
|
||||
cy.get('.runnable-header').find('a').should('have.text', 'relative/path/to/foo.js')
|
||||
})
|
||||
|
||||
it('displays tooltip on hover', () => {
|
||||
cy.get('.runnable-header a').first().trigger('mouseover')
|
||||
cy.get('.cy-tooltip').first().should('have.text', 'Open in IDE')
|
||||
})
|
||||
|
||||
itHandlesFileOpening('.runnable-header a', {
|
||||
file: '/absolute/path/to/foo.js',
|
||||
line: 0,
|
||||
column: 0,
|
||||
})
|
||||
})
|
||||
|
||||
describe('progress bar', function () {
|
||||
it('displays', function () {
|
||||
cy.get('.runnable-active').click()
|
||||
cy.get('.command-progress').should('be.visible')
|
||||
})
|
||||
|
||||
it('calculates correct width', function () {
|
||||
const { wallClockStartedAt } = this.runnables.suites[0].suites[0].tests[1].commands[0]
|
||||
|
||||
// take the wallClockStartedAt of this command and add 2500 milliseconds to it
|
||||
// in order to simulate the command having run for 2.5 seconds of the total 4000 timeout
|
||||
const date = new Date(wallClockStartedAt).setMilliseconds(2500)
|
||||
|
||||
cy.clock(date, ['Date'])
|
||||
cy.get('.runnable-active').click()
|
||||
cy.get('.command-progress > span').should(($span) => {
|
||||
expect($span.attr('style')).to.contain('animation-duration: 1500ms')
|
||||
expect($span.attr('style')).to.contain('width: 37.5%')
|
||||
|
||||
// ensures that actual width hits 0 within default timeout
|
||||
expect($span).to.have.css('width', '0px')
|
||||
.find('.collapsible-content')
|
||||
.should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
it('recalculates correct width after being closed', function () {
|
||||
const { wallClockStartedAt } = this.runnables.suites[0].suites[0].tests[1].commands[0]
|
||||
|
||||
// take the wallClockStartedAt of this command and add 1000 milliseconds to it
|
||||
// in order to simulate the command having run for 1 second of the total 4000 timeout
|
||||
const date = new Date(wallClockStartedAt).setMilliseconds(1000)
|
||||
|
||||
cy.clock(date, ['Date'])
|
||||
cy.get('.runnable-active').click()
|
||||
cy.get('.command-progress > span').should(($span) => {
|
||||
expect($span.attr('style')).to.contain('animation-duration: 3000ms')
|
||||
expect($span.attr('style')).to.contain('width: 75%')
|
||||
describe('header', function () {
|
||||
it('displays', function () {
|
||||
cy.get('.runnable-header').find('a').should('have.text', 'relative/path/to/foo.js')
|
||||
})
|
||||
|
||||
// set the clock ahead as if time has passed
|
||||
cy.tick(2000)
|
||||
it('displays tooltip on hover', () => {
|
||||
cy.get('.runnable-header a').first().trigger('mouseover')
|
||||
cy.get('.cy-tooltip').first().should('have.text', 'Open in IDE')
|
||||
})
|
||||
|
||||
cy.get('.runnable-active > .collapsible > .runnable-wrapper').click().click()
|
||||
cy.get('.command-progress > span').should(($span) => {
|
||||
expect($span.attr('style')).to.contain('animation-duration: 1000ms')
|
||||
expect($span.attr('style')).to.contain('width: 25%')
|
||||
itHandlesFileOpening('.runnable-header a', {
|
||||
file: '/absolute/path/to/foo.js',
|
||||
line: 0,
|
||||
column: 0,
|
||||
})
|
||||
})
|
||||
|
||||
describe('progress bar', function () {
|
||||
it('displays', function () {
|
||||
cy.get('.runnable-active').click()
|
||||
cy.get('.command-progress').should('be.visible')
|
||||
})
|
||||
|
||||
it('calculates correct width', function () {
|
||||
const { wallClockStartedAt } = this.runnables.suites[0].suites[0].tests[1].commands[0]
|
||||
|
||||
// take the wallClockStartedAt of this command and add 2500 milliseconds to it
|
||||
// in order to simulate the command having run for 2.5 seconds of the total 4000 timeout
|
||||
const date = new Date(wallClockStartedAt).setMilliseconds(2500)
|
||||
|
||||
cy.clock(date, ['Date'])
|
||||
cy.get('.runnable-active').click()
|
||||
cy.get('.command-progress > span').should(($span) => {
|
||||
expect($span.attr('style')).to.contain('animation-duration: 1500ms')
|
||||
expect($span.attr('style')).to.contain('width: 37.5%')
|
||||
|
||||
// ensures that actual width hits 0 within default timeout
|
||||
expect($span).to.have.css('width', '0px')
|
||||
})
|
||||
})
|
||||
|
||||
it('recalculates correct width after being closed', function () {
|
||||
const { wallClockStartedAt } = this.runnables.suites[0].suites[0].tests[1].commands[0]
|
||||
|
||||
// take the wallClockStartedAt of this command and add 1000 milliseconds to it
|
||||
// in order to simulate the command having run for 1 second of the total 4000 timeout
|
||||
const date = new Date(wallClockStartedAt).setMilliseconds(1000)
|
||||
|
||||
cy.clock(date, ['Date'])
|
||||
cy.get('.runnable-active').click()
|
||||
cy.get('.command-progress > span').should(($span) => {
|
||||
expect($span.attr('style')).to.contain('animation-duration: 3000ms')
|
||||
expect($span.attr('style')).to.contain('width: 75%')
|
||||
})
|
||||
|
||||
// set the clock ahead as if time has passed
|
||||
cy.tick(2000)
|
||||
|
||||
cy.get('.runnable-active > .collapsible > .runnable-wrapper').click().click()
|
||||
cy.get('.command-progress > span').should(($span) => {
|
||||
expect($span.attr('style')).to.contain('animation-duration: 1000ms')
|
||||
expect($span.attr('style')).to.contain('width: 25%')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { Component, ReactElement } from 'react'
|
||||
|
||||
import FileNameOpener from '../lib/file-name-opener'
|
||||
|
||||
const renderRunnableHeader = (children:ReactElement) => <div className="runnable-header">{children}</div>
|
||||
const renderRunnableHeader = (children: ReactElement) => <div className="runnable-header">{children}</div>
|
||||
|
||||
interface RunnableHeaderProps {
|
||||
spec: Cypress.Cypress['spec']
|
||||
@@ -11,9 +11,16 @@ interface RunnableHeaderProps {
|
||||
class RunnableHeader extends Component<RunnableHeaderProps> {
|
||||
render () {
|
||||
const { spec } = this.props
|
||||
|
||||
const relativeSpecPath = spec.relative
|
||||
|
||||
if (spec.relative === '__all') {
|
||||
if (spec.specFilter) {
|
||||
return renderRunnableHeader(
|
||||
<span><span>Specs matching "{spec.specFilter}"</span></span>,
|
||||
)
|
||||
}
|
||||
|
||||
return renderRunnableHeader(
|
||||
<span><span>All Specs</span></span>,
|
||||
)
|
||||
|
||||
@@ -23,20 +23,25 @@ module.exports = {
|
||||
})
|
||||
},
|
||||
|
||||
handleIframe (req, res, config, getRemoteState) {
|
||||
handleIframe (req, res, config, getRemoteState, extraOptions) {
|
||||
const test = req.params[0]
|
||||
const iframePath = cwd('lib', 'html', 'iframe.html')
|
||||
const specFilter = _.get(extraOptions, 'specFilter')
|
||||
|
||||
debug('handle iframe %o', { test })
|
||||
debug('handle iframe %o', { test, specFilter })
|
||||
|
||||
return this.getSpecs(test, config)
|
||||
return this.getSpecs(test, config, extraOptions)
|
||||
.then((specs) => {
|
||||
return this.getJavascripts(config)
|
||||
.then((js) => {
|
||||
const allFilesToSend = js.concat(specs)
|
||||
|
||||
debug('all files to send %o', _.map(allFilesToSend, 'relative'))
|
||||
|
||||
const iframeOptions = {
|
||||
title: this.getTitle(test),
|
||||
domain: getRemoteState().domainName,
|
||||
scripts: JSON.stringify(js.concat(specs)),
|
||||
scripts: JSON.stringify(allFilesToSend),
|
||||
}
|
||||
|
||||
debug('iframe %s options %o', test, iframeOptions)
|
||||
@@ -46,8 +51,8 @@ module.exports = {
|
||||
})
|
||||
},
|
||||
|
||||
getSpecs (spec, config) {
|
||||
debug('get specs %o', { spec })
|
||||
getSpecs (spec, config, extraOptions = {}) {
|
||||
debug('get specs %o', { spec, extraOptions })
|
||||
|
||||
const convertSpecPath = (spec) => {
|
||||
// get the absolute path to this spec and
|
||||
@@ -59,15 +64,30 @@ module.exports = {
|
||||
return this.prepareForBrowser(convertedSpec, config.projectRoot)
|
||||
}
|
||||
|
||||
const specFilter = _.get(extraOptions, 'specFilter')
|
||||
|
||||
debug('specFilter %o', { specFilter })
|
||||
const specFilterContains = (spec) => {
|
||||
// only makes sense if there is specFilter string
|
||||
// the filter should match the logic in
|
||||
// desktop-gui/src/specs/specs-store.js
|
||||
return spec.relative.toLowerCase().includes(specFilter.toLowerCase())
|
||||
}
|
||||
const specFilterFn = specFilter ? specFilterContains : R.T
|
||||
|
||||
const getSpecsHelper = () => {
|
||||
// grab all of the specs if this is ci
|
||||
const experimentalComponentTestingEnabled = _.get(config, 'resolved.experimentalComponentTesting.value', false)
|
||||
|
||||
if (spec === '__all') {
|
||||
debug('returning all specs')
|
||||
|
||||
return specsUtil.find(config)
|
||||
.then(R.tap((specs) => {
|
||||
return debug('found __all specs %o', specs)
|
||||
})).filter((spec) => {
|
||||
}))
|
||||
.filter(specFilterFn)
|
||||
.filter((spec) => {
|
||||
if (experimentalComponentTestingEnabled) {
|
||||
return spec.specType === 'integration'
|
||||
}
|
||||
|
||||
@@ -127,12 +127,18 @@ const handleEvent = function (options, bus, event, id, type, arg) {
|
||||
case 'launch:browser':
|
||||
// is there a way to lint the arguments received?
|
||||
debug('launching browser for \'%s\' spec: %o', arg.specType, arg.spec)
|
||||
debug('full list of options %o', arg)
|
||||
|
||||
// the "arg" should have objects for
|
||||
// - browser
|
||||
// - spec (with fields)
|
||||
// name, absolute, relative
|
||||
// - specType: "integration" | "component"
|
||||
const fullSpec = _.merge({}, arg.spec, { specType: arg.specType })
|
||||
// - specFilter (optional): the string user searched for
|
||||
const fullSpec = _.merge({}, arg.spec, {
|
||||
specType: arg.specType,
|
||||
specFilter: arg.specFilter,
|
||||
})
|
||||
|
||||
return openProject.launch(arg.browser, fullSpec, {
|
||||
projectRoot: options.projectRoot,
|
||||
|
||||
@@ -501,6 +501,8 @@ class Project extends EE {
|
||||
}
|
||||
|
||||
getSpecUrl (absoluteSpecPath, specType) {
|
||||
debug('get spec url: %s for spec type %s', absoluteSpecPath, specType)
|
||||
|
||||
return this.getConfig()
|
||||
.then((cfg) => {
|
||||
// if we don't have a absoluteSpecPath or its __all
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const path = require('path')
|
||||
const la = require('lazy-ass')
|
||||
const check = require('check-more-types')
|
||||
const _ = require('lodash')
|
||||
const debug = require('debug')('cypress:server:routes')
|
||||
|
||||
const AppData = require('./util/app_data')
|
||||
@@ -46,7 +47,17 @@ module.exports = ({ app, config, getRemoteState, networkProxy, project, onError
|
||||
|
||||
// routing for the dynamic iframe html
|
||||
app.get('/__cypress/iframes/*', (req, res) => {
|
||||
files.handleIframe(req, res, config, getRemoteState)
|
||||
const extraOptions = {
|
||||
specFilter: _.get(project, 'spec.specFilter'),
|
||||
}
|
||||
|
||||
debug('project %o', project)
|
||||
debug('handling iframe for project spec %o', {
|
||||
spec: project.spec,
|
||||
extraOptions,
|
||||
})
|
||||
|
||||
files.handleIframe(req, res, config, getRemoteState, extraOptions)
|
||||
})
|
||||
|
||||
app.all('/__cypress/xhrs/*', (req, res, next) => {
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('Routes', () => {
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
const project = new Project('/path/to/project')
|
||||
this.project = new Project('/path/to/project')
|
||||
|
||||
return Promise.all([
|
||||
// open our https server
|
||||
@@ -168,7 +168,7 @@ describe('Routes', () => {
|
||||
// and open our cypress server
|
||||
(this.server = new Server(new Watchers())),
|
||||
|
||||
this.server.open(cfg, project)
|
||||
this.server.open(cfg, this.project)
|
||||
.spread((port) => {
|
||||
if (initialUrl) {
|
||||
this.server._onDomainSet(initialUrl)
|
||||
@@ -202,6 +202,7 @@ describe('Routes', () => {
|
||||
Fixtures.remove()
|
||||
this.session.destroy()
|
||||
preprocessor.close()
|
||||
this.project = null
|
||||
|
||||
return Promise.join(
|
||||
this.server.close(),
|
||||
@@ -1137,6 +1138,26 @@ describe('Routes', () => {
|
||||
expect(body).to.eq(contents)
|
||||
})
|
||||
})
|
||||
|
||||
it('can send back tests matching spec filter', function () {
|
||||
// only returns tests with "sub_test" in their names
|
||||
const contents = removeWhitespace(Fixtures.get('server/expected_todos_filtered_tests_iframe.html'))
|
||||
|
||||
this.project.spec = {
|
||||
specFilter: 'sub_test',
|
||||
}
|
||||
|
||||
return this.rp('http://localhost:2020/__cypress/iframes/__all')
|
||||
.then((res) => {
|
||||
expect(res.statusCode).to.eq(200)
|
||||
|
||||
const body = cleanResponseBody(res.body)
|
||||
|
||||
console.log(body)
|
||||
|
||||
expect(body).to.eq(contents)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('no-server', () => {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>All Tests</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
document.domain = 'localhost';
|
||||
|
||||
(function(parent) {
|
||||
var Cypress = window.Cypress = parent.Cypress;
|
||||
if (!Cypress) {
|
||||
throw new Error("Tests cannot run without a reference to Cypress!");
|
||||
}
|
||||
return Cypress.onSpecWindow(window, [{"absolute":"/<path-to-project>/todos/tests/_support/spec_helper.js","relative":"tests/_support/spec_helper.js","relativeUrl":"/__cypress/tests?p=tests/_support/spec_helper.js"},{"absolute":"/<path-to-project>/todos/tests/etc/etc.js","relative":"tests/etc/etc.js","relativeUrl":"/__cypress/tests?p=tests/etc/etc.js"},{"absolute":"/<path-to-project>/todos/tests/sub/sub_test.coffee","relative":"tests/sub/sub_test.coffee","relativeUrl":"/__cypress/tests?p=tests/sub/sub_test.coffee"}]);
|
||||
})(window.opener || window.parent);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -973,6 +973,7 @@ describe('lib/gui/events', () => {
|
||||
absolute: '/path/to/bar',
|
||||
relative: 'to/bar',
|
||||
specType: 'integration',
|
||||
specFilter: undefined,
|
||||
})
|
||||
|
||||
opts.onBrowserOpen()
|
||||
@@ -999,6 +1000,43 @@ describe('lib/gui/events', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('passes specFilter', function () {
|
||||
sinon.stub(openProject, 'launch').callsFake((browser, spec, opts) => {
|
||||
debug('spec was %o', spec)
|
||||
expect(browser, 'browser').to.eq('foo')
|
||||
expect(spec, 'spec').to.deep.equal({
|
||||
name: 'bar',
|
||||
absolute: '/path/to/bar',
|
||||
relative: 'to/bar',
|
||||
specType: 'integration',
|
||||
specFilter: 'network',
|
||||
})
|
||||
|
||||
opts.onBrowserOpen()
|
||||
opts.onBrowserClose()
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
const spec = {
|
||||
name: 'bar',
|
||||
absolute: '/path/to/bar',
|
||||
relative: 'to/bar',
|
||||
}
|
||||
const arg = {
|
||||
browser: 'foo',
|
||||
spec,
|
||||
specType: 'integration',
|
||||
specFilter: 'network',
|
||||
}
|
||||
|
||||
return this.handleEvent('launch:browser', arg).then(() => {
|
||||
expect(this.send.getCall(0).args[1].data).to.include({ browserOpened: true })
|
||||
|
||||
expect(this.send.getCall(1).args[1].data).to.include({ browserClosed: true })
|
||||
})
|
||||
})
|
||||
|
||||
it('wraps error titles if not set', function () {
|
||||
const err = new Error('foo')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user