@revoloo/cypress6
Version:
Cypress.io end to end testing tool
414 lines (357 loc) • 10.9 kB
JSX
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 ipc from '../lib/ipc'
import { configFileFormatted } from '../lib/config-file-formatted'
import authStore from '../auth/auth-store'
import RunsStore from './runs-store'
import errors from '../lib/errors'
import runsApi from './runs-api'
import projectsApi from '../projects/projects-api'
import Project from '../project/project-model'
import orgsStore from '../organizations/organizations-store'
import ErrorMessage from './error-message'
import LoginForm from '../auth/login-form'
import Run from './runs-list-item'
import PermissionMessage from './permission-message'
import ProjectNotSetup from './project-not-setup'
import DashboardBanner from './dashboard-banner'
class RunsList extends Component {
state = {
recordKey: null,
isLoading: true,
hasApiServer: false,
apiUrl: '',
apiError: null,
}
constructor (...args) {
super(...args)
this.runsStore = new RunsStore()
}
componentDidMount () {
this._pingApiServer()
this._handlePolling()
this._getKey()
}
componentDidUpdate () {
this._getKey()
this._handlePolling()
}
componentWillUnmount () {
this._stopPolling()
}
_pingApiServer = () => {
this.setState({ isLoading: true })
ipc.pingApiServer()
.then(() => {
this.setState({
apiError: null,
hasApiServer: true,
isLoading: false,
})
})
.catch(({ apiUrl, message }) => {
this.setState({
apiError: message,
apiUrl,
isLoading: false,
})
})
}
_getRuns = () => {
if (authStore.isAuthenticated && !!this.props.project.id) {
runsApi.loadRuns(this.runsStore)
}
}
_handlePolling () {
if (this._shouldPollRuns()) {
this._poll()
} else {
this._stopPolling()
}
}
_shouldPollRuns () {
return (
authStore.isAuthenticated &&
!this.runsStore.error &&
!!this.props.project.id
)
}
_poll () {
if (runsApi.isPolling()) return
runsApi.loadRuns(this.runsStore)
runsApi.pollRuns(this.runsStore)
}
_stopPolling () {
runsApi.stopPollingRuns()
}
_getKey () {
if (this._needsKey()) {
projectsApi.getRecordKeys().then((keys = []) => {
if (keys.length) {
this.setState({ recordKey: keys[0].id })
}
})
}
}
_needsKey () {
return (
!this.state.recordKey &&
authStore.isAuthenticated &&
!this.runsStore.isLoading &&
!this.runsStore.error &&
!this.runsStore.runs.length &&
this.props.project.id
)
}
render () {
const { project } = this.props
// pinging api server to see if we can show anything
if (this.state.isLoading) {
return <Loader color='#888' scale={0.5}/>
}
// no connection to api server, can't load any runs
if (!this.state.hasApiServer) {
return this._noApiServer()
}
// no project id means they have not set up to record
if (!project.id) {
return this._projectNotSetup()
}
// not logged in
if (!authStore.isAuthenticated) {
return this._loginMessage()
}
// If the project is invalid
if (project.isInvalid) {
return this._projectNotSetup(false)
}
// OR if user does not have acces to the project
if (project.isUnauthorized) {
return this._permissionMessage()
}
// OR if there is an error getting the runs
if (this.runsStore.error) {
// project id missing, probably removed manually from cypress.json
if (errors.isMissingProjectId(this.runsStore.error)) {
return this._projectNotSetup()
// the project is invalid
}
if (errors.isNotFound(this.runsStore.error)) {
return this._projectNotSetup(false)
// they have been logged out
}
if (errors.isUnauthenticated(this.runsStore.error)) {
return this._loginMessage()
// they are not authorized to see runs
}
if (errors.isUnauthorized(this.runsStore.error)) {
return this._permissionMessage()
// other error, but only show if we don't already have runs
}
if (!this.runsStore.isLoaded) {
return <ErrorMessage error={this.runsStore.error} />
}
}
// OR the runs are loading for the first time
if (this.runsStore.isLoading && !this.runsStore.isLoaded) return <Loader color='#888' scale={0.5}/>
// OR there are no runs to show
if (!this.runsStore.runs.length) {
// AND they've never setup CI
if (!project.id) {
return this._projectNotSetup()
// OR they have setup CI
}
return this._empty()
}
//--------End Run States----------//
// everything's good, there are runs to show!
return (
<div className='runs'>
<header>
<h5>Runs
{this._lastUpdated()}
<button
aria-label='Refresh'
className='btn btn-link btn-sm'
disabled={this.runsStore.isLoading}
onClick={this._getRuns}
>
<i aria-hidden="true" className={`fas fa-sync-alt ${this.runsStore.isLoading ? 'fa-spin' : ''}`} />
</button>
</h5>
<div>
<a href="#" className='btn btn-sm see-all-runs' onClick={this._openRuns}>
See all runs <i className='fas fa-external-link-alt' />
</a>
</div>
</header>
<ul className='runs-container list-as-table'>
{_.map(this.runsStore.runs, (run) => (
<Run
key={run.id}
goToRun={this._openRun}
run={run}
/>
))}
</ul>
</div>
)
}
_lastUpdated () {
if (!this.runsStore.lastUpdated) return null
return (
<span className='last-updated'>
Last updated: {this.runsStore.lastUpdated}
</span>
)
}
_noApiServer () {
return (
<div className='empty empty-no-api-server'>
<h4><i className='fas fa-wifi' /> Cannot connect to API server</h4>
<p>Viewing runs requires connecting to an external API server.</p>
<p>We tried but failed to connect to the API server at <em>{this.state.apiUrl}</em></p>
<p>
<button
className='btn btn-default btn-sm'
onClick={this._pingApiServer}
>
<i className='fas fa-sync-alt' />{' '}
Try again
</button>
</p>
<p>The following error was encountered:</p>
<pre className='alert alert-danger'><code>{this.state.apiError}</code></pre>
<a onClick={this._openAPIHelp}>Learn more</a>
</div>
)
}
_loginMessage () {
return (
<div className='empty empty-log-in'>
<DashboardBanner/>
<h4>Log in to see test results here!</h4>
<h5>After logging in, you will see recorded runs here and on the <a href='#' onClick={this._openDashboard}>Cypress Dashboard</a>.</h5>
<LoginForm utm='Runs Tab' />
</div>
)
}
_projectNotSetup (isValid = true) {
return (
<ProjectNotSetup
isAuthenticated={authStore.isAuthenticated}
isShowingLogin={authStore.isShowingLogin}
project={this.props.project}
isValid={isValid}
onSetup={this._setProjectDetails}
/>
)
}
_permissionMessage () {
return (
<PermissionMessage
project={this.props.project}
onRetry={this._getRuns}
/>
)
}
_setProjectDetails = (projectDetails) => {
this.runsStore.setError(null)
projectsApi.updateProject(this.props.project, {
id: projectDetails.id,
name: projectDetails.projectName,
public: projectDetails.public,
orgId: projectDetails.orgId,
orgName: (orgsStore.getOrgById(projectDetails.orgId) || {}).name,
state: Project.VALID,
})
}
_empty () {
const recordCommand = `cypress run --record --key ${this.state.recordKey || '<record-key>'}`
return (
<div>
<div className='first-run-instructions'>
<h4>
To record your first run...
</h4>
<h5>
<span>
1. <code>projectId: {this.props.project.id}</code> has been saved to your {configFileFormatted(this.props.project.configFile)}.{' '}
Make sure to check this file into source control.
</span>
<a onClick={this._openProjectIdGuide}>
<i className='fas fa-question-circle' />{' '}
Why?
</a>
</h5>
<h5>
<span>
2. Run this command now, or in CI.
</span>
<a onClick={this._openCiGuide}>
<i className='fas fa-question-circle' />{' '}
Need help?
</a>
</h5>
<pre id="code-record-command" className="copy-to-clipboard">
<a className="action-copy" onClick={() => ipc.setClipboardText(recordCommand)}>
<Tooltip
title='Copy to clipboard'
placement='top'
className='cy-tooltip'
>
<i className='fas fa-clipboard' />
</Tooltip>
</a>
<code>{recordCommand}</code>
</pre>
<hr />
<p className='alert alert-default'>
<i className='fas fa-info-circle' />{' '}
Recorded runs will show up{' '}
<a href='#' onClick={this._openRunGuide}>here</a>{' '}
and on your{' '}
<a href='#' onClick={this._openRuns}>Cypress Dashboard Service</a>.
</p>
</div>
</div>
)
}
_openRunGuide = (e) => {
e.preventDefault()
ipc.externalOpen('https://on.cypress.io/recording-project-runs')
}
_openRuns = (e) => {
e.preventDefault()
ipc.externalOpen(`https://on.cypress.io/dashboard/projects/${this.props.project.id}/runs`)
}
_openCiGuide = (e) => {
e.preventDefault()
ipc.externalOpen('https://on.cypress.io/guides/continuous-integration')
}
_openProjectIdGuide = (e) => {
e.preventDefault()
ipc.externalOpen('https://on.cypress.io/what-is-a-project-id')
}
_openDashboard = (e) => {
e.preventDefault()
ipc.externalOpen({
url: 'https://on.cypress.io/dashboard-landing',
params: {
utm_medium: 'Runs Tab',
utm_campaign: 'Dashboard Link',
},
})
}
_openRun = (buildNumber) => {
ipc.externalOpen(`https://on.cypress.io/dashboard/projects/${this.props.project.id}/runs/${buildNumber}`)
}
_openAPIHelp () {
ipc.externalOpen('https://on.cypress.io/help-connect-to-api')
}
}
export default RunsList