feat: updated scaffolding, new project & user banners, ability to remove scaffolding (#15826)

* update example build scripts

* remove old scaffolding relics

* update

* Fix some issues with scaffolding

* Correctly fix issues with scaffolding

* Replace old onboarding with new banner

* Add ability to remove scaffolded files

* Add banner for new users

* Update tests for new scaffolding

* Compare file sizes before removing

* Add tests for remove file

* Save when user opened cypress rather than boolean

* Update intro link and add tets for banners

* fix small issue

* Update design and copy of onboarding banners

* Update style of new spec file button

* Improve outline button active statE

* Update design of new project a bit more

* Fix specs list tests

* Update banner copy and layout

* Update banner copy and layout

* Switch to docs style alerts

* Fix testing logic

* Update banner styles a bit

* Update banners

* Add confirmation modal for delete specs

* Update tests and fix states

* Upgrade kitchensink dep

* Upgrade kitchen sink version and fix unit tests

* Update integration scaffolding test

* Add further description to warning modal

* Update test for new user and new project case

* Remove check to file tree when removing files

* Update kitchensink version

* Fix edge case where banner could appear when no files have been scaffolded

* Fix tests

* Update styling for 'note' when deleting files

* fix issue with path on windows

* Change remove command

* Fix rm dir

* Fix for windows

* Try to use appveyor to test

* appveyor please

* getting some feedback

* Why doesn't this work

* more info

* I have a feeling this works

* maybe its the other path

* please

* this is the one

* this is it

* this should work

* try reverting that change that might not be needed

* remove appveyor testing scaffolding

Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
This commit is contained in:
Zach Panzarino
2021-06-22 18:59:47 +03:00
committed by GitHub
parent 03ab08d38f
commit da3e49bf59
37 changed files with 682 additions and 504 deletions

View File

@@ -2763,8 +2763,6 @@ declare namespace Cypress {
clientRoute: string
configFile: string
cypressEnv: string
integrationExampleName: string
integrationExamplePath: string
isNewProject: boolean
isTextTerminal: boolean
morgan: boolean

View File

@@ -128,8 +128,6 @@
"execTimeout": 60000,
"fileServerFolder": "/Users/jennifer/Dev/Projects/cypress-example-kitchensink",
"fixturesFolder": "/Users/jennifer/Dev/Projects/cypress-example-kitchensink/cypress/fixtures",
"integrationExamplePath": "/Users/jennifer/Dev/Projects/cypress-example-kitchensink/cypress/integration/examples",
"integrationExampleName": "examples",
"integrationFolder": "/Users/jennifer/Dev/Projects/cypress-example-kitchensink/cypress/integration",
"isHeadless": false,
"isNewProject": false,

View File

@@ -24,10 +24,11 @@ describe('Specs List', function () {
cy.stub(this.ipc, 'openFinder')
cy.stub(this.ipc, 'openFile')
cy.stub(this.ipc, 'externalOpen')
cy.stub(this.ipc, 'onboardingClosed')
cy.stub(this.ipc, 'hasOpenedCypress').resolves(true)
cy.stub(this.ipc, 'onSpecChanged')
cy.stub(this.ipc, 'setUserEditor')
cy.stub(this.ipc, 'showNewSpecDialog').resolves({ specs: null, path: null })
cy.stub(this.ipc, 'removeScaffoldedFiles').resolves()
this.openProject = this.util.deferred()
cy.stub(this.ipc, 'openProject').returns(this.openProject.promise)
@@ -58,8 +59,10 @@ describe('Specs List', function () {
})
})
it('displays help link', () => {
cy.contains('a', 'Need help?')
it('launches system save dialog on click of new spec file', function () {
cy.contains('New Spec File').click().then(function () {
expect(this.ipc.showNewSpecDialog).to.be.called
})
})
it('opens link to docs on click of help link', () => {
@@ -96,98 +99,76 @@ describe('Specs List', function () {
})
})
describe('first time onboarding specs', function () {
describe('new project onboarding', function () {
beforeEach(function () {
this.config.isNewProject = true
this.openProject.resolve(this.config)
})
context('modal', () => {
it('displays', () => {
cy.contains('.modal', 'To help you get started').should('be.visible')
cy.percySnapshot()
})
it('displays the scaffolded files', () => {
cy.get('.folder-preview-onboarding').within(function () {
cy.contains('span', 'fixtures').siblings('ul').within(function () {
})
cy.contains('example.json')
cy.contains('span', 'integration').siblings('ul').within(() => {
cy.contains('examples')
})
cy.contains('span', 'support').siblings('ul').within(function () {
cy.contains('commands.js')
cy.contains('defaults.js')
cy.contains('index.js')
})
cy.contains('span', 'plugins').siblings('ul').within(() => {
cy.contains('index.js')
})
})
})
it('lists folders and files alphabetically', () => {
cy.get('.folder-preview-onboarding').within(() => {
cy.contains('fixtures').parent().next()
.contains('integration')
})
})
it('truncates file lists with more than 3 items', () => {
cy.get('.folder-preview-onboarding').within(function () {
cy.contains('examples').closest('.new-item').find('li')
.should('have.length', 3)
cy.get('.is-more').should('have.text', ' ... 17 more files ...')
})
})
it('can dismiss the modal', function () {
cy.contains('OK, got it!').click()
cy.get('.modal').should('not.be.visible')
.then(function () {
expect(this.ipc.onboardingClosed).to.be.called
})
})
it('triggers open:finder on click of example folder', function () {
cy.get('.modal').contains('examples').click().then(() => {
expect(this.ipc.openFinder).to.be.calledWith(this.config.integrationExamplePath)
})
})
it('triggers open:finder on click of text folder', function () {
cy.get('.modal').contains('cypress/integration').click().then(() => {
expect(this.ipc.openFinder).to.be.calledWith(this.config.integrationFolder)
})
})
})
context('banner', function () {
beforeEach(function () {
cy.get('.modal').find('.btn-success').click()
})
it('displays', function () {
cy.get('.first-test-banner')
cy.get('.new-project-banner')
cy.percySnapshot()
})
it('is dismissable', function () {
cy.get('.first-test-banner').find('.close').click()
cy.get('.first-test-banner').should('not.exist')
cy.get('.new-project-banner').find('.close').click()
cy.get('.new-project-banner').should('not.exist')
})
it('does not display new user banner even when closed', function () {
this.ipc.hasOpenedCypress.resolves(false)
cy.get('.new-user-banner').should('not.exist')
cy.get('.new-project-banner').find('.close').click()
cy.get('.new-project-banner').should('not.exist')
cy.get('.new-user-banner').should('not.exist')
})
it('opens link to docs on click of help link', function () {
cy.contains('a', 'How to write tests').click().then(function () {
expect(this.ipc.externalOpen).to.be.calledWith('https://on.cypress.io/writing-first-test')
cy.contains('a', 'How to write your first test').click().then(function () {
expect(this.ipc.externalOpen).to.be.calledWithMatch({ url: 'https://on.cypress.io/writing-first-test' })
})
})
it('removes scaffolded files on click and confirmation', function () {
cy.contains('delete example files').click()
cy.get('.confirm-remove-scaffolded-files').should('be.visible')
cy.contains('Yes, delete files').click().then(function () {
expect(this.ipc.removeScaffoldedFiles).to.be.called
cy.get('.new-project-banner').should('not.exist')
})
})
})
})
describe('first time user in existing project', function () {
beforeEach(function () {
this.openProject.resolve(this.config)
this.ipc.hasOpenedCypress.resolves(false)
})
context('banner', function () {
it('displays', function () {
cy.get('.new-user-banner')
cy.percySnapshot()
})
it('is dismissable', function () {
cy.get('.new-user-banner').find('.close').click()
cy.get('.new-user-banner').should('not.exist')
})
it('opens link to docs on click of how to link', function () {
cy.contains('a', 'How to write your first test').click().then(function () {
expect(this.ipc.externalOpen).to.be.calledWithMatch({ url: 'https://on.cypress.io/writing-first-test' })
})
})
it('opens link to intro guide on click of intro link', function () {
cy.contains('a', 'Introduction guide to Cypress').click().then(function () {
expect(this.ipc.externalOpen).to.be.calledWithMatch({ url: 'https://on.cypress.io/intro-to-cypress' })
})
})
})

View File

@@ -34,8 +34,8 @@ class Default extends Component {
onDrop={this._drop}
>
<span className="fa-stack fa-lg">
<i className="fas fa-folder fa-stack-2x"></i>
<i className="fas fa-plus fa-stack-1x"></i>
<i className="fas fa-folder fa-stack-2x" />
<i className="fas fa-plus fa-stack-1x" />
</span>
<p>Drag your project here or <a href="#" onClick={this._selectProject}>select manually</a>.</p>
</div>
@@ -51,7 +51,7 @@ class Default extends Component {
return (
<div className='local-install-notice alert alert-info alert-dismissible'>
<p className='text-center'>
<i className='fas fa-info-circle'></i>{' '}
<i className='fas fa-info-circle' />{' '}
We recommend versioning Cypress per project and{' '}
<a onClick={this._openHelp} className='helper-docs-link'>
installing it via <span className='mono'>npm</span>

View File

@@ -71,7 +71,9 @@ register('updater:check', false)
register('updater:run', false)
register('window:open')
register('window:close')
register('onboarding:closed')
register('new:project:banner:closed')
register('has:opened:cypress')
register('remove:scaffolded:files')
register('set:clipboard:text')
register('set:prompt:shown')

View File

@@ -35,7 +35,7 @@ export default class Browsers extends Component {
return (
<li className='close-browser'>
<button className='btn btn-xs btn-danger' onClick={this._closeBrowser.bind(this)}>
<i className='fas fa-fw fa-times'></i>
<i className='fas fa-fw fa-times' />
Stop
</button>
</li>

View File

@@ -12,19 +12,19 @@ export default class ProjectNav extends Component {
<ul className='nav left-nav'>
<li>
<Link to={routes.specs(project)}>
<i className='fas fa-code'></i>{' '}
<i className='fas fa-code' />{' '}
Tests
</Link>
</li>
<li>
<Link to={routes.runs(project)}>
<i className='fas fa-database'></i>{' '}
<i className='fas fa-database' />{' '}
Runs
</Link>
</li>
<li>
<Link to={routes.settings(project)}>
<i className='fas fa-cog'></i>{' '}
<i className='fas fa-cog' />{' '}
Settings
</Link>
</li>

View File

@@ -1,131 +0,0 @@
import cs from 'classnames'
import _ from 'lodash'
import React, { Component } from 'react'
import { observer } from 'mobx-react'
import BootstrapModal from 'react-bootstrap-modal'
import ipc from '../lib/ipc'
@observer
class OnBoarding extends Component {
componentDidMount () {
this._maybeShowModal()
}
componentDidUpdate () {
this._maybeShowModal()
}
_maybeShowModal () {
if (!this.showedModal && this.props.project.isNew) {
this.showedModal = true
this.props.project.openModal()
}
}
render () {
const { project } = this.props
let closeModal = () => {
project.closeModal()
ipc.onboardingClosed()
}
return (
<BootstrapModal
show={project.onBoardingModalOpen}
onHide={closeModal}
backdrop='static'
>
<div className='modal-body'>
<div className='empty-onboarding'>
<h1>To help you get started...</h1>
<p>
We've added some folders and example tests to your project. Try running the tests in the
<strong onClick={this._openExampleSpec}>
<i className='far fa-folder'></i>{' '}
{project.integrationExampleName}{' '}
</strong>
folder or add your own test files to
<strong onClick={this._openIntegrationFolder}>
<i className='far fa-folder'></i>{' '}
cypress/integration
</strong>.
</p>
<div className='folder-preview-onboarding'>
<ul>
<li>
<span>
<i className='far fa-folder-open'></i>{' '}
{project.name}
</span>
<ul>
<li className='app-code'>
<span >
<i className='far fa-folder'></i>{' '}
...
</span>
</li>
{this._scaffoldedFiles(project.scaffoldedFiles, 'new-code')}
</ul>
</li>
</ul>
</div>
<div className='helper-line'>
<BootstrapModal.Dismiss className='btn btn-success'>
OK, got it!
</BootstrapModal.Dismiss>
</div>
</div>
</div>
</BootstrapModal>
)
}
_scaffoldedFiles (files, className) {
files = _.sortBy(files, 'name')
const notFolders = _.every(files, (file) => !file.children)
if (notFolders && files.length > 3) {
const numHidden = files.length - 2
files = files.slice(0, 2).concat({ name: `... ${numHidden} more files ...`, more: true })
}
return _.map(files, (file) => {
if (file.children) {
return (
<li className={cs(className, 'new-item')} key={file.name}>
<span>
<i className='far fa-folder-open'></i>{' '}
{file.name}
</span>
<ul>
{this._scaffoldedFiles(file.children)}
</ul>
</li>
)
}
return (
<li className={cs(className, 'new-item', { 'is-more': file.more })} key={file.name}>
<span>
<i className='far fa-file-code'></i>{' '}
{file.name}
</span>
</li>
)
})
}
_openExampleSpec = () => {
ipc.openFinder(this.props.project.integrationExamplePath)
}
_openIntegrationFolder = () => {
ipc.openFinder(this.props.project.integrationFolder)
}
}
export default OnBoarding

View File

@@ -1,137 +0,0 @@
.empty-onboarding {
margin: 0px auto 0;
overflow: auto;
strong {
margin-left: 4px;
white-space: nowrap;
}
strong:hover {
cursor: pointer;
border-bottom: 1px dotted #333;
}
hr {
margin: 5px 0;
}
h1 {
text-align: center;
color: $pass;
font-size: 18.5px;
margin: 0px 0 10px;
}
img {
text-align: center;
margin: 20px auto;
}
p {
font-size: 14px;
line-height: 21px;
margin-bottom: 0;
a {
border-bottom: 1px dotted lighten($brand-primary, 30%);
}
}
.helper-line {
bottom: 0;
position: inherit;
}
h6 {
margin-top: 30px;
font-weight: bold;
font-size: 14px;
font-style: italic;
color: #999;
margin-bottom: 2px;
}
.folder-preview-onboarding {
background-color: #fcfcfc;
padding: 0;
border: 1px solid #e9e9e9;
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.04);
margin: 0 auto 10px;
color: #555;
margin-top: 10px;
ul {
list-style-type: none;
margin-left: 0;
font-size: 13.5px;
-webkit-padding-start: 0;
&>li {
position: relative;
margin: 3px 0;
&.app-code {
color: #aaa;
font-style: italic;
font-weight: 200;
}
&.new-code {
padding: 2px 0 0 31px;
margin-left: -30px;
margin-bottom: 0;
border-top: 1px solid lighten($pass, 54%);
background-color: lighten($pass, 58%);
overflow: auto;
}
&>ul {
padding-left: 20px;
}
}
}
&>ul {
margin-bottom: 0;
&>li {
margin-bottom: 0;
padding-left: 10px;
}
}
.new-item:after {
color: lighten($pass, 5%);
content: "+";
position: absolute;
top: 0;
left: 10px;
}
.new-item .new-item:after {
top: -2px;
left: -40px;
}
.new-item .new-item .new-item:after {
top: -2px;
left: -60px;
}
.new-item .new-item .new-item .new-item:after {
top: -2px;
left: -80px;
}
.is-more {
i {
display: none;
}
opacity: 0.7;
}
}
}

View File

@@ -23,11 +23,11 @@ const validProps = cacheProps.concat([
'isNew',
'configFile',
'browsers',
'onBoardingModalOpen',
'newProjectBannerOpen',
'newUserBannerOpen',
'browserState',
'resolvedConfig',
'parentTestsFolderDisplay',
'integrationExampleName',
'scaffoldedFiles',
'resolvedNodePath',
'resolvedNodeVersion',
@@ -55,7 +55,8 @@ export default class Project {
@observable isLoading = false
@observable isNew = false
@observable browsers = []
@observable onBoardingModalOpen = false
@observable newProjectBannerOpen = false
@observable newUserBannerOpen = false
@observable browserState = 'closed'
@observable resolvedConfig
@observable error
@@ -63,7 +64,6 @@ export default class Project {
@observable _warnings = {}
@observable apiError
@observable parentTestsFolderDisplay
@observable integrationExampleName
@observable scaffoldedFiles = []
@observable resolvedNodePath
@observable resolvedNodeVersion
@@ -147,12 +147,9 @@ export default class Project {
this.isLoading = isLoading
}
@action openModal () {
this.onBoardingModalOpen = true
}
@action closeModal () {
this.onBoardingModalOpen = false
@action closeBanners () {
this.newProjectBannerOpen = false
this.newUserBannerOpen = false
}
@action browserOpening () {
@@ -206,11 +203,10 @@ export default class Project {
@action setOnBoardingConfig (config) {
this.isNew = config.isNewProject
this.newProjectBannerOpen = config.isNewProject
this.integrationFolder = config.integrationFolder
this.parentTestsFolderDisplay = config.parentTestsFolderDisplay
this.fileServerFolder = config.fileServerFolder
this.integrationExampleName = config.integrationExampleName
this.integrationExamplePath = config.integrationExamplePath
this.scaffoldedFiles = config.scaffoldedFiles
}

View File

@@ -10,7 +10,6 @@ import viewStore from '../lib/view-store'
import ipc from '../lib/ipc'
import Settings from '../settings/settings'
import OnBoarding from './onboarding'
import ProjectNav from '../project-nav/project-nav'
import RunsList from '../runs/runs-list'
import SpecsList from '../specs/specs-list'
@@ -53,7 +52,6 @@ class Project extends Component {
{this._renderWarnings()}
{this._currentView()}
</div>
<OnBoarding project={this.props.project}/>
</>
)
}

View File

@@ -12,7 +12,7 @@ class WarningMessage extends Component {
return (
<div className='alert alert-warning'>
<p className='header'>
<i className='fas fa-exclamation-triangle'></i>{' '}
<i className='fas fa-exclamation-triangle' />{' '}
<strong>Warning</strong>
</p>
<div>

View File

@@ -46,7 +46,7 @@ class ProjectsList extends Component {
return (
<div className='alert alert-danger'>
<p>
<i className='fas fa-exclamation-triangle'></i>{' '}
<i className='fas fa-exclamation-triangle' />{' '}
<strong>Error</strong>
</p>
<p dangerouslySetInnerHTML={{

View File

@@ -5,6 +5,7 @@ import _ from 'lodash'
import React, { Component } from 'react'
import { observer } from 'mobx-react'
import Loader from 'react-loader'
import BootstrapModal from 'react-bootstrap-modal'
import Tooltip from '@cypress/react-tooltip'
import FileOpener from './file-opener'
@@ -24,9 +25,7 @@ const formRunButtonLabel = (areTestsAlreadyRunning, specType, specsN) => {
return `Running ${specType} tests`
}
const label = specsN === 1 ? `Run 1 ${specType} spec` : `Run ${specsN} ${specType} specs`
return label
return specsN === 1 ? `Run 1 ${specType} spec` : `Run ${specsN} ${specType} specs`
}
/**
@@ -60,6 +59,7 @@ class SpecsList extends Component {
super(props)
this.state = {
isFocused: false,
confirmRemoveScaffoldedFiles: false,
}
this.filterRef = React.createRef()
@@ -75,10 +75,12 @@ class SpecsList extends Component {
// @ts-ignore
window.__project = this.props.project
}
}
this.state = {
firstTestBannerDismissed: false,
}
componentDidMount () {
ipc.hasOpenedCypress().then((opened) => {
this.props.project.update({ newUserBannerOpen: !opened })
})
}
componentDidUpdate () {
@@ -98,7 +100,7 @@ class SpecsList extends Component {
}
render () {
if (specsStore.isLoading) return <Loader color='#888' scale={0.5}/>
if (specsStore.isLoading) return <Loader color='#888' scale={0.5} />
const filteredSpecs = specsStore.getFilteredSpecs()
@@ -121,7 +123,8 @@ class SpecsList extends Component {
return (
<div className='specs'>
{this._firstTestBanner()}
{this._banners()}
{this._confirmRemoveScaffoldedFilesDialog()}
<header>
<div className={cs('search', {
'show-clear-filter': !!specsStore.filter,
@@ -151,7 +154,7 @@ class SpecsList extends Component {
</Tooltip>
</div>
<div className='new-file-button'>
<button className='btn btn-link' onClick={this._createNewFile}><i className="fa fa-plus"></i> New Spec File</button>
<button className='btn btn-link' onClick={this._createNewFile}><i className="fa fa-plus" /> New Spec File</button>
</div>
</header>
{this._specsList()}
@@ -415,24 +418,123 @@ class SpecsList extends Component {
{this.props.project.integrationFolder}
</code>
</h5>
<a className='helper-docs-link' onClick={this._openHelp}>
<i className='fas fa-question-circle' />{' '}
Need help?
</a>
<p>
<a onClick={this._createNewFile}>
<i className='fas fa-plus' /> New Spec File
</a>
&nbsp;&nbsp;|&nbsp;&nbsp;
<a className='helper-docs-link' onClick={this._openHelp}>
<i className='fas fa-question-circle' /> Need help?
</a>
</p>
</div>
</div>
)
}
_firstTestBanner () {
if (!this.props.project.isNew || this.state.firstTestBannerDismissed) return
_closeBanners = () => {
this.props.project.closeBanners()
ipc.newProjectBannerClosed()
}
_removeScaffoldedFiles = () => {
ipc.removeScaffoldedFiles().then(this._closeBanners)
}
_openRemoveScaffoldedFilesDialog = () => {
this.setState({ confirmRemoveScaffoldedFiles: true })
}
_closeRemoveScaffoldedFilesDialog = () => {
this.setState({ confirmRemoveScaffoldedFiles: false })
}
_openHowToNewProjectBanner = (e) => {
e.preventDefault()
ipc.externalOpen({
url: 'https://on.cypress.io/writing-first-test',
params: {
utm_medium: 'New Project Banner',
utm_campaign: 'How To',
},
})
}
_openHowToNewUserBanner = (e) => {
e.preventDefault()
ipc.externalOpen({
url: 'https://on.cypress.io/writing-first-test',
params: {
utm_medium: 'New User Banner',
utm_campaign: 'How To',
},
})
}
_openIntroNewUserBanner = (e) => {
e.preventDefault()
ipc.externalOpen({
url: 'https://on.cypress.io/intro-to-cypress',
params: {
utm_medium: 'New User Banner',
utm_campaign: 'Intro Guide',
},
})
}
_banners () {
if (this.props.project.newProjectBannerOpen) {
return (
<div className="onboarding-banner new-project-banner info-box info-box-dismissible">
<p className="header">
<strong>Welcome to Cypress!</strong>
</p>
<p>We've created some sample test files that demonstrate key Cypress concepts to help you get started.</p>
<p className="action-links">
<a onClick={this._openHowToNewProjectBanner}>How to write your first test <i className="fa fa-sm fa-external-link-alt" /></a>
&nbsp;&nbsp;|&nbsp;&nbsp;
<a className="link-danger" onClick={this._openRemoveScaffoldedFilesDialog}>No thanks, delete example files</a>
</p>
<button className="close" onClick={this._closeBanners}><span>&times;</span></button>
</div>
)
}
if (this.props.project.newUserBannerOpen) {
return (
<div className="onboarding-banner new-user-banner info-box info-box-dismissible">
<p className="header">
<strong>New to Cypress?</strong>
</p>
<p>We've created some new user guides on key Cypress concepts to help you get started.</p>
<p className="action-links">
<a onClick={this._openHowToNewUserBanner}>How to write your first test <i className="fa fa-sm fa-external-link-alt" /></a>
&nbsp;&nbsp;|&nbsp;&nbsp;
<a onClick={this._openIntroNewUserBanner}>Introduction guide to Cypress <i className="fa fa-sm fa-external-link-alt" /></a>
</p>
<button className="close" onClick={this._closeBanners}><span>&times;</span></button>
</div>
)
}
return null
}
_confirmRemoveScaffoldedFilesDialog = () => {
if (!this.props.project.newProjectBannerOpen) return null
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}><span>&times;</span></button>
</div>
<BootstrapModal show={this.state.confirmRemoveScaffoldedFiles} onHide={this._closeRemoveScaffoldedFilesDialog} backdrop='static'>
<div className='modal-body confirm-remove-scaffolded-files'>
<BootstrapModal.Dismiss className='btn btn-link close'>&times;</BootstrapModal.Dismiss>
<h4>Are you sure that you want to delete all example spec files?</h4>
<h4 className="note">Note: this will not delete any new or edited files.</h4>
</div>
<div className='modal-footer'>
<BootstrapModal.Dismiss className='btn btn-link'>Cancel</BootstrapModal.Dismiss>
<button className='btn btn-danger' onClick={this._removeScaffoldedFiles}>Yes, delete files</button>
</div>
</BootstrapModal>
)
}
@@ -454,10 +556,6 @@ class SpecsList extends Component {
e.preventDefault()
ipc.externalOpen('https://on.cypress.io/writing-first-test')
}
_removeFirstTestBanner = () => {
this.setState({ firstTestBannerDismissed: true })
}
}
export default SpecsList

View File

@@ -7,10 +7,18 @@ $max-nesting-level: 14;
width: 100%;
min-height: 0;
.empty-well code {
display: block;
line-height: 1.8;
margin-top: 5px;
.empty-well {
code {
color: #666;
background: $light-gray;
display: block;
line-height: 1.8;
margin-top: 5px;
&:hover, &:focus {
color: #333;
}
}
}
header {
@@ -77,8 +85,13 @@ $max-nesting-level: 14;
padding-right: 15px;
button {
color: #637eb9;
font-size: 13px;
padding: 6px 10px;
&:hover, &:focus {
color: #38589c;
}
}
}
@@ -282,9 +295,41 @@ $max-nesting-level: 14;
}
}
.first-test-banner {
.onboarding-banner {
margin: 6px;
padding-left: 20px;
p {
margin-bottom: 2px;
}
.header {
margin-bottom: 10px;
}
.action-links {
margin-top: 5px;
}
.link-danger {
color: #666;
&:hover, &:focus {
color: darken($brand-danger, 5%);
}
}
}
}
.confirm-remove-scaffolded-files {
h4 {
line-height: 24px;
&.note {
font-style: italic;
font-size: 14px;
font-weight: 400;
color: #888;
}
}
}

View File

@@ -122,3 +122,17 @@
right: 10px;
}
}
.info-box {
border-left: 4px solid #2a98b9;
background-color: #f2fafd;
padding: 10px 14px;
position: relative;
&.info-box-dismissible .close {
position: absolute;
top: 5px;
right: 10px;
cursor: pointer;
}
}

View File

@@ -11,7 +11,7 @@ The actual example repo you're probably looking for is [the kitchen sink app her
**THERE'S LIKELY NO REASON YOU NEED TO EDIT ANY OF THE CODE ON THIS REPO.**
- Want to edit the `example` tests? -> edit it [here](https://github.com/cypress-io/cypress-example-kitchensink/blob/master/cypress/integration/examples) instead.
- Want to edit the `example` tests? -> edit it [here](https://github.com/cypress-io/cypress-example-kitchensink/blob/master/cypress/integration) instead.
- Want to edit the actual [https://example.cypress.io](https://example.cypress.io) website? edit it [here](https://github.com/cypress-io/cypress-example-kitchensink/tree/master/app) instead.
## Updating the `example` app

View File

@@ -8,11 +8,9 @@ shell.set('-v') // verbose
shell.set('-e') // any error is fatal
shell.rm('-rf', 'app')
shell.mkdir('app')
shell.cp('-r', join(resolvePkg('cypress-example-kitchensink'), 'app'), '.')
shell.rm('-rf', 'cypress')
shell.rm('-rf', 'cypress')
shell.cp('-r', join(resolvePkg('cypress-example-kitchensink'), 'cypress'), '.')
shell.exec('node ./bin/convert.js')

View File

@@ -41,7 +41,7 @@ function replaceStringsIn (file) {
glob('./app/**/*.html', { realpath: true }, (err, htmlFiles) => {
if (err) throw err
glob('./cypress/integration/examples/**/*', { realpath: true }, (err, specFiles) => {
glob('./cypress/integration/**/*.js', { realpath: true }, (err, specFiles) => {
if (err) throw err
htmlFiles.concat(specFiles).forEach(function (file) {

View File

@@ -1,6 +1,5 @@
declare const example: {
getPathToExamples(): Promise<string[]>;
getFolderName(): string;
getPathToPlugins(): string;
getPathToSupportFiles(): Promise<string[]>;
getPathToTsConfig(): string;

View File

@@ -2,23 +2,22 @@ const path = require('path')
const Promise = require('bluebird')
const glob = Promise.promisify(require('glob'))
const pathToExamples = path.join(
__dirname,
'..',
'cypress',
'integration',
'**',
'*'
)
module.exports = {
getPathToExamples () {
return glob(
path.join(
__dirname,
'..',
'cypress',
'integration',
'examples',
'**',
'*'
)
)
return glob(pathToExamples, { nodir: true })
},
getFolderName () {
return 'examples'
getPathToExampleFolders () {
return glob(`${pathToExamples}${path.sep}`)
},
getPathToPlugins() {

View File

@@ -28,13 +28,13 @@
"devDependencies": {
"chai": "3.5.0",
"cross-env": "6.0.3",
"cypress-example-kitchensink": "1.14.0",
"cypress-example-kitchensink": "1.15.2",
"gulp": "4.0.2",
"gulp-clean": "0.4.0",
"gulp-gh-pages": "0.6.0-6",
"gulp-rev-all": "2.0.2",
"mocha": "2.5.3",
"resolve-pkg": "2.0.0",
"shelljs": "0.8.3"
"shelljs": "0.8.4"
}
}

View File

@@ -8,8 +8,8 @@ const cwd = process.cwd()
/* global describe, it */
describe('Cypress Example', function () {
it('returns path to example_spec', function () {
const expected = path.normalize(`${cwd}/cypress/integration/examples`)
it('returns path to examples', function () {
const expected = path.normalize(`${cwd}/cypress/integration`)
return example.getPathToExamples()
.then(expectToAllEqual(expected))

View File

@@ -3,7 +3,15 @@ exports['lib/scaffold .fileTree returns tree-like structure of scaffolded 1'] =
"name": "tests",
"children": [
{
"name": "examples",
"name": "1-getting-started",
"children": [
{
"name": "todo.spec.js"
}
]
},
{
"name": "2-advanced-examples",
"children": [
{
"name": "actions.spec.js"
@@ -105,7 +113,15 @@ exports['lib/scaffold .fileTree leaves out integration tests if using component
"name": "tests",
"children": [
{
"name": "examples",
"name": "1-getting-started",
"children": [
{
"name": "todo.spec.js"
}
]
},
{
"name": "2-advanced-examples",
"children": [
{
"name": "actions.spec.js"
@@ -207,7 +223,15 @@ exports['lib/scaffold .fileTree leaves out fixtures if configured to false 1'] =
"name": "tests",
"children": [
{
"name": "examples",
"name": "1-getting-started",
"children": [
{
"name": "todo.spec.js"
}
]
},
{
"name": "2-advanced-examples",
"children": [
{
"name": "actions.spec.js"
@@ -301,7 +325,15 @@ exports['lib/scaffold .fileTree leaves out support if configured to false 1'] =
"name": "tests",
"children": [
{
"name": "examples",
"name": "1-getting-started",
"children": [
{
"name": "todo.spec.js"
}
]
},
{
"name": "2-advanced-examples",
"children": [
{
"name": "actions.spec.js"
@@ -445,7 +477,15 @@ exports['lib/scaffold .fileTree leaves out plugins if configured to false 1'] =
"name": "tests",
"children": [
{
"name": "examples",
"name": "1-getting-started",
"children": [
{
"name": "todo.spec.js"
}
]
},
{
"name": "2-advanced-examples",
"children": [
{
"name": "actions.spec.js"

View File

@@ -474,9 +474,6 @@ module.exports = {
setScaffoldPaths (obj) {
obj = _.clone(obj)
obj.integrationExampleName = scaffold.integrationExampleName()
obj.integrationExamplePath = path.join(obj.integrationFolder, obj.integrationExampleName)
debug('set scaffold paths')
return scaffold.fileTree(obj)

View File

@@ -25,6 +25,7 @@ const konfig = require('../konfig')
const editors = require('../util/editors')
const fileOpener = require('../util/file-opener')
const api = require('../api')
const savedState = require('../saved_state')
const nullifyUnserializableValues = (obj) => {
// nullify values that cannot be cloned
@@ -392,9 +393,31 @@ const handleEvent = function (options, bus, event, id, type, arg) {
return sendErr(err)
})
case 'onboarding:closed':
case 'new:project:banner:closed':
return openProject.getProject()
.saveState({ showedOnBoardingModal: true })
.saveState({ showedNewProjectBanner: true })
.then(sendNull)
case 'has:opened:cypress':
return savedState.create()
.then(async (state) => {
const currentState = await state.get()
// we check if there is any state at all so users existing before
// we added firstOpenedCypress are not marked as new
const hasOpenedCypress = !!Object.keys(currentState).length
if (!currentState.firstOpenedCypress) {
await state.set('firstOpenedCypress', Date.now())
}
return hasOpenedCypress
})
.then(send)
case 'remove:scaffolded:files':
return openProject.getProject()
.removeScaffoldedFiles()
.then(sendNull)
case 'set:prompt:shown':

View File

@@ -480,6 +480,14 @@ export class ProjectBase<TServer extends ServerE2E | ServerCt> extends EE {
return this.automation
}
removeScaffoldedFiles () {
if (!this.cfg) {
throw new Error('Missing project config')
}
return scaffold.removeIntegration(this.cfg.integrationFolder, this.cfg)
}
// do not check files again and again - keep previous promise
// to refresh it - just close and open the project again.
determineIsNewProject (folder) {
@@ -511,12 +519,12 @@ export class ProjectBase<TServer extends ServerE2E | ServerCt> extends EE {
throw new Error('Missing integration folder')
}
return this.determineIsNewProject(cfg.integrationFolder)
return this.determineIsNewProject(cfg)
.then((untouchedScaffold) => {
const userHasSeenOnBoarding = _.get(cfg, 'state.showedOnBoardingModal', false)
const userHasSeenBanner = _.get(cfg, 'state.showedNewProjectBanner', false)
debugScaffold(`untouched scaffold ${untouchedScaffold} modal closed ${userHasSeenOnBoarding}`)
cfg.isNewProject = untouchedScaffold && !userHasSeenOnBoarding
debugScaffold(`untouched scaffold ${untouchedScaffold} banner closed ${userHasSeenBanner}`)
cfg.isNewProject = untouchedScaffold && !userHasSeenBanner
})
}

View File

@@ -22,7 +22,8 @@ browserY
isAppDevToolsOpen
isBrowserDevToolsOpen
reporterWidth
showedOnBoardingModal
showedNewProjectBanner
firstOpenedCypress
showedStudioModal
preferredOpener
ctReporterWidth

View File

@@ -1,6 +1,7 @@
const _ = require('lodash')
const Promise = require('bluebird')
const path = require('path')
const os = require('os')
const cypressEx = require('@packages/example')
const { fs } = require('./util/fs')
const glob = require('./util/glob')
@@ -9,8 +10,8 @@ const debug = require('debug')('cypress:server:scaffold')
const { isEmpty } = require('ramda')
const { isDefault } = require('./util/config')
const exampleFolderName = cypressEx.getFolderName()
const getExampleSpecsFullPaths = cypressEx.getPathToExamples()
const getExampleFolderFullPaths = cypressEx.getPathToExampleFolders()
const getPathFromIntegrationFolder = (file) => {
return file.substring(file.indexOf('integration/') + 'integration/'.length)
@@ -20,8 +21,10 @@ const isDifferentNumberOfFiles = (files, exampleSpecs) => {
return files.length !== exampleSpecs.length
}
const getExampleSpecs = () => {
return getExampleSpecsFullPaths
const getExampleSpecs = (foldersOnly = false) => {
const paths = foldersOnly ? getExampleFolderFullPaths : getExampleSpecsFullPaths
return paths
.then((fullPaths) => {
// short paths relative to integration folder (i.e. examples/actions.spec.js)
const shortPaths = _.map(fullPaths, (file) => {
@@ -38,6 +41,11 @@ const getExampleSpecs = () => {
}
const getIndexedExample = (file, index) => {
// convert to using posix sep if on win
if (os.platform() === 'win32') {
file = file.split(path.sep).join(path.posix.sep)
}
return index[getPathFromIntegrationFolder(file)]
}
@@ -51,6 +59,18 @@ const getFileSize = (file) => {
return fs.statAsync(file).get('size')
}
const fileSizeIsSame = (file, index) => {
return Promise.join(
getFileSize(file),
getFileSize(getIndexedExample(file, index)),
).spread((fileSize, originalFileSize) => {
return fileSize === originalFileSize
}).catch((e) => {
// if the file does not exist, return false
return false
})
}
const filesSizesAreSame = (files, index) => {
return Promise.join(
Promise.all(_.map(files, getFileSize)),
@@ -71,16 +91,23 @@ const componentTestingEnabled = (config) => {
return componentTestingEnabled && !isDefault(config, 'componentFolder')
}
const isNewProject = (integrationFolder) => {
const isNewProject = (config) => {
// logic to determine if new project
// 1. component testing is not enabled
// 2. there are no files in 'integrationFolder'
// 3. there is the same number of files in 'integrationFolder'
// 4. the files are named the same as the example files
// 5. the bytes of the files match the example files
// 1. 'integrationFolder' is still the default
// 2. component testing is not enabled
// 3. there are no files in 'integrationFolder'
// 4. there is the same number of files in 'integrationFolder'
// 5. the files are named the same as the example files
// 6. the bytes of the files match the example files
const { integrationFolder } = config
debug('determine if new project by globbing files in %o', { integrationFolder })
if (!isDefault(config, 'integrationFolder')) {
return Promise.resolve(false)
}
// checks for file up to 3 levels deep
return glob('{*,*/*,*/*/*}', { cwd: integrationFolder, realpath: true, nodir: true })
.then((files) => {
@@ -91,7 +118,7 @@ const isNewProject = (integrationFolder) => {
debug('- empty?', isEmpty(files))
if (isEmpty(files)) {
return true
} // 1
}
return getExampleSpecs()
.then((exampleSpecs) => {
@@ -100,14 +127,14 @@ const isNewProject = (integrationFolder) => {
debug('- different number of files?', numFilesDifferent)
if (numFilesDifferent) {
return false
} // 2
}
const filesNamesDifferent = filesNamesAreDifferent(files, exampleSpecs.index)
debug('- different file names?', filesNamesDifferent)
if (filesNamesDifferent) {
return false
} // 3
}
return filesSizesAreSame(files, exampleSpecs.index)
})
@@ -121,10 +148,6 @@ const isNewProject = (integrationFolder) => {
module.exports = {
isNewProject,
integrationExampleName () {
return exampleFolderName
},
integration (folder, config) {
debug(`integration folder ${folder}`)
@@ -140,7 +163,31 @@ module.exports = {
return getExampleSpecs()
.then(({ fullPaths }) => {
return Promise.all(_.map(fullPaths, (file) => {
return this._copy(file, path.join(folder, exampleFolderName), config)
return this._copy(file, folder, config, true)
}))
})
})
},
removeIntegration (folder, config) {
debug(`integration folder ${folder}`)
// skip if user has explicitly set integrationFolder
// since we wouldn't have scaffolded anything
if (!isDefault(config, 'integrationFolder')) {
return Promise.resolve()
}
return getExampleSpecs()
.then(({ shortPaths, index }) => {
return Promise.all(_.map(shortPaths, (file) => {
return this._removeFile(file, folder, index)
}))
}).then(() => {
// remove folders after we've removed all files
return getExampleSpecs(true).then(({ shortPaths }) => {
return Promise.all(_.map(shortPaths, (folderPath) => {
return this._removeFolder(folderPath, folder)
}))
})
})
@@ -198,10 +245,11 @@ module.exports = {
})
},
_copy (file, folder, config) {
_copy (file, folder, config, integration = false) {
// allow file to be relative or absolute
const src = path.resolve(cwd('lib', 'scaffold'), file)
const dest = path.join(folder, path.basename(file))
const destFile = integration ? getPathFromIntegrationFolder(file) : path.basename(file)
const dest = path.join(folder, destFile)
return this._assertInFileTree(dest, config)
.then(() => {
@@ -209,6 +257,27 @@ module.exports = {
})
},
_removeFile (file, folder, index) {
const dest = path.join(folder, file)
return fileSizeIsSame(dest, index)
.then((isSame) => {
if (isSame) {
// catch all errors since the user may have already removed
// the file or changed permissions, etc.
return fs.removeAsync(dest).catch(_.noop)
}
})
},
_removeFolder (folderPath, folder) {
const dest = path.join(folder, folderPath)
// catch all errors since the user may have already removed
// the folder, changed permissions, added their own files to the folder, etc.
return fs.rmdirAsync(dest).catch(_.noop)
},
verifyScaffolding (folder, fn) {
// we want to build out the folder + and example files
// but only create the example files if the folder doesn't

View File

@@ -606,9 +606,10 @@ describe('lib/cypress', () => {
return fs.statAsync(cfg.integrationFolder)
}).then(() => {
return Promise.join(
fs.statAsync(path.join(cfg.integrationFolder, 'examples', 'actions.spec.js')),
fs.statAsync(path.join(cfg.integrationFolder, 'examples', 'files.spec.js')),
fs.statAsync(path.join(cfg.integrationFolder, 'examples', 'viewport.spec.js')),
fs.statAsync(path.join(cfg.integrationFolder, '1-getting-started', 'todo.spec.js')),
fs.statAsync(path.join(cfg.integrationFolder, '2-advanced-examples', 'actions.spec.js')),
fs.statAsync(path.join(cfg.integrationFolder, '2-advanced-examples', 'files.spec.js')),
fs.statAsync(path.join(cfg.integrationFolder, '2-advanced-examples', 'viewport.spec.js')),
)
})
})

View File

@@ -1881,19 +1881,19 @@ describe('lib/config', () => {
})
context('.setScaffoldPaths', () => {
it('sets integrationExamplePath + integrationExampleName + scaffoldedFiles', () => {
it('sets scaffoldedFiles', () => {
const obj = {
integrationFolder: '/_test-output/path/to/project/cypress/integration',
}
sinon.stub(scaffold, 'fileTree').resolves([])
const scaffoldedFiles = ['/_test-output/path/to/project/cypress/integration/example.spec.js']
sinon.stub(scaffold, 'fileTree').resolves(scaffoldedFiles)
return config.setScaffoldPaths(obj).then((result) => {
expect(result).to.deep.eq({
integrationFolder: '/_test-output/path/to/project/cypress/integration',
integrationExamplePath: '/_test-output/path/to/project/cypress/integration/examples',
integrationExampleName: 'examples',
scaffoldedFiles: [],
scaffoldedFiles,
})
})
})

View File

@@ -24,6 +24,7 @@ const files = require(`${root}../lib/gui/files`)
const ensureUrl = require(`${root}../lib/util/ensure-url`)
const konfig = require(`${root}../lib/konfig`)
const api = require(`${root}../lib/api`)
const savedState = require(`${root}../lib/saved_state`)
describe('lib/gui/events', () => {
beforeEach(function () {
@@ -496,6 +497,52 @@ describe('lib/gui/events', () => {
})
})
})
describe('has:opened:cypress', function () {
beforeEach(function () {
this.state = {
set: sinon.stub().resolves(),
get: sinon.stub().resolves({}),
}
sinon.stub(savedState, 'create').resolves(this.state)
})
it('returns false when there is no existing saved state', function () {
return this.handleEvent('has:opened:cypress')
.then((assert) => {
assert.sendCalledWith(false)
})
})
it('returns true when there is any existing saved state', function () {
this.state.get.resolves({ shownOnboardingModal: true })
return this.handleEvent('has:opened:cypress')
.then((assert) => {
assert.sendCalledWith(true)
})
})
it('sets firstOpenedCypress when the user first opened Cypress if not already set', function () {
this.state.get.resolves({ shownOnboardingModal: true })
sinon.stub(Date, 'now').returns(12345)
return this.handleEvent('has:opened:cypress')
.then(() => {
expect(this.state.set).to.be.calledWith('firstOpenedCypress', 12345)
})
})
it('does not set firstOpenedCypress if already set', function () {
this.state.get.resolves({ firstOpenedCypress: 12345 })
return this.handleEvent('has:opened:cypress')
.then(() => {
expect(this.state.set).not.to.be.called
})
})
})
})
context('project events', () => {

View File

@@ -669,6 +669,11 @@ describe('lib/modes/run', () => {
video: true,
videosFolder: 'videos',
integrationFolder: '/path/to/integrationFolder',
resolved: {
integrationFolder: {
integrationFolder: { value: '/path/to/integrationFolder', from: 'config' },
},
},
})
sinon.stub(specsUtil, 'find').resolves([

View File

@@ -155,10 +155,10 @@ describe('lib/project-e2e', () => {
})
})
it('sets cfg.isNewProject to false when state.showedOnBoardingModal is true', function () {
it('sets cfg.isNewProject to false when state.showedNewProjectBanner is true', function () {
return savedState.create(this.todosPath)
.then((state) => {
sinon.stub(state, 'get').resolves({ showedOnBoardingModal: true })
sinon.stub(state, 'get').resolves({ showedNewProjectBanner: true })
return this.project.getConfig({ foo: 'bar' })
.then((cfg) => {
@@ -167,7 +167,7 @@ describe('lib/project-e2e', () => {
isNewProject: false,
baz: 'quux',
state: {
showedOnBoardingModal: true,
showedNewProjectBanner: true,
},
})

View File

@@ -62,6 +62,14 @@ describe('lib/saved_state', () => {
})
})
it('has an empty state by default', () => {
return savedState.create()
.then((state) => state.get())
.then((state) => {
expect(state).to.be.empty
})
})
it('only saves allowed keys', () => {
return savedState.create()
.then((state) => {

View File

@@ -20,46 +20,59 @@ describe('lib/scaffold', () => {
return Fixtures.remove()
})
context('.integrationExampleName', () => {
it('returns examples', () => {
expect(scaffold.integrationExampleName()).to.eq('examples')
})
})
// TODO: fix it later
context.skip('.isNewProject', () => {
context('.isNewProject', () => {
beforeEach(function () {
const todosPath = Fixtures.projectPath('todos')
this.pristinePath = Fixtures.projectPath('pristine')
})
return config.get(todosPath)
it('is true when integrationFolder is empty', function () {
const pristine = new ProjectE2E(this.pristinePath)
return pristine.getConfig()
.then((cfg) => {
this.cfg = cfg;
({ integrationFolder: this.integrationFolder } = this.cfg)
return pristine.determineIsNewProject(cfg)
}).then((ret) => {
expect(ret).to.be.true
})
})
it('is false when integrationFolder has been changed', function () {
const pristine = new ProjectE2E(this.pristinePath)
return pristine.getConfig({ integrationFolder: 'foo' })
.then((cfg) => {
return pristine.determineIsNewProject(cfg)
}).then((ret) => {
expect(ret).to.be.false
})
})
it('is false when files.length isnt 1', function () {
const id = () => {
this.ids = new ProjectE2E(this.idsPath)
const idsPath = Fixtures.projectPath('ids')
this.ids = new ProjectE2E(idsPath)
return this.ids.getConfig()
.then((cfg) => {
return this.ids.scaffold(cfg).return(cfg)
}).then((cfg) => {
return this.ids.determineIsNewProject(cfg.integrationFolder)
return this.ids.determineIsNewProject(cfg)
}).then((ret) => {
expect(ret).to.be.false
})
}
const todo = () => {
this.todos = new ProjectE2E(this.todosPath)
const todosPath = Fixtures.projectPath('todos')
this.todos = new ProjectE2E(todosPath)
return this.todos.getConfig()
.then((cfg) => {
return this.todos.scaffold(cfg).return(cfg)
}).then((cfg) => {
return this.todos.determineIsNewProject(cfg.integrationFolder)
return this.todos.determineIsNewProject(cfg)
}).then((ret) => {
expect(ret).to.be.false
})
@@ -69,29 +82,26 @@ describe('lib/scaffold', () => {
})
it('is true when files, name + bytes match to scaffold', function () {
// TODO this test really can move to scaffold
const pristine = new ProjectE2E(this.pristinePath)
return pristine.getConfig()
.then((cfg) => {
return pristine.scaffold(cfg).return(cfg)
}).then((cfg) => {
return pristine.determineIsNewProject(cfg.integrationFolder)
return pristine.determineIsNewProject(cfg)
}).then((ret) => {
expect(ret).to.be.true
})
})
it('is false when bytes dont match scaffold', function () {
// TODO this test really can move to scaffold
const pristine = new ProjectE2E(this.pristinePath)
return pristine.getConfig()
.then((cfg) => {
return pristine.scaffold(cfg).return(cfg)
}).then((cfg) => {
const example = scaffold.integrationExampleName()
const file = path.join(cfg.integrationFolder, example)
const file = path.join(cfg.integrationFolder, '1-getting-started', 'todo.spec.js')
// write some data to the file so it is now
// different in file size
@@ -102,7 +112,7 @@ describe('lib/scaffold', () => {
return fs.writeFileAsync(file, str).return(cfg)
})
}).then((cfg) => {
return pristine.determineIsNewProject(cfg.integrationFolder)
return pristine.determineIsNewProject(cfg)
}).then((ret) => {
expect(ret).to.be.false
})
@@ -126,10 +136,10 @@ describe('lib/scaffold', () => {
)
.spread((exampleSpecs) => {
return Promise.join(
fs.statAsync(`${this.integrationFolder}/examples/actions.spec.js`).get('size'),
fs.statAsync(`${this.integrationFolder}/1-getting-started/todo.spec.js`).get('size'),
fs.statAsync(exampleSpecs[0]).get('size'),
fs.statAsync(`${this.integrationFolder}/examples/location.spec.js`).get('size'),
fs.statAsync(exampleSpecs[8]).get('size'),
fs.statAsync(`${this.integrationFolder}/2-advanced-examples/location.spec.js`).get('size'),
fs.statAsync(exampleSpecs[9]).get('size'),
).spread((size1, size2, size3, size4) => {
expect(size1).to.eq(size2)
@@ -192,6 +202,117 @@ describe('lib/scaffold', () => {
})
})
context('.removeIntegration', () => {
beforeEach(function () {
const pristinePath = Fixtures.projectPath('pristine')
return config.get(pristinePath).then((cfg) => {
this.cfg = cfg;
({ integrationFolder: this.integrationFolder } = this.cfg)
})
})
it('removes all scaffolded files and folders', function () {
return scaffold.integration(this.integrationFolder, this.cfg)
.then(() => {
return glob('**/*', { cwd: this.integrationFolder })
})
.then((files) => {
expect(files.length).to.be.greaterThan(0)
})
.then(() => {
return scaffold.removeIntegration(this.integrationFolder, this.cfg)
})
.then(() => {
return glob('**/*', { cwd: this.integrationFolder })
})
.then((files) => {
expect(files.length).to.equal(0)
})
})
it('removes all scaffolded files and folders after the user has deleted files', function () {
return scaffold.integration(this.integrationFolder, this.cfg)
.then(() => {
return glob('**/*', { cwd: this.integrationFolder })
})
.then((files) => {
expect(files.length).to.be.greaterThan(0)
return Promise.join(
fs.unlinkAsync(`${this.integrationFolder}/2-advanced-examples/actions.spec.js`),
fs.unlinkAsync(`${this.integrationFolder}/2-advanced-examples/assertions.spec.js`),
fs.unlinkAsync(`${this.integrationFolder}/2-advanced-examples/location.spec.js`),
)
})
.then(() => {
return scaffold.removeIntegration(this.integrationFolder, this.cfg)
})
.then(() => {
return glob('**/*', { cwd: this.integrationFolder })
})
.then((files) => {
expect(files.length).to.equal(0)
})
})
it('does not remove files created by user', function () {
return scaffold.integration(this.integrationFolder, this.cfg)
.then(() => {
return glob('**/*', { cwd: this.integrationFolder })
})
.then((files) => {
expect(files.length).to.be.greaterThan(0)
return Promise.join(
fs.writeFileAsync(`${this.integrationFolder}/2-advanced-examples/custom1.spec.js`, 'foo'),
fs.writeFileAsync(`${this.integrationFolder}/2-advanced-examples/custom2.spec.js`, 'bar'),
)
})
.then(() => {
return scaffold.removeIntegration(this.integrationFolder, this.cfg)
})
.then(() => {
return glob('**/*', { cwd: this.integrationFolder })
})
.then((files) => {
expect(files).to.have.same.members([
'2-advanced-examples',
'2-advanced-examples/custom1.spec.js',
'2-advanced-examples/custom2.spec.js',
])
})
})
it('does not remove files modified by user', function () {
return scaffold.integration(this.integrationFolder, this.cfg)
.then(() => {
return glob('**/*', { cwd: this.integrationFolder })
})
.then((files) => {
expect(files.length).to.be.greaterThan(0)
return Promise.join(
fs.writeFileAsync(`${this.integrationFolder}/2-advanced-examples/actions.spec.js`, 'foo'),
fs.writeFileAsync(`${this.integrationFolder}/2-advanced-examples/location.spec.js`, 'bar'),
)
})
.then(() => {
return scaffold.removeIntegration(this.integrationFolder, this.cfg)
})
.then(() => {
return glob('**/*', { cwd: this.integrationFolder })
})
.then((files) => {
expect(files).to.have.same.members([
'2-advanced-examples',
'2-advanced-examples/actions.spec.js',
'2-advanced-examples/location.spec.js',
])
})
})
})
context('.support', () => {
beforeEach(function () {
const pristinePath = Fixtures.projectPath('pristine')

View File

@@ -15444,10 +15444,10 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
cypress-example-kitchensink@1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/cypress-example-kitchensink/-/cypress-example-kitchensink-1.14.0.tgz#f7048172f5871e64e4e8bc4c065900289d80824a"
integrity sha512-szaMnRjR9EqEDbQ4DE5Xdy8kHZx9F3qA/M55xNDuAsIm60Z9THbJxygKbz+C+sdpr0Rn6z2/cTxH/QzMJ8isNw==
cypress-example-kitchensink@1.15.2:
version "1.15.2"
resolved "https://registry.yarnpkg.com/cypress-example-kitchensink/-/cypress-example-kitchensink-1.15.2.tgz#325015726291a5e1e0d0cf89177eb9dec1c13e19"
integrity sha512-Ni/xbpMEllrNBrDVxh9juu7W4sbyBGpENuWvFdiojjBxzyvCCHaYCJIdF5kgGNzE5aP4AkoGW/jEk1KiKQzALA==
dependencies:
npm-run-all "^4.1.2"
serve "11.3.0"