diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 515ee6236ad6..c1c0463c52b2 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -2763,8 +2763,6 @@ declare namespace Cypress { clientRoute: string configFile: string cypressEnv: string - integrationExampleName: string - integrationExamplePath: string isNewProject: boolean isTextTerminal: boolean morgan: boolean diff --git a/packages/desktop-gui/cypress/fixtures/config.json b/packages/desktop-gui/cypress/fixtures/config.json index 10596c35d9d8..601a2c7a7ecd 100644 --- a/packages/desktop-gui/cypress/fixtures/config.json +++ b/packages/desktop-gui/cypress/fixtures/config.json @@ -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, diff --git a/packages/desktop-gui/cypress/integration/specs_list_spec.js b/packages/desktop-gui/cypress/integration/specs_list_spec.js index 509a5cc585f9..0cdd87393c15 100644 --- a/packages/desktop-gui/cypress/integration/specs_list_spec.js +++ b/packages/desktop-gui/cypress/integration/specs_list_spec.js @@ -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') + context('banner', function () { + it('displays', function () { + cy.get('.new-project-banner') 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('is dismissable', function () { + cy.get('.new-project-banner').find('.close').click() + cy.get('.new-project-banner').should('not.exist') }) - 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) + it('does not display new user banner even when closed', function () { + this.ipc.hasOpenedCypress.resolves(false) - cy.get('.is-more').should('have.text', ' ... 17 more files ...') - }) + 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('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('opens link to docs on click of help 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('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('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') }) }) + }) + }) - 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) - }) - }) + describe('first time user in existing project', function () { + beforeEach(function () { + this.openProject.resolve(this.config) + this.ipc.hasOpenedCypress.resolves(false) }) context('banner', function () { - beforeEach(function () { - cy.get('.modal').find('.btn-success').click() - }) - it('displays', function () { - cy.get('.first-test-banner') + cy.get('.new-user-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-user-banner').find('.close').click() + 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') + 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' }) }) }) }) diff --git a/packages/desktop-gui/src/app/intro.jsx b/packages/desktop-gui/src/app/intro.jsx index 0aa0d6ab8159..00a6b5dd57dc 100644 --- a/packages/desktop-gui/src/app/intro.jsx +++ b/packages/desktop-gui/src/app/intro.jsx @@ -34,8 +34,8 @@ class Default extends Component { onDrop={this._drop} > - - + +

Drag your project here or select manually.

@@ -51,7 +51,7 @@ class Default extends Component { return (

- {' '} + {' '} We recommend versioning Cypress per project and{' '} installing it via npm diff --git a/packages/desktop-gui/src/lib/ipc.js b/packages/desktop-gui/src/lib/ipc.js index 0e825f2dc1c4..6cc4843c7cc2 100644 --- a/packages/desktop-gui/src/lib/ipc.js +++ b/packages/desktop-gui/src/lib/ipc.js @@ -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') diff --git a/packages/desktop-gui/src/project-nav/browsers.jsx b/packages/desktop-gui/src/project-nav/browsers.jsx index 38becbd202c2..359bda1d58ba 100644 --- a/packages/desktop-gui/src/project-nav/browsers.jsx +++ b/packages/desktop-gui/src/project-nav/browsers.jsx @@ -35,7 +35,7 @@ export default class Browsers extends Component { return (

  • diff --git a/packages/desktop-gui/src/project-nav/project-nav.jsx b/packages/desktop-gui/src/project-nav/project-nav.jsx index 08b5915a9976..3dd55d873c15 100644 --- a/packages/desktop-gui/src/project-nav/project-nav.jsx +++ b/packages/desktop-gui/src/project-nav/project-nav.jsx @@ -12,19 +12,19 @@ export default class ProjectNav extends Component {
    • - {' '} + {' '} Tests
    • - {' '} + {' '} Runs
    • - {' '} + {' '} Settings
    • diff --git a/packages/desktop-gui/src/project/onboarding.jsx b/packages/desktop-gui/src/project/onboarding.jsx deleted file mode 100644 index 851b8ff89277..000000000000 --- a/packages/desktop-gui/src/project/onboarding.jsx +++ /dev/null @@ -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 ( - -
      -
      -

      To help you get started...

      -

      - We've added some folders and example tests to your project. Try running the tests in the - - {' '} - {project.integrationExampleName}{' '} - - folder or add your own test files to - - {' '} - cypress/integration - . -

      -
      -
        -
      • - - {' '} - {project.name} - -
          -
        • - - {' '} - ... - -
        • - {this._scaffoldedFiles(project.scaffoldedFiles, 'new-code')} -
        -
      • -
      -
      -
      - - OK, got it! - -
      -
      -
      -
      - ) - } - - _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 ( -
    • - - {' '} - {file.name} - -
        - {this._scaffoldedFiles(file.children)} -
      -
    • - ) - } - - return ( -
    • - - {' '} - {file.name} - -
    • - ) - }) - } - - _openExampleSpec = () => { - ipc.openFinder(this.props.project.integrationExamplePath) - } - - _openIntegrationFolder = () => { - ipc.openFinder(this.props.project.integrationFolder) - } -} - -export default OnBoarding diff --git a/packages/desktop-gui/src/project/onboarding.scss b/packages/desktop-gui/src/project/onboarding.scss deleted file mode 100644 index cc939162c1e4..000000000000 --- a/packages/desktop-gui/src/project/onboarding.scss +++ /dev/null @@ -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; - } - } -} diff --git a/packages/desktop-gui/src/project/project-model.js b/packages/desktop-gui/src/project/project-model.js index ec39edffddcf..c7732780bec2 100644 --- a/packages/desktop-gui/src/project/project-model.js +++ b/packages/desktop-gui/src/project/project-model.js @@ -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 } diff --git a/packages/desktop-gui/src/project/project.jsx b/packages/desktop-gui/src/project/project.jsx index ce1a1828dc25..433460088394 100644 --- a/packages/desktop-gui/src/project/project.jsx +++ b/packages/desktop-gui/src/project/project.jsx @@ -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()}
    - ) } diff --git a/packages/desktop-gui/src/project/warning-message.jsx b/packages/desktop-gui/src/project/warning-message.jsx index 156a6c8d9149..dec4d868e695 100644 --- a/packages/desktop-gui/src/project/warning-message.jsx +++ b/packages/desktop-gui/src/project/warning-message.jsx @@ -12,7 +12,7 @@ class WarningMessage extends Component { return (

    - {' '} + {' '} Warning

    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 (

    ) } - _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 ( +
    +

    + Welcome to Cypress! +

    +

    We've created some sample test files that demonstrate key Cypress concepts to help you get started.

    +

    + How to write your first test +   |   + No thanks, delete example files +

    + +
    + ) + } + + if (this.props.project.newUserBannerOpen) { + return ( +
    +

    + New to Cypress? +

    +

    We've created some new user guides on key Cypress concepts to help you get started.

    +

    + How to write your first test +   |   + Introduction guide to Cypress +

    + +
    + ) + } + + 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"