feat: run only filtered specs (#8007)

Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
This commit is contained in:
Gleb Bahmutov
2020-07-30 10:29:21 -04:00
committed by GitHub
parent d246272bca
commit fe96d7cf2a
15 changed files with 527 additions and 183 deletions

View File

@@ -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
}
/**

View File

@@ -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',

View File

@@ -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 = {}) => {

View File

@@ -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) {

View File

@@ -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))

View File

@@ -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;

View File

@@ -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%')
})
})
})
})

View File

@@ -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>,
)

View File

@@ -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'
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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', () => {

View File

@@ -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>

View File

@@ -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')