feat: updated record run instructions (#17317)

Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
This commit is contained in:
Zach Panzarino
2021-08-12 15:42:03 -04:00
committed by GitHub
parent 4d2d7e07aa
commit 24774b6624
8 changed files with 411 additions and 114 deletions

View File

@@ -852,34 +852,101 @@ describe('Runs List', function () {
})
})
it('displays empty message', () => {
cy.contains('To record your first')
})
context('a/b control group', function () {
beforeEach(function () {
this.getProjectStatus.resolve({
orgId: '0',
})
})
it('opens project id guide on clicking "Why?"', () => {
cy.contains('Why?').click()
.then(function () {
expect(this.ipc.externalOpen).to.be.calledWith('https://on.cypress.io/what-is-a-project-id')
it('displays empty message', () => {
cy.contains('To record your first run')
cy.percySnapshot()
})
it('opens project id guide on clicking "Why?"', () => {
cy.contains('Why?').click()
.then(function () {
expect(this.ipc.externalOpen).to.be.calledWithMatch({ url: 'https://on.cypress.io/what-is-a-project-id' })
})
})
it('opens dashboard on clicking "Cypress Dashboard"', () => {
cy.contains('Cypress Dashboard').click()
.then(function () {
expect(this.ipc.externalOpen).to.be.calledWith(`https://on.cypress.io/dashboard/projects/${this.config.projectId}/runs`)
})
})
it('shows tooltip on hover of copy to clipboard', () => {
cy.get('#code-record-command').find('.action-copy').trigger('mouseover')
cy.get('.cy-tooltip').should('contain', 'Copy to clipboard')
cy.get('#code-record-command').find('.action-copy').trigger('mouseout')
})
it('copies record key command to clipboard', () => {
cy.get('#code-record-command').find('.action-copy').click()
.then(function () {
expect(this.ipc.setClipboardText).to.be.calledWith(`cypress run --record --key <record-key>`)
})
})
})
it('opens dashboard on clicking "Cypress Dashboard"', () => {
cy.contains('Cypress Dashboard').click()
.then(function () {
expect(this.ipc.externalOpen).to.be.calledWith(`https://on.cypress.io/dashboard/projects/${this.config.projectId}/runs`)
context('a/b test group', function () {
beforeEach(function () {
this.getProjectStatus.resolve({
orgId: '1',
})
})
})
it('shows tooltip on hover of copy to clipboard', () => {
cy.get('#code-record-command').find('.action-copy').trigger('mouseover')
cy.get('.cy-tooltip').should('contain', 'Copy to clipboard')
cy.get('#code-record-command').find('.action-copy').trigger('mouseout')
})
it('displays empty message', () => {
cy.contains('How to record your first run')
cy.percySnapshot()
})
it('copies record key command to clipboard', () => {
cy.get('#code-record-command').find('.action-copy').click()
.then(function () {
expect(this.ipc.setClipboardText).to.be.calledWith(`cypress run --record --key <record-key>`)
it('displays tooltip with project id info', () => {
cy.get('.help-text').eq(0).find('a').trigger('mouseover')
cy.get('.cy-tooltip').should('contain', 'This helps Cypress uniquely identify your project')
.contains('Learn more').click()
.then(function () {
expect(this.ipc.externalOpen).to.be.calledWithMatch({ url: 'https://on.cypress.io/what-is-a-project-id' })
})
})
it('displays tooltip with record run command info', () => {
cy.get('.help-text').eq(1).find('a').trigger('mouseover')
cy.get('.cy-tooltip').should('contain', 'Close this application and run this command')
.contains('Learn more').click()
.then(function () {
expect(this.ipc.externalOpen).to.be.calledWithMatch({ url: 'https://on.cypress.io/recording-project-runs' })
})
})
it('shows tooltip on hover of copy to clipboard', () => {
cy.get('#code-record-command').find('.action-copy').trigger('mouseover')
cy.get('.cy-tooltip').should('contain', 'Copy to clipboard')
cy.get('#code-record-command').find('.action-copy').trigger('mouseout')
})
it('copies record key command to clipboard', () => {
cy.get('#code-record-command').find('.action-copy').click()
.then(function () {
expect(this.ipc.setClipboardText).to.be.calledWith(`cypress run --record --key <record-key>`)
})
})
it('displays run in ci panel with link', () => {
cy.contains('Run in CI').parents('.panel').contains('Show me how').click()
.then(function () {
expect(this.ipc.externalOpen).to.be.calledWithMatch({ url: 'https://on.cypress.io/ci' })
})
})
it('displays sample project panel with link', () => {
cy.contains('Sample Project').parents('.panel').contains('See the sample').click()
.then(function () {
expect(this.ipc.externalOpen).to.be.calledWithMatch({ url: 'https://on.cypress.io/rwa-dashboard' })
})
})
})
})

View File

@@ -75,7 +75,7 @@ const onSubmitNewProject = function (orgId) {
it('displays empty runs page', function () {
cy.get('.setup-project').should('not.exist')
cy.contains('To record your first')
cy.contains('How to record your first')
cy.contains('cypress run --record --key record-key-123')
})

View File

@@ -282,4 +282,10 @@ export default class Project {
serialize () {
return _.pick(this, cacheProps)
}
getTestGroup (numGroups) {
const numKey = this.orgId && this.orgId.length ? this.orgId.charCodeAt(0) : 0
return numKey % numGroups
}
}

View File

@@ -137,6 +137,17 @@ const closeProject = (project) => {
])
}
const updateProjectStatus = (project) => {
return ipc.getProjectStatus(project.clientDetails())
.then((projectDetails) => {
project.update(projectDetails)
})
.catch(ipc.isUnauthed, ipc.handleUnauthed)
.catch((err) => {
project.setApiError(err)
})
}
const openProject = (project) => {
specsStore.loading(true)
@@ -145,17 +156,6 @@ const openProject = (project) => {
project.setError(err)
}
const updateProjectStatus = () => {
return ipc.getProjectStatus(project.clientDetails())
.then((projectDetails) => {
project.update(projectDetails)
})
.catch(ipc.isUnauthed, ipc.handleUnauthed)
.catch((err) => {
project.setApiError(err)
})
}
const updateConfig = (config) => {
project.update({
id: config.projectId,
@@ -199,9 +199,9 @@ const openProject = (project) => {
project.setLoading(false)
getSpecs(setProjectError)
projectPollingId = setInterval(updateProjectStatus, 10000)
projectPollingId = setInterval(() => updateProjectStatus(project), 10000)
return updateProjectStatus()
return updateProjectStatus(project)
})
.catch(setProjectError)
}
@@ -242,6 +242,7 @@ const getRecordKeys = () => {
export default {
loadProjects,
updateProjectStatus,
openProject,
reopenProject,
closeProject,

View File

@@ -0,0 +1,228 @@
import React, { Component } from 'react'
import { observer } from 'mobx-react'
import Tooltip from '@cypress/react-tooltip'
import Loader from 'react-loader'
import ipc from '../lib/ipc'
import projectsApi from '../projects/projects-api'
import { configFileFormatted } from '../lib/config-file-formatted'
@observer
class RunsListEmpty extends Component {
componentDidMount () {
this._updateProjectStatus()
}
componentDidUpdate () {
this._updateProjectStatus()
}
_updateProjectStatus = () => {
const { project } = this.props
if (!project.orgId) {
projectsApi.updateProjectStatus(project)
}
}
_openProjectIdGuide = (e, utm_medium = 'Empty Runs Tab') => {
e.preventDefault()
ipc.externalOpen({
url: 'https://on.cypress.io/what-is-a-project-id',
params: {
utm_medium,
utm_campaign: 'Run Guide',
},
})
}
_openRuns = (e) => {
e.preventDefault()
ipc.externalOpen(`https://on.cypress.io/dashboard/projects/${this.props.project.id}/runs`)
}
_openCiGuide = (e, utm_medium = 'Empty Runs Tab') => {
e.preventDefault()
ipc.externalOpen({
url: 'https://on.cypress.io/ci',
params: {
utm_medium,
utm_campaign: 'Run Guide',
},
})
}
_openRunGuide = (e, utm_medium = 'Empty Runs Tab') => {
e.preventDefault()
ipc.externalOpen({
url: 'https://on.cypress.io/recording-project-runs',
params: {
utm_medium,
utm_campaign: 'Run Guide',
},
})
}
_openSampleProject = (e) => {
e.preventDefault()
ipc.externalOpen({
url: 'https://on.cypress.io/rwa-dashboard',
params: {
utm_medium: 'Empty Runs Tab',
utm_camptain: 'Sample Project',
},
})
}
_recordCommand = () => {
return `cypress run --record --key ${this.props.recordKey || '<record-key>'}`
}
_control = () => {
return (
<div>
<div className='first-run-instructions'>
<h4 className='center'>
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={(e) => this._openProjectIdGuide(e, 'Control Empty Runs Tab')}>
<i className='fas fa-question-circle' />{' '}
Why?
</a>
</h5>
<h5>
<span>
2. Run this command now, or in CI.
</span>
<a onClick={(e) => this._openCiGuide(e, 'Control Empty Runs Tab')}>
<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(this._recordCommand())}>
<Tooltip
title='Copy to clipboard'
placement='top'
className='cy-tooltip'
>
<i className='fas fa-clipboard' />
</Tooltip>
</a>
<code>{this._recordCommand()}</code>
</pre>
<hr />
<p className='alert alert-default'>
<i className='fas fa-info-circle' />{' '}
Recorded runs will show up{' '}
<a href='#' onClick={(e) => this._openRunGuide(e, 'Control Empty Runs Tab')}>here</a>{' '}
and on your{' '}
<a href='#' onClick={this._openRuns}>Cypress Dashboard Service</a>.
</p>
</div>
</div>
)
}
_new = () => {
return (
<div>
<div className='first-run-instructions new-first-run-instructions'>
<h4>
How to record your first run
</h4>
<p className='subtitle'>
Recording test runs to the Dashboard enables you to run tests faster with parallelization and load balancing, debug failed tests in CI with screenshots and videos, and identify flaky tests.
</p>
<div className='step'>
<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>
<span className='help-text'>
<Tooltip
className='tooltip-text-left cy-tooltip'
title={<span>This helps Cypress uniquely identify your project. If altered or deleted, analytics and load balancing will not function properly. <a onClick={this._openProjectIdGuide}>Learn more</a></span>}
>
<a><i className='fas fa-question-circle' /></a>
</Tooltip>
</span>
</div>
<div className='step'>
<span>
2. Run this command now, or in CI.
</span>
<span className='help-text'>
<Tooltip
className='tooltip-text-left cy-tooltip'
title={<span>Close this application and run this command with <code className='tooltip-code'>npx</code> or <code className='tooltip-code'>yarn</code> in your terminal. <a onClick={this._openRunGuide}>Learn more</a></span>}
>
<a><i className='fas fa-question-circle' /></a>
</Tooltip>
</span>
</div>
<pre id="code-record-command" className="copy-to-clipboard">
<a className="action-copy" onClick={() => ipc.setClipboardText(this._recordCommand())}>
<Tooltip
title='Copy to clipboard'
placement='top'
className='cy-tooltip'
>
<i className='fas fa-clipboard' />
</Tooltip>
</a>
<code>{this._recordCommand()}</code>
</pre>
<hr />
<div className='panel-wrapper'>
<div className='panel'>
<div className='panel-icon panel-icon-small'>
<i className='fas fa-infinity fa-fw' />
</div>
<div>
<p>
<strong>Run in CI</strong>
<br />
Cypress was designed to be run in your CI, enabling parallel test runs and rich test analytics. <a onClick={this._openCiGuide}>Show me how</a>
</p>
</div>
</div>
<div className='panel'>
<div className='panel-icon'>
<i className='fas fa-tasks fa-fw' />
</div>
<div>
<p>
<strong>Sample Project</strong>
<br />
Want to see what a recorded run looks like? See an example project in the Dashboard. <a onClick={this._openSampleProject}>See the sample</a>
</p>
</div>
</div>
</div>
</div>
</div>
)
}
render () {
const { project } = this.props
if (!project.orgId) {
return <Loader color='#888' scale={0.5}/>
}
if (project.getTestGroup(2)) {
return this._new()
}
return this._control()
}
}
export default RunsListEmpty

View File

@@ -2,10 +2,8 @@ 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'
@@ -21,6 +19,7 @@ import PermissionMessage from './permission-message'
import ProjectNotSetup from './project-not-setup'
import DashboardBanner from './dashboard-banner'
import WhatIsDashboard from './what-is-dashboard'
import RunsListEmpty from './runs-list-empty'
@observer
class RunsList extends Component {
@@ -45,8 +44,8 @@ class RunsList extends Component {
}
componentDidUpdate () {
this._getRecordKeys()
this._handlePolling()
this._getRecordKeys()
}
componentWillUnmount () {
@@ -204,7 +203,7 @@ class RunsList extends Component {
// OR they have setup CI
}
return this._empty()
return <RunsListEmpty project={this.props.project} recordKey={this.state.recordKey} />
}
//--------End Run States----------//
@@ -320,79 +319,6 @@ class RunsList extends Component {
})
}
_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({
@@ -404,6 +330,11 @@ class RunsList extends Component {
})
}
_openRuns = (e) => {
e.preventDefault()
ipc.externalOpen(`https://on.cypress.io/dashboard/projects/${this.props.project.id}/runs`)
}
_openRun = (buildNumber) => {
ipc.externalOpen(`https://on.cypress.io/dashboard/projects/${this.props.project.id}/runs/${buildNumber}`)
}

View File

@@ -22,6 +22,10 @@
.first-run-instructions {
padding: 40px 130px;
&.new-first-run-instructions {
padding: 40px 100px;
}
a {
cursor: pointer;
}
@@ -49,6 +53,18 @@
}
}
.step {
display: flex;
font-weight: 400;
font-size: 14px;
margin: 20px 0 10px;
line-height: 18px;
.help-text {
margin-left: auto;
}
}
.alert {
border-radius: 3px;
border: 1px solid #eee;
@@ -74,7 +90,8 @@
margin-top: 25px;
margin-bottom: 25px;
}
h4, .center {
.center {
text-align: center;
}
@@ -85,6 +102,41 @@
border-radius: 3px;
color: #eee;
}
.subtitle {
color: #666;
font-size: 13px;
text-align: left;
}
.panel-wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
.panel {
border: 1px solid #E5E5E5;
border-radius: 6px;
display: flex;
font-size: 13px;
padding: 12px;
p {
margin-bottom: 0;
text-align: left;
}
.panel-icon {
color: $brand-primary;
font-size: 18px;
padding-right: 12px;
&.panel-icon-small {
font-size: 16px;
}
}
}
}
}
.runs {

View File

@@ -150,4 +150,16 @@ pre.copy-to-clipboard {
color: #E2E8F0;
}
}
}
}
.cy-tooltip {
&.tooltip-text-left {
text-align: left;
}
.tooltip-code {
color: #fff;
background-color: #252831;
border: none;
}
}