diff --git a/packages/desktop-gui/cypress/integration/runs_list_spec.js b/packages/desktop-gui/cypress/integration/runs_list_spec.js index a90f7a1fcc..18ca1a6092 100644 --- a/packages/desktop-gui/cypress/integration/runs_list_spec.js +++ b/packages/desktop-gui/cypress/integration/runs_list_spec.js @@ -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 `) + }) }) }) - 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 `) + 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 `) + }) + }) + + 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' }) + }) }) }) }) diff --git a/packages/desktop-gui/cypress/integration/setup_project_spec.js b/packages/desktop-gui/cypress/integration/setup_project_spec.js index bc1b5b5345..dd64beec60 100644 --- a/packages/desktop-gui/cypress/integration/setup_project_spec.js +++ b/packages/desktop-gui/cypress/integration/setup_project_spec.js @@ -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') }) diff --git a/packages/desktop-gui/src/project/project-model.js b/packages/desktop-gui/src/project/project-model.js index a0f2cb2339..badfb77f87 100644 --- a/packages/desktop-gui/src/project/project-model.js +++ b/packages/desktop-gui/src/project/project-model.js @@ -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 + } } diff --git a/packages/desktop-gui/src/projects/projects-api.js b/packages/desktop-gui/src/projects/projects-api.js index 2c7b87831a..9d48eb2f63 100644 --- a/packages/desktop-gui/src/projects/projects-api.js +++ b/packages/desktop-gui/src/projects/projects-api.js @@ -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, diff --git a/packages/desktop-gui/src/runs/runs-list-empty.jsx b/packages/desktop-gui/src/runs/runs-list-empty.jsx new file mode 100644 index 0000000000..8e91047232 --- /dev/null +++ b/packages/desktop-gui/src/runs/runs-list-empty.jsx @@ -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 || ''}` + } + + _control = () => { + return ( +
+
+

+ To record your first run... +

+
+ + 1. projectId: {this.props.project.id} has been saved to your {configFileFormatted(this.props.project.configFile)}.{' '} + Make sure to check this file into source control. + + this._openProjectIdGuide(e, 'Control Empty Runs Tab')}> + {' '} + Why? + +
+
+ + 2. Run this command now, or in CI. + + this._openCiGuide(e, 'Control Empty Runs Tab')}> + {' '} + Need help? + +
+
+             ipc.setClipboardText(this._recordCommand())}>
+              
+                
+              
+            
+            {this._recordCommand()}
+          
+
+

+ {' '} + Recorded runs will show up{' '} + this._openRunGuide(e, 'Control Empty Runs Tab')}>here{' '} + and on your{' '} + Cypress Dashboard Service. +

+
+
+ ) + } + + _new = () => { + return ( +
+
+

+ How to record your first run +

+

+ 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. +

+
+ + 1. projectId: {this.props.project.id} has been saved to your {configFileFormatted(this.props.project.configFile)}.{' '} + Make sure to check this file into source control. + + + This helps Cypress uniquely identify your project. If altered or deleted, analytics and load balancing will not function properly. Learn more} + > + + + +
+
+ + 2. Run this command now, or in CI. + + + Close this application and run this command with npx or yarn in your terminal. Learn more} + > + + + +
+
+             ipc.setClipboardText(this._recordCommand())}>
+              
+                
+              
+            
+            {this._recordCommand()}
+          
+
+
+
+
+ +
+
+

+ Run in CI +
+ Cypress was designed to be run in your CI, enabling parallel test runs and rich test analytics. Show me how +

+
+
+
+
+ +
+
+

+ Sample Project +
+ Want to see what a recorded run looks like? See an example project in the Dashboard. See the sample +

+
+
+
+
+
+ ) + } + + render () { + const { project } = this.props + + if (!project.orgId) { + return + } + + if (project.getTestGroup(2)) { + return this._new() + } + + return this._control() + } +} + +export default RunsListEmpty diff --git a/packages/desktop-gui/src/runs/runs-list.jsx b/packages/desktop-gui/src/runs/runs-list.jsx index f353d24242..35b887a721 100644 --- a/packages/desktop-gui/src/runs/runs-list.jsx +++ b/packages/desktop-gui/src/runs/runs-list.jsx @@ -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 } //--------End Run States----------// @@ -320,79 +319,6 @@ class RunsList extends Component { }) } - _empty () { - const recordCommand = `cypress run --record --key ${this.state.recordKey || ''}` - - return ( -
-
-

- To record your first run... -

-
- - 1. projectId: {this.props.project.id} has been saved to your {configFileFormatted(this.props.project.configFile)}.{' '} - Make sure to check this file into source control. - - - {' '} - Why? - -
-
- - 2. Run this command now, or in CI. - - - {' '} - Need help? - -
-
-             ipc.setClipboardText(recordCommand)}>
-              
-                
-              
-            
-            {recordCommand}
-          
-
-

- {' '} - Recorded runs will show up{' '} - here{' '} - and on your{' '} - Cypress Dashboard Service. -

-
-
- ) - } - - _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}`) } diff --git a/packages/desktop-gui/src/runs/runs.scss b/packages/desktop-gui/src/runs/runs.scss index a93ea1469e..7526519c84 100644 --- a/packages/desktop-gui/src/runs/runs.scss +++ b/packages/desktop-gui/src/runs/runs.scss @@ -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 { diff --git a/packages/desktop-gui/src/styles/components/_general.scss b/packages/desktop-gui/src/styles/components/_general.scss index ec3372356d..6ddf476f60 100644 --- a/packages/desktop-gui/src/styles/components/_general.scss +++ b/packages/desktop-gui/src/styles/components/_general.scss @@ -150,4 +150,16 @@ pre.copy-to-clipboard { color: #E2E8F0; } } -} \ No newline at end of file +} + +.cy-tooltip { + &.tooltip-text-left { + text-align: left; + } + + .tooltip-code { + color: #fff; + background-color: #252831; + border: none; + } +}