@revoloo/cypress6
Version:
Cypress.io end to end testing tool
378 lines (316 loc) • 11.5 kB
JSX
// @ts-check
import cs from 'classnames'
import _ from 'lodash'
import React, { Component } from 'react'
import { observer } from 'mobx-react'
import Loader from 'react-loader'
import Tooltip from '@cypress/react-tooltip'
import FileOpener from './file-opener'
import ipc from '../lib/ipc'
import projectsApi from '../projects/projects-api'
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
}
/**
* Returns array of specs sorted with folders first, then file.
* @param {any[]} specs array of specs with random order of 'file'/'folder'
*/
const sortedSpecList = (specs) => {
let list = []
let folders = []
let files = []
_.map(specs, (spec) => {
if (spec.hasChildren) {
folders.push(spec)
} else {
files.push(spec)
}
})
list = list.concat(folders)
list = list.concat(files)
return list
}
// Note: this component can be mounted and unmounted
// if you need to persist the data through mounts, "save" it in the specsStore
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
// @ts-ignore
if (window.Cypress) {
// expose project object for testing
// @ts-ignore
window.__project = this.props.project
}
this.state = {
firstTestBannerDismissed: false,
}
}
render () {
if (specsStore.isLoading) return <Loader color='#888' scale={0.5}/>
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
if (hasNoSpecs) {
return this._empty()
}
const areTestsRunning = this._areTestsRunning()
// 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'>
{this._firstTestBanner()}
<header>
<div className={cs('search', {
'show-clear-filter': !!specsStore.filter,
})}>
<label htmlFor='filter'>
<i className='fas fa-search' />
</label>
<input
id='filter'
className='filter'
placeholder='Search...'
value={specsStore.filter || ''}
ref={this.filterRef}
onChange={this._updateFilter}
onKeyUp={this._executeFilterAction}
/>
<Tooltip
title='Clear search'
className='browser-info-tooltip cy-tooltip'
>
<a className='clear-filter fas fa-times' onClick={this._clearFilter} />
</Tooltip>
</div>
</header>
{this._specsList()}
</div>
)
}
_specsList () {
if (specsStore.filter && !specsStore.specs.length) {
return (
<div className='empty-well'>
No specs match your search: "<strong>{specsStore.filter}</strong>"
<br/>
<a onClick={() => {
this._clearFilter()
this.filterRef.current.focus()
}} className='btn btn-link'>
<i className='fas fa-times'/> Clear search
</a>
</div>
)
}
return (
<ul className='specs-list list-as-table'>
{_.map(specsStore.specs, (spec) => this._specItem(spec, 0))}
</ul>
)
}
_specItem (spec, nestingLevel) {
return spec.hasChildren ? this._folderContent(spec, nestingLevel) : this._specContent(spec, nestingLevel)
}
_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) {
return isChosen ? 'far fa-dot-circle green' : 'far fa-file'
}
_clearFilter = () => {
const { id, path } = this.props.project
specsStore.clearFilter({ id, path })
}
_updateFilter = (e) => {
const { id, path } = this.props.project
specsStore.setFilter({ id, path }, e.target.value)
}
_executeFilterAction = (e) => {
if (e.key === 'Escape') {
this._clearFilter()
}
}
_selectSpec (spec, e) {
e.preventDefault()
e.stopPropagation()
if (specsStore.isChosen(spec)) return
const { project } = this.props
specsStore.setSelectedSpec(spec)
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) {
e.preventDefault()
e.stopPropagation()
specsStore.setExpandSpecChildren(specFolderPath, isExpanded)
specsStore.setExpandSpecFolder(specFolderPath, true)
}
_selectSpecFolder (specFolderPath, e) {
e.preventDefault()
specsStore.toggleExpandSpecFolder(specFolderPath)
}
_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
if (specsStore.selectedSpec) {
// only show the button matching current running spec type
if (spec.specType !== specsStore.selectedSpec.specType) {
return <></>
}
if (specsStore.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'}`}>
<div>
<div className="folder-name" onClick={this._selectSpecFolder.bind(this, spec)}>
<i className={`folder-collapse-icon fas fa-fw ${isExpanded ? 'fa-caret-down' : 'fa-caret-right'}`} />
{nestingLevel !== 0 ? <i className={`far fa-fw ${isExpanded ? 'fa-folder-open' : 'fa-folder'}`} /> : null}
{
nestingLevel === 0 ?
<>
{spec.displayName} tests
{specsStore.specHasFolders(spec) ?
<span>
<a onClick={this._setExpandRootFolder.bind(this, spec, false)}>collapse all</a>{' | '}
<a onClick={this._setExpandRootFolder.bind(this, spec, true)}>expand all</a>
</span> :
null
}
</> :
spec.displayName
}
{nestingLevel === 0 ? getSpecRunButton() : <></>}
</div>
{
isExpanded ?
<div>
<ul className={`list-as-table ${specType}`}>
{_.map(sortedSpecList(spec.children), (spec) => this._specItem(spec, nestingLevel + 1))}
</ul>
</div> :
null
}
</div>
</li>
)
}
_specContent (spec, nestingLevel) {
const fileDetails = {
absoluteFile: spec.absolute,
originalFile: spec.relative,
relativeFile: spec.relative,
}
const isActive = specsStore.isChosen(spec)
const className = cs(`file level-${nestingLevel}`, { active: isActive })
return (
<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(isActive)}`} />
{spec.displayName}
</div>
</a>
<FileOpener fileDetails={fileDetails} className="file-open-in-ide" />
</li>
)
}
_empty () {
return (
<div className='specs'>
<div className='empty-well'>
<h5>
No files found in
<code onClick={this._openIntegrationFolder.bind(this)}>
{this.props.project.integrationFolder}
</code>
</h5>
<a className='helper-docs-link' onClick={this._openHelp}>
<i className='fas fa-question-circle' />{' '}
Need help?
</a>
</div>
</div>
)
}
_firstTestBanner () {
if (!this.props.project.isNew || this.state.firstTestBannerDismissed) return
return (
<div className="first-test-banner alert alert-info alert-dismissible">
<p>We've created some sample tests around key Cypress concepts. Run the first one or create your own test file.</p>
<p><a onClick={this._openHelp}>How to write tests</a></p>
<button className="close" onClick={this._removeFirstTestBanner.bind(this)}><span>×</span></button>
</div>
)
}
_openIntegrationFolder () {
ipc.openFinder(this.props.project.integrationFolder)
}
_openHelp (e) {
e.preventDefault()
ipc.externalOpen('https://on.cypress.io/writing-first-test')
}
_removeFirstTestBanner () {
this.setState({ firstTestBannerDismissed: true })
}
}
export default SpecsList