UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

337 lines (269 loc) 9.79 kB
const _ = require('lodash') const Promise = require('bluebird') const path = require('path') const cypressEx = require('@packages/example') const { fs } = require('./util/fs') const glob = require('./util/glob') const cwd = require('./cwd') 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 getPathFromIntegrationFolder = (file) => { return file.substring(file.indexOf('integration/') + 'integration/'.length) } const isDifferentNumberOfFiles = (files, exampleSpecs) => { return files.length !== exampleSpecs.length } const getExampleSpecs = () => { return getExampleSpecsFullPaths .then((fullPaths) => { // short paths relative to integration folder (i.e. examples/actions.spec.js) const shortPaths = _.map(fullPaths, (file) => { return getPathFromIntegrationFolder(file) }) // index for quick lookup and for getting full path from short path const index = _.transform(shortPaths, (memo, spec, i) => { return memo[spec] = fullPaths[i] }, {}) return { fullPaths, shortPaths, index } }) } const getIndexedExample = (file, index) => { return index[getPathFromIntegrationFolder(file)] } const filesNamesAreDifferent = (files, index) => { return _.some(files, (file) => { return !getIndexedExample(file, index) }) } const getFileSize = (file) => { return fs.statAsync(file).get('size') } const filesSizesAreSame = (files, index) => { return Promise.join( Promise.all(_.map(files, getFileSize)), Promise.all(_.map(files, (file) => { return getFileSize(getIndexedExample(file, index)) })), ) .spread((fileSizes, originalFileSizes) => { return _.every(fileSizes, (size, i) => { return size === originalFileSizes[i] }) }) } const componentTestingEnabled = (config) => { const experimentalComponentTestingEnabled = _.get(config, 'resolved.experimentalComponentTesting.value', false) return experimentalComponentTestingEnabled && !isDefault(config, 'componentFolder') } const isNewProject = (integrationFolder) => { // 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 debug('determine if new project by globbing files in %o', { integrationFolder }) // checks for file up to 3 levels deep return glob('{*,*/*,*/*/*}', { cwd: integrationFolder, realpath: true, nodir: true }) .then((files) => { debug(`found ${files.length} files in folder ${integrationFolder}`) debug('determine if we should scaffold:') // TODO: add tests for this debug('- empty?', isEmpty(files)) if (isEmpty(files)) { return true } // 1 return getExampleSpecs() .then((exampleSpecs) => { const numFilesDifferent = isDifferentNumberOfFiles(files, exampleSpecs.shortPaths) 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) }) }).then((sameSizes) => { debug('- same sizes?', sameSizes) return sameSizes }) } module.exports = { isNewProject, integrationExampleName () { return exampleFolderName }, integration (folder, config) { debug(`integration folder ${folder}`) // skip if user has explicitly set integrationFolder // or if user has set up component testing if (!isDefault(config, 'integrationFolder') || componentTestingEnabled(config)) { return Promise.resolve() } return this.verifyScaffolding(folder, () => { debug(`copying examples into ${folder}`) return getExampleSpecs() .then(({ fullPaths }) => { return Promise.all(_.map(fullPaths, (file) => { return this._copy(file, path.join(folder, exampleFolderName), config) })) }) }) }, fixture (folder, config) { debug(`fixture folder ${folder}`) // skip if user has explicitly set fixturesFolder if (!config.fixturesFolder || !isDefault(config, 'fixturesFolder')) { return Promise.resolve() } return this.verifyScaffolding(folder, () => { debug(`copying example.json into ${folder}`) return this._copy(cypressEx.getPathToFixture(), folder, config) }) }, support (folder, config) { debug(`support folder ${folder}, support file ${config.supportFile}`) // skip if user has explicitly set supportFile if (!config.supportFile || !isDefault(config, 'supportFile')) { return Promise.resolve() } return this.verifyScaffolding(folder, () => { debug(`copying commands.js and index.js to ${folder}`) return cypressEx.getPathToSupportFiles() .then((supportFiles) => { return Promise.all( supportFiles.map((supportFilePath) => { return this._copy(supportFilePath, folder, config) }), ) }) }) }, plugins (folder, config) { debug(`plugins folder ${folder}`) // skip if user has explicitly set pluginsFile if (!config.pluginsFile || !isDefault(config, 'pluginsFile')) { return Promise.resolve() } return this.verifyScaffolding(folder, () => { debug(`copying index.js into ${folder}`) return this._copy(cypressEx.getPathToPlugins(), folder, config) }) }, _copy (file, folder, config) { // allow file to be relative or absolute const src = path.resolve(cwd('lib', 'scaffold'), file) const dest = path.join(folder, path.basename(file)) return this._assertInFileTree(dest, config) .then(() => { return fs.copyAsync(src, dest) }) }, verifyScaffolding (folder, fn) { // we want to build out the folder + and example files // but only create the example files if the folder doesn't // exist // // this allows us to automatically insert the folder on existing // projects (whenever they are booted) but allows the user to delete // the file and not have it re-generated each time // // this is ideal because users who are upgrading to newer cypress version // will still get the files scaffolded but existing users won't be // annoyed by new example files coming into their projects unnecessarily // console.debug('-- verify', folder) debug(`verify scaffolding in ${folder}`) return fs.statAsync(folder) .then(() => { return debug(`folder ${folder} already exists`) }).catch(() => { debug(`missing folder ${folder}`) return fn.call(this) }) }, fileTree (config = {}) { // returns a tree-like structure of what files are scaffolded. // this should be updated any time we add, remove, or update the name // of a scaffolded file const getFilePath = (dir, name) => { return path.relative(config.projectRoot, path.join(dir, name)) } return getExampleSpecs() .then((specs) => { let files = [] if (!componentTestingEnabled(config)) { files = _.map(specs.shortPaths, (file) => { return getFilePath(config.integrationFolder, file) }) } if (config.fixturesFolder) { files = files.concat([ getFilePath(config.fixturesFolder, 'example.json'), ]) } if (config.supportFolder && (config.supportFile !== false)) { files = files.concat([ getFilePath(config.supportFolder, 'commands.js'), getFilePath(config.supportFolder, 'index.js'), ]) } if (config.pluginsFile) { files = files.concat([ getFilePath(path.dirname(config.pluginsFile), 'index.js'), ]) } debug('scaffolded files %j', files) return this._fileListToTree(files) }) }, _fileListToTree (files) { // turns a list of file paths into a tree-like structure where // each entry has a name and children if it's a directory return _.reduce(files, (tree, file) => { let placeholder = tree const parts = file.split('/') _.each(parts, (part, index) => { let entry = _.find(placeholder, { name: part }) if (!entry) { entry = { name: part } if (index < (parts.length - 1)) { // if it's not the last, it's a directory entry.children = [] } placeholder.push(entry) } placeholder = entry.children }) return tree }, []) }, _assertInFileTree (filePath, config) { const relativeFilePath = path.relative(config.projectRoot, filePath) return this.fileTree(config) .then((fileTree) => { if (!this._inFileTree(fileTree, relativeFilePath)) { throw new Error(`You attempted to scaffold a file, '${relativeFilePath}', that's not in the scaffolded file tree. This is because you added, removed, or changed a scaffolded file. Make sure to update scaffold#fileTree to match your changes.`) } }) }, _inFileTree (fileTree, filePath) { let branch = fileTree const parts = filePath.split('/') for (let part of parts) { let found = _.find(branch, { name: part }) if (found) { branch = found.children } else { return false } } return true }, }