diff --git a/packages/desktop-gui/src/projects/projects-list.jsx b/packages/desktop-gui/src/projects/projects-list.jsx
index 86b21af8a904..035f9ac61d2d 100644
--- a/packages/desktop-gui/src/projects/projects-list.jsx
+++ b/packages/desktop-gui/src/projects/projects-list.jsx
@@ -46,7 +46,7 @@ class ProjectsList extends Component {
return (
- {' '}
+ {' '}
Error
{
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
+ if (specsStore.isLoading) return
const filteredSpecs = specsStore.getFilteredSpecs()
@@ -121,7 +123,8 @@ class SpecsList extends Component {
return (
- {this._firstTestBanner()}
+ {this._banners()}
+ {this._confirmRemoveScaffoldedFilesDialog()}
{this._specsList()}
@@ -415,24 +418,123 @@ class SpecsList extends Component {
{this.props.project.integrationFolder}
-
- {' '}
- Need help?
-
+
+
+ New Spec File
+
+ |
+
+ Need help?
+
+
)
}
- _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 (
+
+ )
+ }
+
+ if (this.props.project.newUserBannerOpen) {
+ return (
+
+ )
+ }
+
+ return null
+ }
+
+ _confirmRemoveScaffoldedFilesDialog = () => {
+ if (!this.props.project.newProjectBannerOpen) return null
return (
-
-
We've created some sample tests around key Cypress concepts. Run the first one or create your own test file.
-
How to write tests
-
-
+
+
+ ×
+
Are you sure that you want to delete all example spec files?
+ Note: this will not delete any new or edited files.
+
+
+ Cancel
+
+
+
)
}
@@ -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
diff --git a/packages/desktop-gui/src/specs/specs.scss b/packages/desktop-gui/src/specs/specs.scss
index a255240ef5f3..f4ce5e9755ad 100644
--- a/packages/desktop-gui/src/specs/specs.scss
+++ b/packages/desktop-gui/src/specs/specs.scss
@@ -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;
+ }
}
}
diff --git a/packages/desktop-gui/src/styles/components/_alerts.scss b/packages/desktop-gui/src/styles/components/_alerts.scss
index 223c5181cd6d..c9545e85e77d 100644
--- a/packages/desktop-gui/src/styles/components/_alerts.scss
+++ b/packages/desktop-gui/src/styles/components/_alerts.scss
@@ -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;
+ }
+}
diff --git a/packages/example/README.md b/packages/example/README.md
index a9f94349e587..6ab87bd6b9f7 100644
--- a/packages/example/README.md
+++ b/packages/example/README.md
@@ -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
diff --git a/packages/example/bin/build.js b/packages/example/bin/build.js
index e46b80331f1e..3873aabd2046 100644
--- a/packages/example/bin/build.js
+++ b/packages/example/bin/build.js
@@ -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')
diff --git a/packages/example/bin/convert.js b/packages/example/bin/convert.js
index 4936485cd923..c1fd6aa3c569 100755
--- a/packages/example/bin/convert.js
+++ b/packages/example/bin/convert.js
@@ -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) {
diff --git a/packages/example/lib/example.d.ts b/packages/example/lib/example.d.ts
index 35ea4dbef283..538f7529821d 100644
--- a/packages/example/lib/example.d.ts
+++ b/packages/example/lib/example.d.ts
@@ -1,6 +1,5 @@
declare const example: {
getPathToExamples(): Promise
;
- getFolderName(): string;
getPathToPlugins(): string;
getPathToSupportFiles(): Promise;
getPathToTsConfig(): string;
diff --git a/packages/example/lib/example.js b/packages/example/lib/example.js
index e5e95a541c1f..6c070722e328 100644
--- a/packages/example/lib/example.js
+++ b/packages/example/lib/example.js
@@ -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() {
diff --git a/packages/example/package.json b/packages/example/package.json
index 7b9a4cbc3b1f..d9fe37f507fe 100644
--- a/packages/example/package.json
+++ b/packages/example/package.json
@@ -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"
}
}
diff --git a/packages/example/test/example_spec.js b/packages/example/test/example_spec.js
index f793c968705e..3e1d38374452 100644
--- a/packages/example/test/example_spec.js
+++ b/packages/example/test/example_spec.js
@@ -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))
diff --git a/packages/server/__snapshots__/scaffold_spec.js b/packages/server/__snapshots__/scaffold_spec.js
index 2e80a884555b..31ecc78eb865 100644
--- a/packages/server/__snapshots__/scaffold_spec.js
+++ b/packages/server/__snapshots__/scaffold_spec.js
@@ -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"
diff --git a/packages/server/lib/config.js b/packages/server/lib/config.js
index c15fa6ef67ef..4320f4a4ba94 100644
--- a/packages/server/lib/config.js
+++ b/packages/server/lib/config.js
@@ -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)
diff --git a/packages/server/lib/gui/events.js b/packages/server/lib/gui/events.js
index f23aff5534d1..b4ed086abb06 100644
--- a/packages/server/lib/gui/events.js
+++ b/packages/server/lib/gui/events.js
@@ -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':
diff --git a/packages/server/lib/project-base.ts b/packages/server/lib/project-base.ts
index 3066605f4f2a..ce45b42933b9 100644
--- a/packages/server/lib/project-base.ts
+++ b/packages/server/lib/project-base.ts
@@ -480,6 +480,14 @@ export class ProjectBase 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 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
})
}
diff --git a/packages/server/lib/saved_state.js b/packages/server/lib/saved_state.js
index d4bdd8a75359..07e48f7ec897 100644
--- a/packages/server/lib/saved_state.js
+++ b/packages/server/lib/saved_state.js
@@ -22,7 +22,8 @@ browserY
isAppDevToolsOpen
isBrowserDevToolsOpen
reporterWidth
-showedOnBoardingModal
+showedNewProjectBanner
+firstOpenedCypress
showedStudioModal
preferredOpener
ctReporterWidth
diff --git a/packages/server/lib/scaffold.js b/packages/server/lib/scaffold.js
index 2dbb92f0fd7f..7f29e59608ef 100644
--- a/packages/server/lib/scaffold.js
+++ b/packages/server/lib/scaffold.js
@@ -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
diff --git a/packages/server/test/integration/cypress_spec.js b/packages/server/test/integration/cypress_spec.js
index 4f75a8eaa115..00d63dd49bc3 100644
--- a/packages/server/test/integration/cypress_spec.js
+++ b/packages/server/test/integration/cypress_spec.js
@@ -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')),
)
})
})
diff --git a/packages/server/test/unit/config_spec.js b/packages/server/test/unit/config_spec.js
index 44166d173791..aab1239f8dcb 100644
--- a/packages/server/test/unit/config_spec.js
+++ b/packages/server/test/unit/config_spec.js
@@ -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,
})
})
})
diff --git a/packages/server/test/unit/gui/events_spec.js b/packages/server/test/unit/gui/events_spec.js
index 61ae3a1d8c7b..463db504a127 100644
--- a/packages/server/test/unit/gui/events_spec.js
+++ b/packages/server/test/unit/gui/events_spec.js
@@ -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', () => {
diff --git a/packages/server/test/unit/modes/run_spec.js b/packages/server/test/unit/modes/run_spec.js
index 382c391bca01..09d0e2c1477c 100644
--- a/packages/server/test/unit/modes/run_spec.js
+++ b/packages/server/test/unit/modes/run_spec.js
@@ -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([
diff --git a/packages/server/test/unit/project_spec.js b/packages/server/test/unit/project_spec.js
index 5ef047b3c19e..0abfab05327f 100644
--- a/packages/server/test/unit/project_spec.js
+++ b/packages/server/test/unit/project_spec.js
@@ -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,
},
})
diff --git a/packages/server/test/unit/saved_state_spec.js b/packages/server/test/unit/saved_state_spec.js
index 76676957153c..3e71d4b74098 100644
--- a/packages/server/test/unit/saved_state_spec.js
+++ b/packages/server/test/unit/saved_state_spec.js
@@ -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) => {
diff --git a/packages/server/test/unit/scaffold_spec.js b/packages/server/test/unit/scaffold_spec.js
index 48483e719426..558fe0ceb468 100644
--- a/packages/server/test/unit/scaffold_spec.js
+++ b/packages/server/test/unit/scaffold_spec.js
@@ -20,46 +20,59 @@ describe('lib/scaffold', () => {
return Fixtures.remove()
})
- context('.integrationExampleName', () => {
- it('returns examples', () => {
- expect(scaffold.integrationExampleName()).to.eq('examples')
+ context('.isNewProject', () => {
+ beforeEach(function () {
+ this.pristinePath = Fixtures.projectPath('pristine')
})
- })
- // TODO: fix it later
- context.skip('.isNewProject', () => {
- beforeEach(function () {
- const todosPath = Fixtures.projectPath('todos')
+ it('is true when integrationFolder is empty', function () {
+ const pristine = new ProjectE2E(this.pristinePath)
- return config.get(todosPath)
+ 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')
diff --git a/yarn.lock b/yarn.lock
index 6d0496974667..5fa7f947011d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -15456,10 +15456,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"