From 5790f75661f423da3e911eea8d6a2d98b181d831 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Mon, 2 Nov 2020 18:35:10 +0200 Subject: [PATCH 1/9] Implement the basic babel code transformation for configs --- npm/create-cypress-tests/package.json | 7 +- .../component-testing/babel/babelTransform.ts | 114 +++++++++++++ .../init-component-testing.ts | 42 +++-- .../component-testing/templates/Template.ts | 4 + .../templates/react/babel.test.ts | 18 ++- .../templates/react/babel.ts | 12 ++ npm/create-cypress-tests/src/index.ts | 1 - npm/create-cypress-tests/tsconfig.json | 2 + .../example/cypress/fixtures/example.json | 2 +- packages/example/cypress/plugins/index.js | 1 + packages/example/cypress/support/commands.js | 8 +- packages/example/cypress/tsconfig.json | 10 -- yarn.lock | 153 ++++++++++++++++++ 13 files changed, 341 insertions(+), 33 deletions(-) create mode 100644 npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts delete mode 100644 packages/example/cypress/tsconfig.json diff --git a/npm/create-cypress-tests/package.json b/npm/create-cypress-tests/package.json index 512ea12d4de4..40bb00ebb2e3 100644 --- a/npm/create-cypress-tests/package.json +++ b/npm/create-cypress-tests/package.json @@ -7,9 +7,13 @@ "scripts": { "build": "yarn prepare-example && tsc -p ./tsconfig.json && chmod +x dist/src/index.js", "prepare-example": "rimraf initial-template/* && cp -r ../../packages/example/* initial-template", - "test": "cross-env TS_NODE_PROJECT=./tsconfig.test.json mocha --config .mocharc.json ./src/**/*.test.ts" + "test": "cross-env TS_NODE_PROJECT=./tsconfig.test.json mocha --config .mocharc.json './src/**/*.test.ts'", + "test:watch": "yarn test -w" }, "dependencies": { + "@babel/core": "^7.5.4", + "@babel/plugin-transform-typescript": "^7.2.0", + "@babel/types": "^7.5.0", "chalk": "4.1.0", "cli-highlight": "2.1.4", "commander": "6.1.0", @@ -18,6 +22,7 @@ "ora": "^5.1.0" }, "devDependencies": { + "@types/babel__core": "^7.1.2", "@types/inquirer": "7.3.1", "@types/mock-fs": "4.10.0", "@types/node": "9.6.49", diff --git a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts new file mode 100644 index 000000000000..cec00b954e92 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts @@ -0,0 +1,114 @@ +import path from 'path' +import * as fs from 'fs-extra' +import * as babel from '@babel/core' +import * as babelTypes from '@babel/types' + +type PluginsAst = Record<'Require' | 'ModuleExportsBody', ReturnType> + +export function createTransformPluginsFileBabelPlugin (ast: PluginsAst): babel.PluginObj { + return { + visitor: { + Program: (path) => { + path.unshiftContainer('body', ast.Require()) + }, + Function: (path) => { + if (!babelTypes.isAssignmentExpression(path.parent)) { + return + } + + const assignment = path.parent.left + + const isModuleExports = + babelTypes.isMemberExpression(assignment) + && babelTypes.isIdentifier(assignment.object) + && assignment.object.name === 'module' + && babelTypes.isIdentifier(assignment.property) + && assignment.property.name === 'exports' + + if (isModuleExports && babelTypes.isFunction(path.parent.right)) { + const paramsLength = path.parent.right.params.length + + if (paramsLength === 0) { + path.parent.right.params.push(babelTypes.identifier('on')) + } + + if (paramsLength === 1) { + path.parent.right.params.push(babelTypes.identifier('config')) + } + + path.get('body').pushContainer('body' as never, ast.ModuleExportsBody()) + } + }, + }, + } +} + +function tryRequirePrettier () { + try { + return require('prettier') + } catch (e) { + return null + } +} + +async function transformFileViaPlugin (filePath: string, babelPlugin: babel.PluginObj) { + try { + const pluginsFile = await fs.readFile(filePath, { encoding: 'utf-8' }) + + const updatedResult = await babel.transformAsync(pluginsFile, { + plugins: [babelPlugin], + }) + + if (!updatedResult) { + return false + } + + let finalCode = updatedResult.code + const maybePrettier = tryRequirePrettier() + + if (maybePrettier && maybePrettier.format) { + finalCode = maybePrettier.format(finalCode, { parser: 'babel' }) + } + + await fs.writeFile(filePath, finalCode) + + return true + } catch (e) { + return false + } +} + +export async function autoInjectPluginsCode (pluginsFilePath: string, ast: PluginsAst) { + return transformFileViaPlugin(pluginsFilePath, createTransformPluginsFileBabelPlugin(ast)) +} + +export async function getPluginsSourceExample (ast: PluginsAst) { + const exampleCode = [ + 'module.exports = (on, config) => {', + '', + '}', + ].join('\n') + + const babelResult = await babel.transformAsync(exampleCode, { + plugins: [createTransformPluginsFileBabelPlugin(ast)], + }) + + if (!babelResult?.code) { + throw new Error('Can not generate code example for plugins file') + } + + return babelResult.code +} + +export async function injectImportSupportCode (supportFilePath: string, importCode: string) { + const template = babel.template(importCode) + const plugin: babel.PluginObj = { + visitor: { + Program: (path) => { + path.unshiftContainer('body', template()) + }, + }, + } + + return transformFileViaPlugin(supportFilePath, plugin) +} diff --git a/npm/create-cypress-tests/src/component-testing/init-component-testing.ts b/npm/create-cypress-tests/src/component-testing/init-component-testing.ts index a6b277b27d09..2423a1d117ab 100644 --- a/npm/create-cypress-tests/src/component-testing/init-component-testing.ts +++ b/npm/create-cypress-tests/src/component-testing/init-component-testing.ts @@ -6,6 +6,7 @@ import highlight from 'cli-highlight' import { Template } from './templates/Template' import { guessTemplate } from './templates/guessTemplate' import { installAdapter } from './installAdapter' +import { autoInjectPluginsCode, getPluginsSourceExample } from './babel/babelTransform' function printCypressJsonHelp ( cypressJsonPath: string, @@ -31,7 +32,7 @@ function printCypressJsonHelp ( console.log(`\n${highlightedCode}\n`) } -function printSupportHelper (supportFilePath: string, framework: string) { +function injectAndShowSupportConfig (supportFilePath: string, framework: string) { const stepNumber = chalk.bold('2.') const importCode = `import \'@cypress/${framework}/support\'` const requireCode = `require(\'@cypress/${framework}/support\')` @@ -60,15 +61,30 @@ function printSupportHelper (supportFilePath: string, framework: string) { } } -function printPluginHelper (pluginCode: string, pluginsFilePath: string) { - const highlightedPluginCode = highlight(pluginCode, { language: 'js' }) - const relativePluginsFilePath = path.relative(process.cwd(), pluginsFilePath) +async function injectAndShowPluginConfig (template: Template, pluginsFilePath: string, emptyProject: boolean) { + const ast = template.getPluginsCodeAst?.() - const stepTitle = fs.existsSync(pluginsFilePath) - ? `And this to the ${chalk.green(relativePluginsFilePath)}` - : `And this to your plugins file (https://docs.cypress.io/guides/tooling/plugins-guide.html)` + if (!ast) { + return + } + + const injected = await autoInjectPluginsCode(pluginsFilePath, ast) + + if (injected && emptyProject) { + return + } - console.log(`${chalk.bold('3.')} ${stepTitle}:`) + const pluginsCode = await getPluginsSourceExample(ast) + const highlightedPluginCode = highlight(pluginsCode, { language: 'js' }) + const relativePluginsFilePath = fs.existsSync(pluginsFilePath) + ? path.relative(process.cwd(), pluginsFilePath) + : 'plugins file (https://docs.cypress.io/guides/tooling/plugins-guide.html)`' + + const stepTitle = injected + ? `✅ Injected into ${chalk.green(relativePluginsFilePath)}` + : `❌ ${chalk.red(`We were not able to modify your ${relativePluginsFilePath}`)}. Add this manually:` + + console.log(stepTitle) console.log(`\n${highlightedPluginCode}\n`) } @@ -142,13 +158,9 @@ export async function initComponentTesting ({ config, useYarn, cypressConfigP console.log(`Here are instructions of how to get started with component testing for ${chalk.cyan(chosenTemplateName)}:`) printCypressJsonHelp(cypressConfigPath, componentFolder) - printSupportHelper(supportFilePath, framework) - printPluginHelper( - chosenTemplate.getPluginsCode(templatePayload, { - cypressProjectRoot, - }), - pluginsFilePath, - ) + injectAndShowSupportConfig(supportFilePath, framework) + + await injectAndShowPluginConfig(chosenTemplate, pluginsFilePath, false) if (chosenTemplate.printHelper) { chosenTemplate.printHelper() diff --git a/npm/create-cypress-tests/src/component-testing/templates/Template.ts b/npm/create-cypress-tests/src/component-testing/templates/Template.ts index 32c4484b65a2..b73d5f6fc210 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/Template.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/Template.ts @@ -1,8 +1,12 @@ +import * as babel from '@babel/core' + export interface Template { message: string getExampleUrl: ({ componentFolder }: { componentFolder: string }) => string recommendedComponentFolder: string test(rootPath: string): { success: boolean, payload?: T } + getPluginsTransformAst?: () => babel.PluginObj + getPluginsCodeAst?: () => Record<'Require' | 'ModuleExportsBody', ReturnType> getPluginsCode: ( payload: T | null, options: { cypressProjectRoot: string }, diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts index cd62f0db0d50..9651e49f072c 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts @@ -1,8 +1,10 @@ import { expect } from 'chai' import mockFs from 'mock-fs' +import * as babel from '@babel/core' import { BabelTemplate } from './babel' +import { createTransformPluginsFileBabelPlugin } from '../../bablTransform' -describe('babel installation template', () => { +describe.only('babel installation template', () => { beforeEach(mockFs.restore) it('resolves babel.config.json', () => { @@ -67,4 +69,18 @@ describe('babel installation template', () => { expect(success).to.equal(true) }) + + it.only('automatically injects config to the code', () => { + const code = [ + 'const something = require("something")', + 'module.exports = (on) => {', + '};', + ].join('\n') + + const output = babel.transformSync(code, { + plugins: [createTransformPluginsFileBabelPlugin(BabelTemplate.getPluginsCodeAst!())], + }) + + console.log('OUTPUT\n', output?.code) + }) }) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts b/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts index c85828456545..1a2a52617a19 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts @@ -1,5 +1,7 @@ import chalk from 'chalk' import findUp from 'find-up' +import * as babel from '@babel/core' + import { Template } from '../Template' import { createFindPackageJsonIterator } from '../../../findPackageJson' @@ -23,6 +25,16 @@ export const BabelTemplate: Template = { '}', ].join('\n') }, + getPluginsCodeAst: () => { + return { + Require: babel.template('const preprocessor = require(\'@cypress/react/plugins/babel\')'), + ModuleExportsBody: babel.template([ + ' preprocessor(on, config)', + ' // IMPORTANT to return the config object', + ' return config', + ].join('\n')), + } + }, test: (cwd) => { const babelConfig = findUp.sync( ['babel.config.js', 'babel.config.json', '.babelrc', '.babelrc.json'], diff --git a/npm/create-cypress-tests/src/index.ts b/npm/create-cypress-tests/src/index.ts index 525b2eaf6e3c..3d90280343d5 100644 --- a/npm/create-cypress-tests/src/index.ts +++ b/npm/create-cypress-tests/src/index.ts @@ -8,7 +8,6 @@ program .option('--use-npm', 'Use npm even if yarn is available') .option('--ignore-ts', 'Ignore typescript if available') .option('--component-tests', 'Run component testing installation wizard without asking') -.help((autoGeneratedHelp) => `${autoGeneratedHelp}\nRun the ${chalk.green('cypress 🌲')} installation wizard. Make sure to run this command from ${chalk.bold('your existing project directory')}.\n`) program.parse(process.argv) diff --git a/npm/create-cypress-tests/tsconfig.json b/npm/create-cypress-tests/tsconfig.json index d75e56bc1bf4..36480af8fb93 100644 --- a/npm/create-cypress-tests/tsconfig.json +++ b/npm/create-cypress-tests/tsconfig.json @@ -3,6 +3,7 @@ "outDir": "./dist", "rootDir": ".", "esModuleInterop": true, + "allowJs": true, "allowSyntheticDefaultImports": true, "declaration": true, "moduleResolution": "node", @@ -24,6 +25,7 @@ ], "include": [ "./src/**/*.ts", + "src/component-testing/babel/examples/*", "src/initial-template/*" ] } diff --git a/packages/example/cypress/fixtures/example.json b/packages/example/cypress/fixtures/example.json index da18d9352a17..02e4254378e9 100644 --- a/packages/example/cypress/fixtures/example.json +++ b/packages/example/cypress/fixtures/example.json @@ -2,4 +2,4 @@ "name": "Using fixtures to represent data", "email": "hello@cypress.io", "body": "Fixtures are a great way to mock data for responses to routes" -} \ No newline at end of file +} diff --git a/packages/example/cypress/plugins/index.js b/packages/example/cypress/plugins/index.js index aa9918d21530..59b2bab6e4e6 100644 --- a/packages/example/cypress/plugins/index.js +++ b/packages/example/cypress/plugins/index.js @@ -15,6 +15,7 @@ /** * @type {Cypress.PluginConfig} */ +// eslint-disable-next-line no-unused-vars module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config diff --git a/packages/example/cypress/support/commands.js b/packages/example/cypress/support/commands.js index ca4d256f3eb1..119ab03f7cda 100644 --- a/packages/example/cypress/support/commands.js +++ b/packages/example/cypress/support/commands.js @@ -10,16 +10,16 @@ // // // -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) +// Cypress.Commands.add('login', (email, password) => { ... }) // // // -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) // // // -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) // // // -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) diff --git a/packages/example/cypress/tsconfig.json b/packages/example/cypress/tsconfig.json deleted file mode 100644 index 4109e0ec1aff..000000000000 --- a/packages/example/cypress/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["es5", "dom"], - "types": ["cypress"] - }, - "include": [ - "**/*.ts" - ] -} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e5d6d2df2118..9fcbf39a55c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -142,6 +142,28 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/core@^7.5.4": + version "7.12.3" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.3.tgz#1b436884e1e3bff6fb1328dc02b208759de92ad8" + integrity sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.1" + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helpers" "^7.12.1" + "@babel/parser" "^7.12.3" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/generator@^7.11.5", "@babel/generator@^7.11.6", "@babel/generator@^7.4.0", "@babel/generator@^7.4.4", "@babel/generator@^7.6.0", "@babel/generator@^7.9.0", "@babel/generator@^7.9.3": version "7.11.6" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620" @@ -151,6 +173,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.1.tgz#0d70be32bdaa03d7c51c8597dda76e0df1f15468" + integrity sha512-DB+6rafIdc9o72Yc3/Ph5h+6hUjeOp66pF0naQBgUFFuPqzQwIlPTm3xZR7YNvduIMtkDIj2t21LSQwnbCrXvg== + dependencies: + "@babel/types" "^7.12.1" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" @@ -206,6 +237,17 @@ "@babel/helper-replace-supers" "^7.10.4" "@babel/helper-split-export-declaration" "^7.10.4" +"@babel/helper-create-class-features-plugin@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz#3c45998f431edd4a9214c5f1d3ad1448a6137f6e" + integrity sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.12.1" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.1" + "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/helper-create-regexp-features-plugin@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" @@ -262,6 +304,13 @@ dependencies: "@babel/types" "^7.11.0" +"@babel/helper-member-expression-to-functions@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz#fba0f2fcff3fba00e6ecb664bb5e6e26e2d6165c" + integrity sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ== + dependencies: + "@babel/types" "^7.12.1" + "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" @@ -269,6 +318,13 @@ dependencies: "@babel/types" "^7.10.4" +"@babel/helper-module-imports@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.1.tgz#1644c01591a15a2f084dd6d092d9430eb1d1216c" + integrity sha512-ZeC1TlMSvikvJNy1v/wPIazCu3NdOwgYZLIkmIyAsGhqkNpiDoQQRmaCK8YP4Pq3GPTLPV9WXaPCJKvx06JxKA== + dependencies: + "@babel/types" "^7.12.1" + "@babel/helper-module-transforms@^7.10.1", "@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.11.0", "@babel/helper-module-transforms@^7.9.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" @@ -282,6 +338,21 @@ "@babel/types" "^7.11.0" lodash "^4.17.19" +"@babel/helper-module-transforms@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c" + integrity sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w== + dependencies: + "@babel/helper-module-imports" "^7.12.1" + "@babel/helper-replace-supers" "^7.12.1" + "@babel/helper-simple-access" "^7.12.1" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/helper-validator-identifier" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + lodash "^4.17.19" + "@babel/helper-optimise-call-expression@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" @@ -322,6 +393,16 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helper-replace-supers@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.1.tgz#f15c9cc897439281891e11d5ce12562ac0cf3fa9" + integrity sha512-zJjTvtNJnCFsCXVi5rUInstLd/EIVNmIKA1Q9ynESmMBWPWd+7sdR+G4/wdu+Mppfep0XLyG2m7EBPvjCeFyrw== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.12.1" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + "@babel/helper-simple-access@^7.10.1", "@babel/helper-simple-access@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" @@ -330,6 +411,13 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helper-simple-access@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136" + integrity sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA== + dependencies: + "@babel/types" "^7.12.1" + "@babel/helper-skip-transparent-expression-wrappers@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" @@ -368,6 +456,15 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helpers@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.1.tgz#8a8261c1d438ec18cb890434df4ec768734c1e79" + integrity sha512-9JoDSBGoWtmbay98efmT2+mySkwjzeFeAL9BuWNoVQpkPFQF8SIIFUfY5os9u8wVzglzoiPRSW7cuJmBDUt43g== + dependencies: + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + "@babel/highlight@^7.0.0", "@babel/highlight@^7.10.4", "@babel/highlight@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" @@ -382,6 +479,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== +"@babel/parser@^7.12.1", "@babel/parser@^7.12.3": + version "7.12.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.3.tgz#a305415ebe7a6c7023b40b5122a0662d928334cd" + integrity sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw== + "@babel/plugin-proposal-async-generator-functions@^7.10.4", "@babel/plugin-proposal-async-generator-functions@^7.2.0", "@babel/plugin-proposal-async-generator-functions@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.4.tgz#4b65abb3d9bacc6c657aaa413e56696f9f170fc6" @@ -674,6 +776,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-typescript@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.1.tgz#460ba9d77077653803c3dd2e673f76d66b4029e5" + integrity sha512-UZNEcCY+4Dp9yYRCAHrHDU+9ZXLYaY9MgBXSRLkB9WjYFRR6quJBumfVrEkUxrePPBwFcpWfNKXqVRQQtm7mMA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-arrow-functions@^7.10.4", "@babel/plugin-transform-arrow-functions@^7.2.0", "@babel/plugin-transform-arrow-functions@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd" @@ -1040,6 +1149,15 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-typescript" "^7.10.4" +"@babel/plugin-transform-typescript@^7.2.0": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.1.tgz#d92cc0af504d510e26a754a7dbc2e5c8cd9c7ab4" + integrity sha512-VrsBByqAIntM+EYMqSm59SiMEf7qkmI9dqMt6RbD/wlwueWmYcI0FFK5Fj47pP6DRZm+3teXjosKlwcZJ5lIMw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-typescript" "^7.12.1" + "@babel/plugin-transform-unicode-escapes@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz#feae523391c7651ddac115dae0a9d06857892007" @@ -1473,6 +1591,21 @@ globals "^11.1.0" lodash "^4.17.19" +"@babel/traverse@^7.12.1": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.1.tgz#941395e0c5cc86d5d3e75caa095d3924526f0c1e" + integrity sha512-MA3WPoRt1ZHo2ZmoGKNqi20YnPt0B1S0GTZEPhhd+hw2KGUzBlHuVunj6K4sNuK+reEvyiPwtp0cpaqLzJDmAw== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.1" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/parser" "^7.12.1" + "@babel/types" "^7.12.1" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + "@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.6.0", "@babel/types@^7.6.1", "@babel/types@^7.7.0", "@babel/types@^7.9.0", "@babel/types@^7.9.5": version "7.11.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" @@ -1482,6 +1615,15 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.12.1", "@babel/types@^7.5.0": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.1.tgz#e109d9ab99a8de735be287ee3d6a9947a190c4ae" + integrity sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@bahmutov/all-paths@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@bahmutov/all-paths/-/all-paths-1.0.2.tgz#9ae0dcdf9022dd6e5e14d7fda3479e6a330d035b" @@ -4816,6 +4958,17 @@ "@types/babel__template" "*" "@types/babel__traverse" "*" +"@types/babel__core@^7.1.2": + version "7.1.11" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.11.tgz#7fae4660a009a4031e293f25b213f142d823b3c4" + integrity sha512-E5nSOzrjnvhURYnbOR2dClTqcyhPbPvtEwLHf7JJADKedPbcZsoJVfP+I2vBNfBjz4bnZIuhL/tNmRi5nJ7Jlw== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + "@types/babel__generator@*": version "7.6.1" resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.1.tgz#4901767b397e8711aeb99df8d396d7ba7b7f0e04" From 3e13c11328a099e25eac1e4db03892cd9af5bd00 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Fri, 6 Nov 2020 15:04:24 +0200 Subject: [PATCH 2/9] Add more tests for edge-cases --- .../init-component-testing.test.ts.js | 112 +++++++++++ npm/create-cypress-tests/package.json | 5 +- npm/create-cypress-tests/scripts/example.js | 19 ++ .../babel/babelTransform.test.ts | 85 +++++++++ .../component-testing/babel/babelTransform.ts | 109 ++++++----- .../init-component-testing.test.ts | 174 +++++++++++++----- .../init-component-testing.ts | 144 ++++++++------- ...lAdapter.ts => installFrameworkAdapter.ts} | 2 +- .../templates/guessTemplate.ts | 3 - .../templates/react/babel.test.ts | 6 +- .../component-testing/templates/react/next.ts | 17 +- .../templates/react/react-scripts.ts | 11 ++ npm/create-cypress-tests/src/main.test.ts | 6 +- npm/create-cypress-tests/src/main.ts | 2 +- npm/create-cypress-tests/tsconfig.json | 3 +- packages/example/cypress/tsconfig.json | 0 yarn.lock | 2 +- 17 files changed, 525 insertions(+), 175 deletions(-) create mode 100644 npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js create mode 100644 npm/create-cypress-tests/scripts/example.js create mode 100644 npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts rename npm/create-cypress-tests/src/component-testing/{installAdapter.ts => installFrameworkAdapter.ts} (95%) create mode 100644 packages/example/cypress/tsconfig.json diff --git a/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js b/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js new file mode 100644 index 000000000000..7afa8a834b6a --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js @@ -0,0 +1,112 @@ +exports['injected plugins/index.js'] = ` +const preprocessor = require("@cypress/react/plugins/react-scripts"); + +module.exports = (on, config) => { + preprocessor(on, config); // IMPORTANT to return the config object + + return config; +}; + +` + +exports['injected support/index.js'] = ` +import "./commands.js"; +import "@cypress/react/support"; + +` + +exports['next.js template injected plugins/index.js'] = ` +const preprocessor = require("@cypress/react/plugins/next"); + +module.exports = (on, config) => { + preprocessor(on, config); + return config; +}; + +` + +exports['next.js template injected support/index.js'] = ` +import "@cypress/react/support"; + +` + +exports['create-react-app template injected plugins/index.js'] = ` +const preprocessor = require("@cypress/react/plugins/react-scripts"); + +module.exports = (on, config) => { + preprocessor(on, config); // IMPORTANT to return the config object + + return config; +}; + +` + +exports['create-react-app template injected support/index.js'] = ` +import "./commands.js"; +import "@cypress/react/support"; + +` + +exports['create-react-app template injected cypress.json'] = ` +const preprocessor = require("@cypress/react/plugins/react-scripts"); + +module.exports = (on, config) => { + preprocessor(on, config); // IMPORTANT to return the config object + + return config; +}; + +` + +exports['Injects guessed next.js template cypress.json'] = ` +const preprocessor = require("@cypress/react/plugins/next"); + +module.exports = (on, config) => { + preprocessor(on, config); + return config; +}; + +` + +exports['Injects guessed next.js template plugins/index.js'] = ` +const preprocessor = require("@cypress/react/plugins/next"); + +module.exports = (on, config) => { + preprocessor(on, config); + return config; +}; + +` + +exports['Injects guessed next.js template support/index.js'] = ` +import "@cypress/react/support"; + +` + +exports['Injected overridden webpack template cypress.json'] = ` +const preprocessor = require("@cypress/react/plugins/react-scripts"); + +module.exports = (on, config) => { + preprocessor(on, config); // IMPORTANT to return the config object + + return config; +}; + +` + +exports['Injected overridden webpack template plugins/index.js'] = ` +const preprocessor = require("@cypress/react/plugins/react-scripts"); + +module.exports = (on, config) => { + preprocessor(on, config); // IMPORTANT to return the config object + + return config; +}; + +` + +exports['Injected overridden webpack template support/index.js'] = ` +import "./commands.js"; +import "@cypress/react/support"; + +` diff --git a/npm/create-cypress-tests/package.json b/npm/create-cypress-tests/package.json index 40bb00ebb2e3..39a496fc5d24 100644 --- a/npm/create-cypress-tests/package.json +++ b/npm/create-cypress-tests/package.json @@ -5,14 +5,15 @@ "private": false, "main": "index.js", "scripts": { - "build": "yarn prepare-example && tsc -p ./tsconfig.json && chmod +x dist/src/index.js", - "prepare-example": "rimraf initial-template/* && cp -r ../../packages/example/* initial-template", + "build": "yarn prepare-example && tsc -p ./tsconfig.json && chmod +x dist/src/index.js && node scripts/example copy-to ./dist/initial-template", + "prepare-example": "node scripts/example copy-to ./initial-template", "test": "cross-env TS_NODE_PROJECT=./tsconfig.test.json mocha --config .mocharc.json './src/**/*.test.ts'", "test:watch": "yarn test -w" }, "dependencies": { "@babel/core": "^7.5.4", "@babel/plugin-transform-typescript": "^7.2.0", + "@babel/template": "^7.5.4", "@babel/types": "^7.5.0", "chalk": "4.1.0", "cli-highlight": "2.1.4", diff --git a/npm/create-cypress-tests/scripts/example.js b/npm/create-cypress-tests/scripts/example.js new file mode 100644 index 000000000000..23fd021fea45 --- /dev/null +++ b/npm/create-cypress-tests/scripts/example.js @@ -0,0 +1,19 @@ +const fs = require('fs-extra') +const chalk = require('chalk') +const path = require('path') +const program = require('commander') + +program +.command('copy-to [destination]') +.description('copy cypress/packages/example into destination') +.action(async (destination) => { + const exampleFolder = path.resolve(__dirname, '..', '..', '..', 'packages', 'example') + const destinationPath = path.resolve(process.cwd(), destination) + + await fs.remove(destinationPath) + await fs.copy(exampleFolder, destinationPath, { recursive: true }) + + console.log(`✅ Example was successfully created at ${chalk.cyan(destination)}`) +}) + +program.parse(process.argv) diff --git a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts new file mode 100644 index 000000000000..beff2da9036b --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts @@ -0,0 +1,85 @@ +import * as babel from '@babel/core' +import { expect } from 'chai' +import { createSupportBabelPlugin, createTransformPluginsFileBabelPlugin, PluginsAst } from './babelTransform' + +describe('babel transform', () => { + context('support babel template', () => { + it('injects import after the last import in the file', () => { + const plugin = createSupportBabelPlugin('import "@cypress/react"') + + const output = babel.transformSync([ + 'import "./commands.js"', + ].join('\n'), { + plugins: [plugin], + })?.code + + expect(output).to.equal([ + 'import "./commands.js";', + 'import "@cypress/react";', + ].join('\n')) + }) + + it('injects import after the last import if a lot of imports and code inside', () => { + const plugin = createSupportBabelPlugin('import "@cypress/react"') + + const output = babel.transformSync([ + 'import "./commands.js";', + 'import "./commands4.js";', + 'import "./commands3.js";', + 'import "./commands2.js";', + '', + 'function hello() {', + ' console.log("world");', + '}', + ].join('\n'), { + plugins: [plugin], + })?.code + + expect(output).to.equal([ + 'import "./commands.js";', + 'import "./commands4.js";', + 'import "./commands3.js";', + 'import "./commands2.js";', + 'import "@cypress/react";', + '', + 'function hello() {', + ' console.log("world");', + '}', + ].join('\n')) + }) + + it('adds import as 1st line if no imports or require found', () => { + const plugin = createSupportBabelPlugin('import "@cypress/react"') + + const output = babel.transformSync('', { plugins: [plugin] })?.code + + expect(output).to.equal('import "@cypress/react";') + }) + }) + + context('Plugins config babel plugin', () => { + it('injects code into the plugins file based on ast', () => { + const plugin = createTransformPluginsFileBabelPlugin({ + Require: babel.template('require("something")'), + ModuleExportsBody: babel.template('yey()'), + }) + + const output = babel.transformSync([ + 'module.exports = (on, config) => {', + 'on("do")', + '}', + ].join('\n'), { + plugins: [plugin], + })?.code + + expect(output).to.equal([ + 'require("something");', + '', + 'module.exports = (on, config) => {', + ' on("do");', + ' yey();', + '};', + ].join(`\n`)) + }) + }) +}) diff --git a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts index cec00b954e92..e83fb0d7ba6e 100644 --- a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts +++ b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts @@ -1,9 +1,48 @@ -import path from 'path' import * as fs from 'fs-extra' import * as babel from '@babel/core' import * as babelTypes from '@babel/types' -type PluginsAst = Record<'Require' | 'ModuleExportsBody', ReturnType> +export type PluginsAst = Record<'Require' | 'ModuleExportsBody', ReturnType> + +function tryRequirePrettier () { + try { + return require('prettier') + } catch (e) { + return null + } +} + +async function transformFileViaPlugin (filePath: string, babelPlugin: babel.PluginObj) { + try { + const initialCode = await fs.readFile(filePath, { encoding: 'utf-8' }) + + const updatedResult = await babel.transformAsync(initialCode, { + plugins: [babelPlugin], + }) + + if (!updatedResult) { + return false + } + + let finalCode = updatedResult.code + + if (finalCode === initialCode) { + return false + } + + const maybePrettier = tryRequirePrettier() + + if (maybePrettier && maybePrettier.format) { + finalCode = maybePrettier.format(finalCode, { parser: 'babel' }) + } + + await fs.writeFile(filePath, finalCode) + + return true + } catch (e) { + return false + } +} export function createTransformPluginsFileBabelPlugin (ast: PluginsAst): babel.PluginObj { return { @@ -30,6 +69,7 @@ export function createTransformPluginsFileBabelPlugin (ast: PluginsAst): babel.P if (paramsLength === 0) { path.parent.right.params.push(babelTypes.identifier('on')) + path.parent.right.params.push(babelTypes.identifier('config')) } if (paramsLength === 1) { @@ -43,42 +83,7 @@ export function createTransformPluginsFileBabelPlugin (ast: PluginsAst): babel.P } } -function tryRequirePrettier () { - try { - return require('prettier') - } catch (e) { - return null - } -} - -async function transformFileViaPlugin (filePath: string, babelPlugin: babel.PluginObj) { - try { - const pluginsFile = await fs.readFile(filePath, { encoding: 'utf-8' }) - - const updatedResult = await babel.transformAsync(pluginsFile, { - plugins: [babelPlugin], - }) - - if (!updatedResult) { - return false - } - - let finalCode = updatedResult.code - const maybePrettier = tryRequirePrettier() - - if (maybePrettier && maybePrettier.format) { - finalCode = maybePrettier.format(finalCode, { parser: 'babel' }) - } - - await fs.writeFile(filePath, finalCode) - - return true - } catch (e) { - return false - } -} - -export async function autoInjectPluginsCode (pluginsFilePath: string, ast: PluginsAst) { +export async function injectPluginsCode (pluginsFilePath: string, ast: PluginsAst) { return transformFileViaPlugin(pluginsFilePath, createTransformPluginsFileBabelPlugin(ast)) } @@ -100,15 +105,33 @@ export async function getPluginsSourceExample (ast: PluginsAst) { return babelResult.code } -export async function injectImportSupportCode (supportFilePath: string, importCode: string) { +export function createSupportBabelPlugin (importCode: string): babel.PluginObj { const template = babel.template(importCode) - const plugin: babel.PluginObj = { + + const plugin: babel.PluginObj<{ + root: babel.NodePath + lastImport: babel.NodePath |null + }> = { visitor: { - Program: (path) => { - path.unshiftContainer('body', template()) + Program (path) { + this.root = path + }, + ImportDeclaration (path) { + this.lastImport = path }, }, + post () { + if (this.lastImport) { + this.lastImport.insertAfter(template()) + } else if (this.root) { + this.root.unshiftContainer('body', template()) + } + }, } - return transformFileViaPlugin(supportFilePath, plugin) + return plugin +} + +export async function injectImportSupportCode (supportFilePath: string, importCode: string) { + return transformFileViaPlugin(supportFilePath, createSupportBabelPlugin(importCode)) } diff --git a/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts b/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts index ccd0a86a547e..83fd89aa498b 100644 --- a/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts +++ b/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts @@ -1,8 +1,10 @@ +import path from 'path' +import fs from 'fs-extra' +import snapshot from 'snap-shot-it' import { expect, use } from 'chai' import sinon, { SinonStub, SinonSpy } from 'sinon' import chalk from 'chalk' import mockFs from 'mock-fs' -import highlight from 'cli-highlight' import { initComponentTesting } from './init-component-testing' import inquirer from 'inquirer' import sinonChai from 'sinon-chai' @@ -17,13 +19,19 @@ describe('init component tests script', () => { let processExitStub: SinonStub | null = null let execStub: SinonStub | null = null - beforeEach(() => { + const e2eTestOutputPath = path.resolve(__dirname, '..', 'test-output', 'init-component-testing') + const cypressConfigPath = path.join(e2eTestOutputPath, 'cypress.json') + + beforeEach(async () => { logSpy = sinon.spy(global.console, 'log') // @ts-ignores execStub = sinon.stub(childProcess, 'exec').callsFake((command, callback) => callback()) processExitStub = sinon.stub(process, 'exit').callsFake(() => { throw new Error(`${chalk.red('process.exit')} should not be called`) }) + + await fs.removeSync(e2eTestOutputPath) + await fs.mkdirSync(e2eTestOutputPath) }) afterEach(() => { @@ -34,36 +42,46 @@ describe('init component tests script', () => { execStub?.restore() }) - it('automatically suggests to the user which config to use', async () => { - mockFs({ - '/cypress.json': '{}', - '/package.json': JSON.stringify({ - dependencies: { - react: '^16.10.0', - }, - }), - '/webpack.config.js': 'module.exports = { }', - }, { createCwd: true }) + function createTempFiles (tempFiles: Record) { + Object.entries(tempFiles).forEach(([fileName, content]) => { + fs.outputFileSync( + path.join(e2eTestOutputPath, fileName), + content, + ) + }) + } - promptSpy = sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ - chosenTemplateName: 'create-react-app', - componentFolder: 'cypress/component', - }) as any) + function snapshotGeneratedFiles (name: string) { + snapshot( + `${name} cypress.json`, + fs.readFileSync( + path.join(e2eTestOutputPath, 'cypress', 'plugins', 'index.js'), + { encoding: 'utf-8' }, + ), + ) - await initComponentTesting({ config: {}, cypressConfigPath: '/', useYarn: true }) - const [{ choices, message }] = (inquirer.prompt as any).args[0][0] + snapshot( + `${name} plugins/index.js`, + fs.readFileSync( + path.join(e2eTestOutputPath, 'cypress', 'plugins', 'index.js'), + { encoding: 'utf-8' }, + ), + ) - expect(choices[0]).to.equal('webpack') - expect(message).to.contain( - `Press ${chalk.inverse(' Enter ')} to continue with ${chalk.green( - 'webpack', - )} configuration`, + snapshot( + `${name} support/index.js`, + fs.readFileSync( + path.join(e2eTestOutputPath, 'cypress', 'support', 'index.js'), + { encoding: 'utf-8' }, + ), ) - }) + } it('determines more presumable configuration to suggest', async () => { - mockFs({ + createTempFiles({ '/cypress.json': '{}', + '/cypress/support/index.js': '', + '/cypress/plugins/index.js': 'module.exports = (on, config) => {}', // For next.js user will have babel config, but we want to suggest to use the closest config for the application code '/babel.config.js': 'module.exports = { }', '/package.json': JSON.stringify({ dependencies: { react: '^17.x', next: '^9.2.0' } }), @@ -74,20 +92,47 @@ describe('init component tests script', () => { componentFolder: 'src', }) as any) - await initComponentTesting({ config: {}, cypressConfigPath: '/', useYarn: true }) + await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - const [{ choices, message }] = (inquirer.prompt as any).args[0][0] + const [{ choices }] = (inquirer.prompt as any).args[0][0] expect(choices[0]).to.equal('next.js') + snapshotGeneratedFiles('Injects guessed next.js template') + }) + + it('automatically suggests to the user which config to use', async () => { + createTempFiles({ + '/cypress.json': '{}', + '/cypress/support/index.js': 'import "./commands.js";', + '/cypress/plugins/index.js': 'module.exports = () => {}', + '/package.json': JSON.stringify({ + dependencies: { + react: '^16.10.0', + }, + }), + '/webpack.config.js': 'module.exports = { }', + }) + + promptSpy = sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ + chosenTemplateName: 'create-react-app', + componentFolder: 'cypress/component', + }) as any) + + await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) + const [{ choices, message }] = (inquirer.prompt as any).args[0][0] + + expect(choices[0]).to.equal('webpack') expect(message).to.contain( `Press ${chalk.inverse(' Enter ')} to continue with ${chalk.green( - 'next.js', + 'webpack', )} configuration`, ) + + snapshotGeneratedFiles('Injected overridden webpack template') }) - it('Asks for framework if can not determine the right one', async () => { - mockFs({ + it('Asks for preferred bundling tool if can not determine the right one', async () => { + createTempFiles({ '/cypress.json': '{}', '/webpack.config.js': 'module.exports = { }', '/package.json': JSON.stringify({ dependencies: { } }), @@ -104,7 +149,7 @@ describe('init component tests script', () => { componentFolder: 'src', }) as any) - await initComponentTesting({ config: {}, cypressConfigPath: '/', useYarn: true }) + await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) expect( someOfSpyCallsIncludes(global.console.log, 'We were unable to automatically determine your framework 😿'), @@ -112,7 +157,7 @@ describe('init component tests script', () => { }) it('Asks for framework if more than 1 option was auto detected', async () => { - mockFs({ + createTempFiles({ '/cypress.json': '{}', '/webpack.config.js': 'module.exports = { }', '/package.json': JSON.stringify({ dependencies: { react: '*', vue: '*' } }), @@ -129,15 +174,19 @@ describe('init component tests script', () => { componentFolder: 'src', }) as any) - await initComponentTesting({ config: {}, cypressConfigPath: '/', useYarn: true }) + await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) expect( someOfSpyCallsIncludes(global.console.log, `It looks like all these frameworks: ${chalk.yellow('react, vue')} are available from this directory.`), ).to.be.true }) + it('installs the right adapter', () => { + + }) + it('suggest the right instruction based on user template choice', async () => { - mockFs({ + createTempFiles({ '/package.json': JSON.stringify({ dependencies: { react: '^16.0.0', @@ -151,14 +200,17 @@ describe('init component tests script', () => { componentFolder: 'src', }) as any) - await initComponentTesting({ config: {}, cypressConfigPath: '/', useYarn: true }) + await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) expect( - someOfSpyCallsIncludes(global.console.log, 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/react-scripts'), + someOfSpyCallsIncludes( + global.console.log, + 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/react-scripts', + ), ).to.be.true }) it('suggests right docs example and cypress.json config based on the `componentFolder` answer', async () => { - mockFs({ + createTempFiles({ '/cypress.json': '{}', '/package.json': JSON.stringify({ dependencies: { @@ -172,21 +224,43 @@ describe('init component tests script', () => { componentFolder: 'cypress/component', }) as any) - await initComponentTesting({ config: {}, cypressConfigPath: '/', useYarn: true }) + await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - const expectedCode = highlight( - JSON.stringify( - { - experimentalComponentTesting: true, - componentFolder: 'cypress/component', - testFiles: '**/*.spec.{js,ts,jsx,tsx}', + const injectedCode = fs.readFileSync(path.join(e2eTestOutputPath, 'cypress.json'), { encoding: 'utf-8' }) + + expect(injectedCode).to.equal(JSON.stringify( + { + experimentalComponentTesting: true, + componentFolder: 'cypress/component', + testFiles: '**/*.spec.{js,ts,jsx,tsx}', + }, + null, + 2, + )) + }) + + it('Shows help message if cypress files are not created', async () => { + createTempFiles({ + '/cypress.json': '{}', + '/package.json': JSON.stringify({ + dependencies: { + react: '^16.0.0', }, - null, - 2, - ), - { language: 'json' }, - ) + }), + }) - expect(global.console.log).to.be.calledWith(`\n${expectedCode}\n`) + sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ + chosenTemplateName: 'create-react-app', + componentFolder: 'cypress/component', + }) as any) + + await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) + + expect( + someOfSpyCallsIncludes( + global.console.log, + 'was not updated automatically. Please add the following config manually:', + ), + ).to.be.true }) }) diff --git a/npm/create-cypress-tests/src/component-testing/init-component-testing.ts b/npm/create-cypress-tests/src/component-testing/init-component-testing.ts index 2423a1d117ab..97f9f703f983 100644 --- a/npm/create-cypress-tests/src/component-testing/init-component-testing.ts +++ b/npm/create-cypress-tests/src/component-testing/init-component-testing.ts @@ -1,64 +1,93 @@ -import fs from 'fs' +import fs from 'fs-extra' import path from 'path' import chalk from 'chalk' import inqueier from 'inquirer' import highlight from 'cli-highlight' import { Template } from './templates/Template' import { guessTemplate } from './templates/guessTemplate' -import { installAdapter } from './installAdapter' -import { autoInjectPluginsCode, getPluginsSourceExample } from './babel/babelTransform' +import { installFrameworkAdapter } from './installFrameworkAdapter' +import { injectImportSupportCode, injectPluginsCode, getPluginsSourceExample } from './babel/babelTransform' + +async function injectOrShowConfigCode (injectFn: () => Promise, { + code, + filePath, + fallbackFileMessage, + language, +}: { + code: string + filePath: string + language: string + fallbackFileMessage: string +}) { + const fileExists = fs.existsSync(filePath) + const readableFilePath = fileExists ? path.relative(process.cwd(), filePath) : fallbackFileMessage + + const printCode = () => { + console.log() + console.log(highlight(code, { language })) + console.log() + } + + const printSuccess = () => { + console.log(`✅ ${chalk.bold.green(readableFilePath)} was updated with the following config:`) + printCode() + } + + const printFailure = () => { + console.log(`❌ ${chalk.bold.red(readableFilePath)} was not updated automatically. Please add the following config manually: `) + printCode() + } + + if (!fileExists) { + printFailure() + + return + } -function printCypressJsonHelp ( + // something get completely wrong when using babel or something. Print error message. + const injected = await injectFn().catch(() => false) + + injected ? printSuccess() : printFailure() +} + +async function injectAndShowCypressJsonConfig ( cypressJsonPath: string, componentFolder: string, ) { - const resultObject = { + const configToInject = { experimentalComponentTesting: true, componentFolder, testFiles: '**/*.spec.{js,ts,jsx,tsx}', } - const relativeCypressJsonPath = path.relative(process.cwd(), cypressJsonPath) - const highlightedCode = highlight(JSON.stringify(resultObject, null, 2), { - language: 'json', - }) + async function autoInjectCypressJson () { + const currentConfig = JSON.parse(await fs.readFile(cypressJsonPath, { encoding: 'utf-8' })) - console.log( - `\n${chalk.bold('1.')} Add this to the ${chalk.green( - relativeCypressJsonPath, - )}:`, - ) + await fs.writeFile(cypressJsonPath, JSON.stringify({ + ...currentConfig, + ...configToInject, + }, null, 2)) + + return true + } - console.log(`\n${highlightedCode}\n`) + await injectOrShowConfigCode(autoInjectCypressJson, { + code: JSON.stringify(configToInject, null, 2), + language: 'js', + filePath: cypressJsonPath, + fallbackFileMessage: 'cypress.json config file', + }) } -function injectAndShowSupportConfig (supportFilePath: string, framework: string) { - const stepNumber = chalk.bold('2.') +async function injectAndShowSupportConfig (supportFilePath: string, framework: string) { const importCode = `import \'@cypress/${framework}/support\'` - const requireCode = `require(\'@cypress/${framework}/support\')` - - if (fs.existsSync(supportFilePath)) { - const fileContent = fs.readFileSync(supportFilePath, { encoding: 'utf-8' }) - const relativeSupportPath = path.relative(process.cwd(), supportFilePath) - const importCodeWithPreferredStyle = fileContent.includes('import ') - ? importCode - : requireCode - - console.log( - `\n${stepNumber} This to the ${chalk.green(relativeSupportPath)}:`, - ) - - console.log( - `\n${highlight(importCodeWithPreferredStyle, { language: 'js' })}\n`, - ) - } else { - console.log( - `\n${stepNumber} This to the support file https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Support-file`, - ) - - console.log(`\n${highlight(requireCode, { language: 'js' })}\n`) - } + await injectOrShowConfigCode(() => injectImportSupportCode(supportFilePath, importCode), { + code: importCode, + language: 'js', + filePath: supportFilePath, + fallbackFileMessage: 'support file (https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Support-file)', + }) } async function injectAndShowPluginConfig (template: Template, pluginsFilePath: string, emptyProject: boolean) { @@ -68,24 +97,12 @@ async function injectAndShowPluginConfig (template: Template, pluginsFileP return } - const injected = await autoInjectPluginsCode(pluginsFilePath, ast) - - if (injected && emptyProject) { - return - } - - const pluginsCode = await getPluginsSourceExample(ast) - const highlightedPluginCode = highlight(pluginsCode, { language: 'js' }) - const relativePluginsFilePath = fs.existsSync(pluginsFilePath) - ? path.relative(process.cwd(), pluginsFilePath) - : 'plugins file (https://docs.cypress.io/guides/tooling/plugins-guide.html)`' - - const stepTitle = injected - ? `✅ Injected into ${chalk.green(relativePluginsFilePath)}` - : `❌ ${chalk.red(`We were not able to modify your ${relativePluginsFilePath}`)}. Add this manually:` - - console.log(stepTitle) - console.log(`\n${highlightedPluginCode}\n`) + await injectOrShowConfigCode(() => injectPluginsCode(pluginsFilePath, ast), { + code: await getPluginsSourceExample(ast), + language: 'js', + filePath: pluginsFilePath, + fallbackFileMessage: 'plugins file (https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Plugin-files)', + }) } type InitComponentTestingOptions = { @@ -97,7 +114,7 @@ type InitComponentTestingOptions = { export async function initComponentTesting ({ config, useYarn, cypressConfigPath }: InitComponentTestingOptions) { const cypressProjectRoot = path.resolve(cypressConfigPath, '..') - const framework = await installAdapter(cypressProjectRoot, { useYarn }) + const framework = await installFrameworkAdapter(cypressProjectRoot, { useYarn }) const { possibleTemplates, defaultTemplate, @@ -156,10 +173,10 @@ export async function initComponentTesting ({ config, useYarn, cypressConfigP console.log() console.log(`Here are instructions of how to get started with component testing for ${chalk.cyan(chosenTemplateName)}:`) + console.log() - printCypressJsonHelp(cypressConfigPath, componentFolder) - injectAndShowSupportConfig(supportFilePath, framework) - + await injectAndShowCypressJsonConfig(cypressConfigPath, componentFolder) + await injectAndShowSupportConfig(supportFilePath, framework) await injectAndShowPluginConfig(chosenTemplate, pluginsFilePath, false) if (chosenTemplate.printHelper) { @@ -181,4 +198,7 @@ export async function initComponentTesting ({ config, useYarn, cypressConfigP )}`, ) } + + // render delimiter + console.log(new Array(process.stdout.columns).fill('═').join('')) } diff --git a/npm/create-cypress-tests/src/component-testing/installAdapter.ts b/npm/create-cypress-tests/src/component-testing/installFrameworkAdapter.ts similarity index 95% rename from npm/create-cypress-tests/src/component-testing/installAdapter.ts rename to npm/create-cypress-tests/src/component-testing/installFrameworkAdapter.ts index 89f06760a6b8..c08f0074bc50 100644 --- a/npm/create-cypress-tests/src/component-testing/installAdapter.ts +++ b/npm/create-cypress-tests/src/component-testing/installFrameworkAdapter.ts @@ -47,7 +47,7 @@ type InstallAdapterOptions = { useYarn: boolean } -export async function installAdapter (cwd: string, options: InstallAdapterOptions) { +export async function installFrameworkAdapter (cwd: string, options: InstallAdapterOptions) { const framework = await guessOrAskForFramework(cwd) await installDependency(`@cypress/${framework}`, options) diff --git a/npm/create-cypress-tests/src/component-testing/templates/guessTemplate.ts b/npm/create-cypress-tests/src/component-testing/templates/guessTemplate.ts index 56f77693fd64..dffffe4e0e7f 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/guessTemplate.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/guessTemplate.ts @@ -1,7 +1,4 @@ -import chalk from 'chalk' -import inquirer from 'inquirer' import { Template } from './Template' -import { scanFSForAvailableDependency } from '../../findPackageJson' import { reactTemplates } from './react' import { vueTemplates } from './vue' diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts index 9651e49f072c..c5bb89562120 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts @@ -2,9 +2,9 @@ import { expect } from 'chai' import mockFs from 'mock-fs' import * as babel from '@babel/core' import { BabelTemplate } from './babel' -import { createTransformPluginsFileBabelPlugin } from '../../bablTransform' +import { createTransformPluginsFileBabelPlugin } from '../../babel/babelTransform' -describe.only('babel installation template', () => { +describe('babel installation template', () => { beforeEach(mockFs.restore) it('resolves babel.config.json', () => { @@ -70,7 +70,7 @@ describe.only('babel installation template', () => { expect(success).to.equal(true) }) - it.only('automatically injects config to the code', () => { + it('automatically injects config to the code', () => { const code = [ 'const something = require("something")', 'module.exports = (on) => {', diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/next.ts b/npm/create-cypress-tests/src/component-testing/templates/react/next.ts index 0e4f0c037a77..324eb7cedde8 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/next.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/next.ts @@ -1,3 +1,4 @@ +import * as babel from '@babel/core' import { createFindPackageJsonIterator } from '../../../findPackageJson' import { Template } from '../Template' import { validateSemverVersion } from '../../../utils' @@ -19,10 +20,20 @@ export const NextTemplate: Template = { '}', ].join('\n') }, - test: () => { - const packageJsonIterator = createFindPackageJsonIterator(process.cwd()) + getPluginsCodeAst: () => { + return { + Require: babel.template('const preprocessor = require(\'@cypress/react/plugins/next\')'), + ModuleExportsBody: babel.template([ + ' preprocessor(on, config)', + ' // IMPORTANT to return the config object', + ' return config', + ].join('\n')), + } + }, + test: (cwd) => { + const packageJsonIterator = createFindPackageJsonIterator(cwd) - return packageJsonIterator.map(({ dependencies, devDependencies }) => { + return packageJsonIterator.map(({ dependencies, devDependencies }, path) => { if (!dependencies && !devDependencies) { return { success: false } } diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts index 4d93eb9863fa..2ea269f97e9d 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts @@ -3,6 +3,7 @@ import { createFindPackageJsonIterator } from '../../../findPackageJson' import { Template } from '../Template' import { validateSemverVersion } from '../../../utils' import { MIN_SUPPORTED_VERSION } from '../../versions' +import * as babel from '@babel/core' export const ReactScriptsTemplate: Template = { recommendedComponentFolder: 'src', @@ -22,6 +23,16 @@ export const ReactScriptsTemplate: Template = { '}', ].join('\n') }, + getPluginsCodeAst: () => { + return { + Require: babel.template('const preprocessor = require(\'@cypress/react/plugins/react-scripts\')'), + ModuleExportsBody: babel.template([ + ' preprocessor(on, config)', + ' // IMPORTANT to return the config object', + ' return config', + ].join('\n'), { preserveComments: true }), + } + }, test: () => { // TODO also determine ejected create react app const packageJsonIterator = createFindPackageJsonIterator(process.cwd()) diff --git a/npm/create-cypress-tests/src/main.test.ts b/npm/create-cypress-tests/src/main.test.ts index e595da4c1ecd..c904a408bc29 100644 --- a/npm/create-cypress-tests/src/main.test.ts +++ b/npm/create-cypress-tests/src/main.test.ts @@ -97,13 +97,11 @@ describe('init script', () => { fsCopyStub?.restore() sinon.stub(process, 'cwd').returns(e2eTestOutputPath) - // @ts-ignore - fsExtra.rmdirSync(e2eTestOutputPath, { recursive: true }) - + await fsExtra.remove(e2eTestOutputPath) await fsExtra.mkdir(e2eTestOutputPath) }) - it('Really copies real plugins and support files', async () => { + it('Copies plugins and support files', async () => { await fsExtra.outputFile( path.join(e2eTestOutputPath, 'package.json'), JSON.stringify({ name: 'test' }, null, 2), diff --git a/npm/create-cypress-tests/src/main.ts b/npm/create-cypress-tests/src/main.ts index 12053f31e8b1..28d5f435ef2b 100644 --- a/npm/create-cypress-tests/src/main.ts +++ b/npm/create-cypress-tests/src/main.ts @@ -69,7 +69,7 @@ export async function main ({ useNpm, ignoreTs, setupComponentTesting, ignoreExa await initComponentTesting({ config, cypressConfigPath, useYarn }) } - console.log(`\n✅ Success! Cypress is installed and ready to run tests.`) + console.log(`\n👍 Success! Cypress is installed and ready to run tests.`) printCypressCommandsHelper({ useYarn }) console.log(`\nHappy testing with ${chalk.green('cypress.io')} 🌲\n`) diff --git a/npm/create-cypress-tests/tsconfig.json b/npm/create-cypress-tests/tsconfig.json index 36480af8fb93..5b995210b087 100644 --- a/npm/create-cypress-tests/tsconfig.json +++ b/npm/create-cypress-tests/tsconfig.json @@ -25,7 +25,6 @@ ], "include": [ "./src/**/*.ts", - "src/component-testing/babel/examples/*", - "src/initial-template/*" + "./initial-template/**/*.{js}" ] } diff --git a/packages/example/cypress/tsconfig.json b/packages/example/cypress/tsconfig.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/yarn.lock b/yarn.lock index 9fcbf39a55c6..0b8b9d2bf35f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1567,7 +1567,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.10.4", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.6.0", "@babel/template@^7.8.6": +"@babel/template@^7.10.4", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.5.4", "@babel/template@^7.6.0", "@babel/template@^7.8.6": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== From e47ba9fb1eccd7e5bf5a94a83f0f6e5972f316a2 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Fri, 6 Nov 2020 16:35:27 +0200 Subject: [PATCH 3/9] Add snapshot tests for autogenerated code for each template --- .eslintignore | 3 +- .../__snapshots__/babel.test.ts.js | 10 +++ .../init-component-testing.test.ts.js | 74 ++----------------- .../__snapshots__/next.test.ts.js | 10 +++ .../__snapshots__/react-scripts.test.ts.js | 10 +++ .../__snapshots__/reactWebpackFile.test.ts.js | 24 ++++++ .../__snapshots__/rollup.test.ts.js | 32 ++++++++ .../__snapshots__/vueWebpackFile.test.ts.js | 24 ++++++ .../__snapshots__/webpackOptions.test.ts.js | 29 ++++++++ .../babel/babelTransform.test.ts | 8 +- .../component-testing/babel/babelTransform.ts | 18 ++--- .../init-component-testing.ts | 22 ++++-- .../component-testing/templates/Template.ts | 7 +- .../templates/react/babel.test.ts | 17 +---- .../templates/react/babel.ts | 26 ++----- .../templates/react/next.test.ts | 3 + .../component-testing/templates/react/next.ts | 21 ++---- .../templates/react/react-scripts.test.ts | 3 + .../templates/react/react-scripts.ts | 19 +---- .../templates/react/reactWebpackFile.test.ts | 20 +++-- .../templates/react/reactWebpackFile.ts | 26 +++---- .../templates/react/rollup.test.ts | 20 +++-- .../templates/react/rollup.ts | 39 +++++----- ...webpack-options-module-exports.template.js | 29 ++++++++ .../templates/react/webpack-options.ts | 48 +++--------- .../templates/react/webpackOptions.test.ts | 6 ++ .../templates/vue/vueWebpackFile.test.ts | 22 +++--- .../templates/vue/vueWebpackFile.ts | 32 ++++---- npm/create-cypress-tests/src/test-utils.ts | 25 +++++++ 29 files changed, 348 insertions(+), 279 deletions(-) create mode 100644 npm/create-cypress-tests/__snapshots__/babel.test.ts.js create mode 100644 npm/create-cypress-tests/__snapshots__/next.test.ts.js create mode 100644 npm/create-cypress-tests/__snapshots__/react-scripts.test.ts.js create mode 100644 npm/create-cypress-tests/__snapshots__/reactWebpackFile.test.ts.js create mode 100644 npm/create-cypress-tests/__snapshots__/rollup.test.ts.js create mode 100644 npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js create mode 100644 npm/create-cypress-tests/__snapshots__/webpackOptions.test.ts.js create mode 100644 npm/create-cypress-tests/src/component-testing/templates/react/webpack-options-module-exports.template.js create mode 100644 npm/create-cypress-tests/src/component-testing/templates/react/webpackOptions.test.ts diff --git a/.eslintignore b/.eslintignore index 866ba2fa5d8f..388684fdd50c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -44,4 +44,5 @@ npm/webpack-preprocessor/examples/use-babelrc/cypress/integration/spec.js /npm/react/bin/* /npm/react/**/coverage **/.next/** -/npm/create-cypress-tests/initial-template \ No newline at end of file +/npm/create-cypress-tests/initial-template +/npm/create-cypress-tests/**/*.template.* \ No newline at end of file diff --git a/npm/create-cypress-tests/__snapshots__/babel.test.ts.js b/npm/create-cypress-tests/__snapshots__/babel.test.ts.js new file mode 100644 index 000000000000..5e5dc1658279 --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/babel.test.ts.js @@ -0,0 +1,10 @@ +exports['babel installation template correctly generates plugins config 1'] = ` +const preprocessor = require('@cypress/react/plugins/babel'); + +const something = require("something"); + +module.exports = (on, config) => { + preprocessor(on, config); + return config; // IMPORTANT to return the config object +}; +` diff --git a/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js b/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js index 7afa8a834b6a..bc623f51e593 100644 --- a/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js +++ b/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js @@ -1,69 +1,9 @@ -exports['injected plugins/index.js'] = ` -const preprocessor = require("@cypress/react/plugins/react-scripts"); - -module.exports = (on, config) => { - preprocessor(on, config); // IMPORTANT to return the config object - - return config; -}; - -` - -exports['injected support/index.js'] = ` -import "./commands.js"; -import "@cypress/react/support"; - -` - -exports['next.js template injected plugins/index.js'] = ` -const preprocessor = require("@cypress/react/plugins/next"); - -module.exports = (on, config) => { - preprocessor(on, config); - return config; -}; - -` - -exports['next.js template injected support/index.js'] = ` -import "@cypress/react/support"; - -` - -exports['create-react-app template injected plugins/index.js'] = ` -const preprocessor = require("@cypress/react/plugins/react-scripts"); - -module.exports = (on, config) => { - preprocessor(on, config); // IMPORTANT to return the config object - - return config; -}; - -` - -exports['create-react-app template injected support/index.js'] = ` -import "./commands.js"; -import "@cypress/react/support"; - -` - -exports['create-react-app template injected cypress.json'] = ` -const preprocessor = require("@cypress/react/plugins/react-scripts"); - -module.exports = (on, config) => { - preprocessor(on, config); // IMPORTANT to return the config object - - return config; -}; - -` - exports['Injects guessed next.js template cypress.json'] = ` const preprocessor = require("@cypress/react/plugins/next"); module.exports = (on, config) => { preprocessor(on, config); - return config; + return config; // IMPORTANT to return the config object }; ` @@ -73,7 +13,7 @@ const preprocessor = require("@cypress/react/plugins/next"); module.exports = (on, config) => { preprocessor(on, config); - return config; + return config; // IMPORTANT to return the config object }; ` @@ -87,9 +27,8 @@ exports['Injected overridden webpack template cypress.json'] = ` const preprocessor = require("@cypress/react/plugins/react-scripts"); module.exports = (on, config) => { - preprocessor(on, config); // IMPORTANT to return the config object - - return config; + preprocessor(on, config); + return config; // IMPORTANT to return the config object }; ` @@ -98,9 +37,8 @@ exports['Injected overridden webpack template plugins/index.js'] = ` const preprocessor = require("@cypress/react/plugins/react-scripts"); module.exports = (on, config) => { - preprocessor(on, config); // IMPORTANT to return the config object - - return config; + preprocessor(on, config); + return config; // IMPORTANT to return the config object }; ` diff --git a/npm/create-cypress-tests/__snapshots__/next.test.ts.js b/npm/create-cypress-tests/__snapshots__/next.test.ts.js new file mode 100644 index 000000000000..5fffc9059960 --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/next.test.ts.js @@ -0,0 +1,10 @@ +exports['next.js install template correctly generates plugins config 1'] = ` +const preprocessor = require('@cypress/react/plugins/next'); + +const something = require("something"); + +module.exports = (on, config) => { + preprocessor(on, config); + return config; // IMPORTANT to return the config object +}; +` diff --git a/npm/create-cypress-tests/__snapshots__/react-scripts.test.ts.js b/npm/create-cypress-tests/__snapshots__/react-scripts.test.ts.js new file mode 100644 index 000000000000..53f5b90ea4ae --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/react-scripts.test.ts.js @@ -0,0 +1,10 @@ +exports['create-react-app install template correctly generates plugins config 1'] = ` +const preprocessor = require('@cypress/react/plugins/react-scripts'); + +const something = require("something"); + +module.exports = (on, config) => { + preprocessor(on, config); + return config; // IMPORTANT to return the config object +}; +` diff --git a/npm/create-cypress-tests/__snapshots__/reactWebpackFile.test.ts.js b/npm/create-cypress-tests/__snapshots__/reactWebpackFile.test.ts.js new file mode 100644 index 000000000000..314d0ac6f470 --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/reactWebpackFile.test.ts.js @@ -0,0 +1,24 @@ +exports['webpack-file install template correctly generates plugins config when webpack config path is missing 1'] = ` +const preprocessor = require("@cypress/react/plugins/load-webpack"); + +const something = require("something"); + +module.exports = (on, config) => { + // TODO replace with valid webpack config path + config.env.webpackFilename = './webpack.config.js'; + preprocessor(on, config); + return config; // IMPORTANT to return the config object +}; +` + +exports['webpack-file install template correctly generates plugins config when webpack config path is provided 1'] = ` +const preprocessor = require("@cypress/react/plugins/load-webpack"); + +const something = require("something"); + +module.exports = (on, config) => { + config.env.webpackFilename = 'config/webpack.config.js'; + preprocessor(on, config); + return config; // IMPORTANT to return the config object +}; +` diff --git a/npm/create-cypress-tests/__snapshots__/rollup.test.ts.js b/npm/create-cypress-tests/__snapshots__/rollup.test.ts.js new file mode 100644 index 000000000000..3e28e760ff9b --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/rollup.test.ts.js @@ -0,0 +1,32 @@ +exports['rollup-file install template correctly generates plugins config when webpack config path is missing 1'] = ` +const rollupPreprocessor = require("@bahmutov/cy-rollup"); + +const something = require("something"); + +module.exports = (on, config) => { + on('file:preprocessor', rollupPreprocessor({ + // TODO replace with valid rollup config path + configFile: 'rollup.config.js' + })); + + require('@cypress/code-coverage/task')(on, config); + + return config; // IMPORTANT to return the config object +}; +` + +exports['rollup-file install template correctly generates plugins config when webpack config path is provided 1'] = ` +const rollupPreprocessor = require("@bahmutov/cy-rollup"); + +const something = require("something"); + +module.exports = (on, config) => { + on('file:preprocessor', rollupPreprocessor({ + configFile: 'config/rollup.config.js' + })); + + require('@cypress/code-coverage/task')(on, config); + + return config; // IMPORTANT to return the config object +}; +` diff --git a/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js b/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js new file mode 100644 index 000000000000..1c7ed57c8395 --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js @@ -0,0 +1,24 @@ +exports['vue webpack-file install template correctly generates plugins config when webpack config path is missing 1'] = ` +const { + onFilePreprocessor +} = require('@cypress/vue/preprocessor/webpack'); + +const something = require("something"); + +module.exports = (on, config) => { + // TODO replace with valid webpack config path + on('file:preprocessor', onFilePreprocessor('./webpack.config.js')); +}; +` + +exports['vue webpack-file install template correctly generates plugins config when webpack config path is provided 1'] = ` +const { + onFilePreprocessor +} = require('@cypress/vue/preprocessor/webpack'); + +const something = require("something"); + +module.exports = (on, config) => { + on('file:preprocessor', onFilePreprocessor('build/webpack.config.js')); +}; +` diff --git a/npm/create-cypress-tests/__snapshots__/webpackOptions.test.ts.js b/npm/create-cypress-tests/__snapshots__/webpackOptions.test.ts.js new file mode 100644 index 000000000000..352d39aff258 --- /dev/null +++ b/npm/create-cypress-tests/__snapshots__/webpackOptions.test.ts.js @@ -0,0 +1,29 @@ +exports['webpack-options template correctly generates plugins config 1'] = ` +const webpackPreprocessor = require("@cypress/webpack-preprocessor"); + +const something = require("something"); + +module.exports = (on, config) => { + const opts = webpackPreprocessor.defaultOptions; + const babelLoader = opts.webpackOptions.module.rules[0].use[0]; // add React preset to be able to transpile JSX + + babelLoader.options.presets.push(require.resolve('@babel/preset-react')); // We can also push Babel istanbul plugin to instrument the code on the fly + // and get code coverage reports from component tests (optional) + + if (!babelLoader.options.plugins) { + babelLoader.options.plugins = []; + } + + babelLoader.options.plugins.push(require.resolve('babel-plugin-istanbul')); // in order to mock named imports, need to include a plugin + + babelLoader.options.plugins.push([require.resolve('@babel/plugin-transform-modules-commonjs'), { + loose: true + }]); // add code coverage plugin + + require('@cypress/code-coverage/task')(on, config); + + on('file:preprocessor', webpackPreprocessor(opts)); // if adding code coverage, important to return updated config + + return config; +}; +` diff --git a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts index beff2da9036b..78c6e5c93c2a 100644 --- a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts +++ b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts @@ -1,8 +1,8 @@ import * as babel from '@babel/core' import { expect } from 'chai' -import { createSupportBabelPlugin, createTransformPluginsFileBabelPlugin, PluginsAst } from './babelTransform' +import { createSupportBabelPlugin, createTransformPluginsFileBabelPlugin } from './babelTransform' -describe('babel transform', () => { +describe('babel transform utils', () => { context('support babel template', () => { it('injects import after the last import in the file', () => { const plugin = createSupportBabelPlugin('import "@cypress/react"') @@ -60,8 +60,8 @@ describe('babel transform', () => { context('Plugins config babel plugin', () => { it('injects code into the plugins file based on ast', () => { const plugin = createTransformPluginsFileBabelPlugin({ - Require: babel.template('require("something")'), - ModuleExportsBody: babel.template('yey()'), + Require: babel.template.ast('require("something")'), + ModuleExportsBody: babel.template.ast('yey()'), }) const output = babel.transformSync([ diff --git a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts index e83fb0d7ba6e..db9d76ee313a 100644 --- a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts +++ b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts @@ -2,7 +2,7 @@ import * as fs from 'fs-extra' import * as babel from '@babel/core' import * as babelTypes from '@babel/types' -export type PluginsAst = Record<'Require' | 'ModuleExportsBody', ReturnType> +export type PluginsConfigAst = Record<'Require' | 'ModuleExportsBody', ReturnType> function tryRequirePrettier () { try { @@ -44,11 +44,11 @@ async function transformFileViaPlugin (filePath: string, babelPlugin: babel.Plug } } -export function createTransformPluginsFileBabelPlugin (ast: PluginsAst): babel.PluginObj { +export function createTransformPluginsFileBabelPlugin (ast: PluginsConfigAst): babel.PluginObj { return { visitor: { Program: (path) => { - path.unshiftContainer('body', ast.Require()) + path.unshiftContainer('body', ast.Require) }, Function: (path) => { if (!babelTypes.isAssignmentExpression(path.parent)) { @@ -76,18 +76,18 @@ export function createTransformPluginsFileBabelPlugin (ast: PluginsAst): babel.P path.parent.right.params.push(babelTypes.identifier('config')) } - path.get('body').pushContainer('body' as never, ast.ModuleExportsBody()) + path.get('body').pushContainer('body' as never, ast.ModuleExportsBody) } }, }, } } -export async function injectPluginsCode (pluginsFilePath: string, ast: PluginsAst) { +export async function injectPluginsCode (pluginsFilePath: string, ast: PluginsConfigAst) { return transformFileViaPlugin(pluginsFilePath, createTransformPluginsFileBabelPlugin(ast)) } -export async function getPluginsSourceExample (ast: PluginsAst) { +export async function getPluginsSourceExample (ast: PluginsConfigAst) { const exampleCode = [ 'module.exports = (on, config) => {', '', @@ -106,7 +106,7 @@ export async function getPluginsSourceExample (ast: PluginsAst) { } export function createSupportBabelPlugin (importCode: string): babel.PluginObj { - const template = babel.template(importCode) + const template = babel.template.ast(importCode) const plugin: babel.PluginObj<{ root: babel.NodePath @@ -122,9 +122,9 @@ export function createSupportBabelPlugin (importCode: string): babel.PluginObj (template: Template, pluginsFilePath: string, emptyProject: boolean) { - const ast = template.getPluginsCodeAst?.() - - if (!ast) { - return - } +async function injectAndShowPluginConfig (template: Template, { + templatePayload, + pluginsFilePath, + cypressProjectRoot, +}: { + templatePayload: T | null + pluginsFilePath: string + cypressProjectRoot: string +}) { + const ast = template.getPluginsCodeAst(templatePayload, { cypressProjectRoot }) await injectOrShowConfigCode(() => injectPluginsCode(pluginsFilePath, ast), { code: await getPluginsSourceExample(ast), @@ -177,7 +181,11 @@ export async function initComponentTesting ({ config, useYarn, cypressConfigP await injectAndShowCypressJsonConfig(cypressConfigPath, componentFolder) await injectAndShowSupportConfig(supportFilePath, framework) - await injectAndShowPluginConfig(chosenTemplate, pluginsFilePath, false) + await injectAndShowPluginConfig(chosenTemplate, { + templatePayload, + pluginsFilePath, + cypressProjectRoot, + }) if (chosenTemplate.printHelper) { chosenTemplate.printHelper() diff --git a/npm/create-cypress-tests/src/component-testing/templates/Template.ts b/npm/create-cypress-tests/src/component-testing/templates/Template.ts index b73d5f6fc210..44933daa1280 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/Template.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/Template.ts @@ -1,15 +1,14 @@ import * as babel from '@babel/core' +import { PluginsConfigAst } from '../babel/babelTransform' export interface Template { message: string getExampleUrl: ({ componentFolder }: { componentFolder: string }) => string recommendedComponentFolder: string test(rootPath: string): { success: boolean, payload?: T } - getPluginsTransformAst?: () => babel.PluginObj - getPluginsCodeAst?: () => Record<'Require' | 'ModuleExportsBody', ReturnType> - getPluginsCode: ( + getPluginsCodeAst: ( payload: T | null, options: { cypressProjectRoot: string }, - ) => string + ) => PluginsConfigAst printHelper?: () => void } diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts index c5bb89562120..1bff7af467af 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts @@ -1,8 +1,7 @@ import { expect } from 'chai' import mockFs from 'mock-fs' -import * as babel from '@babel/core' import { BabelTemplate } from './babel' -import { createTransformPluginsFileBabelPlugin } from '../../babel/babelTransform' +import { snapshotPluginsAstCode } from '../../../test-utils' describe('babel installation template', () => { beforeEach(mockFs.restore) @@ -70,17 +69,5 @@ describe('babel installation template', () => { expect(success).to.equal(true) }) - it('automatically injects config to the code', () => { - const code = [ - 'const something = require("something")', - 'module.exports = (on) => {', - '};', - ].join('\n') - - const output = babel.transformSync(code, { - plugins: [createTransformPluginsFileBabelPlugin(BabelTemplate.getPluginsCodeAst!())], - }) - - console.log('OUTPUT\n', output?.code) - }) + it('correctly generates plugins config', () => snapshotPluginsAstCode(BabelTemplate)) }) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts b/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts index 1a2a52617a19..5232653d17f8 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts @@ -1,7 +1,6 @@ import chalk from 'chalk' import findUp from 'find-up' import * as babel from '@babel/core' - import { Template } from '../Template' import { createFindPackageJsonIterator } from '../../../findPackageJson' @@ -11,28 +10,15 @@ export const BabelTemplate: Template = { )} This is not a replacement for bundling tool. We will use ${chalk.red( 'webpack', )} to bundle the components for testing.`, - getExampleUrl: () => { - return 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/babel' - }, recommendedComponentFolder: 'cypress/component', - getPluginsCode: () => { - return [ - 'const preprocessor = require(\'@cypress/react/plugins/babel\')', - 'module.exports = (on, config) => {', - ' preprocessor(on, config)', - ' // IMPORTANT to return the config object', - ' return config', - '}', - ].join('\n') - }, + getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/babel', getPluginsCodeAst: () => { return { - Require: babel.template('const preprocessor = require(\'@cypress/react/plugins/babel\')'), - ModuleExportsBody: babel.template([ - ' preprocessor(on, config)', - ' // IMPORTANT to return the config object', - ' return config', - ].join('\n')), + Require: babel.template.ast('const preprocessor = require(\'@cypress/react/plugins/babel\')'), + ModuleExportsBody: babel.template.ast([ + 'preprocessor(on, config)', + 'return config // IMPORTANT to return the config object', + ].join('\n'), { preserveComments: true }), } }, test: (cwd) => { diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/next.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/next.test.ts index 18b05a86443d..4701791877e3 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/next.test.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/next.test.ts @@ -3,6 +3,7 @@ import { expect, use } from 'chai' import sinonChai from 'sinon-chai' import mockFs from 'mock-fs' import { NextTemplate } from './next' +import { snapshotPluginsAstCode } from '../../../test-utils' use(sinonChai) @@ -71,4 +72,6 @@ describe('next.js install template', () => { expect(global.console.warn).to.be.called }) + + it('correctly generates plugins config', () => snapshotPluginsAstCode(NextTemplate)) }) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/next.ts b/npm/create-cypress-tests/src/component-testing/templates/react/next.ts index 324eb7cedde8..aee4ce57f2d5 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/next.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/next.ts @@ -10,24 +10,13 @@ export const NextTemplate: Template = { return 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/nextjs' }, recommendedComponentFolder: 'cypress/component', - getPluginsCode: () => { - return [ - 'const preprocessor = require(\'@cypress/react/plugins/next\')', - 'module.exports = (on, config) => {', - ' preprocessor(on, config)', - ' // IMPORTANT to return the config object', - ' return config', - '}', - ].join('\n') - }, getPluginsCodeAst: () => { return { - Require: babel.template('const preprocessor = require(\'@cypress/react/plugins/next\')'), - ModuleExportsBody: babel.template([ - ' preprocessor(on, config)', - ' // IMPORTANT to return the config object', - ' return config', - ].join('\n')), + Require: babel.template.ast('const preprocessor = require(\'@cypress/react/plugins/next\')'), + ModuleExportsBody: babel.template.ast([ + 'preprocessor(on, config)', + 'return config // IMPORTANT to return the config object', + ].join('\n'), { preserveComments: true }), } }, test: (cwd) => { diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.test.ts index 0e13ecbe9bfd..7bf01624006a 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.test.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.test.ts @@ -3,6 +3,7 @@ import { expect, use } from 'chai' import sinonChai from 'sinon-chai' import mockFs from 'mock-fs' import { ReactScriptsTemplate } from './react-scripts' +import { snapshotPluginsAstCode } from '../../../test-utils' use(sinonChai) @@ -60,4 +61,6 @@ describe('create-react-app install template', () => { expect(success).to.equal(false) expect(global.console.warn).to.be.called }) + + it('correctly generates plugins config', () => snapshotPluginsAstCode(ReactScriptsTemplate)) }) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts index 2ea269f97e9d..bddd0e5afb22 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts @@ -13,23 +13,12 @@ export const ReactScriptsTemplate: Template = { ? 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/react-scripts' : 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/react-scripts-folder' }, - getPluginsCode: () => { - return [ - 'const preprocessor = require(\'@cypress/react/plugins/react-scripts\')', - 'module.exports = (on, config) => {', - ' preprocessor(on, config)', - ' // IMPORTANT to return the config object', - ' return config', - '}', - ].join('\n') - }, getPluginsCodeAst: () => { return { - Require: babel.template('const preprocessor = require(\'@cypress/react/plugins/react-scripts\')'), - ModuleExportsBody: babel.template([ - ' preprocessor(on, config)', - ' // IMPORTANT to return the config object', - ' return config', + Require: babel.template.ast('const preprocessor = require(\'@cypress/react/plugins/react-scripts\')'), + ModuleExportsBody: babel.template.ast([ + 'preprocessor(on, config)', + 'return config // IMPORTANT to return the config object', ].join('\n'), { preserveComments: true }), } }, diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.test.ts index d51ed092b559..4212fcc04f51 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.test.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.test.ts @@ -1,21 +1,11 @@ import { expect } from 'chai' import mockFs from 'mock-fs' +import { snapshotPluginsAstCode } from '../../../test-utils' import { WebpackTemplate } from './reactWebpackFile' describe('webpack-file install template', () => { afterEach(mockFs.restore) - it('suggests the right code', () => { - expect( - WebpackTemplate.getPluginsCode( - { - webpackConfigPath: '/somePath/webpack.config.js', - }, - { cypressProjectRoot: '/' }, - ), - ).to.contain('config.env.webpackFilename = \'somePath/webpack.config.js\'') - }) - it('resolves webpack.config.js', () => { mockFs({ '/webpack.config.js': 'module.exports = { }', @@ -73,4 +63,12 @@ describe('webpack-file install template', () => { expect(success).to.equal(false) expect(payload).to.equal(undefined) }) + + it('correctly generates plugins config when webpack config path is missing', () => { + snapshotPluginsAstCode(WebpackTemplate) + }) + + it('correctly generates plugins config when webpack config path is provided', () => { + snapshotPluginsAstCode(WebpackTemplate, { webpackConfigPath: '/config/webpack.config.js' }) + }) }) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.ts b/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.ts index 5638b5abe69f..c977f31afc52 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.ts @@ -1,3 +1,4 @@ +import * as babel from '@babel/core' import path from 'path' import { Template } from '../Template' import { findWebpackConfig } from '../templateUtils' @@ -9,24 +10,23 @@ export const WebpackTemplate: Template<{ webpackConfigPath: string }> = { return 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/webpack-file' }, recommendedComponentFolder: 'cypress/component', - getPluginsCode: (payload, { cypressProjectRoot }) => { + getPluginsCodeAst: (payload, { cypressProjectRoot }) => { const includeWarnComment = !payload const webpackConfigPath = payload ? path.relative(cypressProjectRoot, payload.webpackConfigPath) : './webpack.config.js' - return [ - 'const preprocessor = require(\'@cypress/react/plugins/load-webpack\')', - 'module.exports = (on, config) => {', - includeWarnComment - ? '// TODO replace with valid webpack config path' - : '', - `config.env.webpackFilename = '${webpackConfigPath}'`, - ' preprocessor(on, config)', - ' // IMPORTANT to return the config object', - ' return config', - '}', - ].join('\n') + return { + Require: babel.template.ast('const preprocessor = require("@cypress/react/plugins/load-webpack")'), + ModuleExportsBody: babel.template.ast([ + includeWarnComment + ? '// TODO replace with valid webpack config path' + : '', + `config.env.webpackFilename = '${webpackConfigPath}'`, + 'preprocessor(on, config)', + 'return config // IMPORTANT to return the config object', + ].join('\n'), { preserveComments: true }), + } }, test: (root) => { const webpackConfigPath = findWebpackConfig(root) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/rollup.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/rollup.test.ts index ae5e44d6e0e0..789419d84da5 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/rollup.test.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/rollup.test.ts @@ -1,21 +1,11 @@ import { expect } from 'chai' import mockFs from 'mock-fs' +import { snapshotPluginsAstCode } from '../../../test-utils' import { RollupTemplate } from './rollup' describe('rollup-file install template', () => { afterEach(mockFs.restore) - it('suggests the right code', () => { - expect( - RollupTemplate.getPluginsCode( - { - rollupConfigPath: '/configs/rollup.config.js', - }, - { cypressProjectRoot: '/' }, - ), - ).to.contain('configFile: \'configs/rollup.config.js\'') - }) - it('resolves rollup.config.js', () => { mockFs({ '/rollup.config.js': 'module.exports = { }', @@ -71,4 +61,12 @@ describe('rollup-file install template', () => { expect(success).to.equal(false) expect(payload).to.equal(undefined) }) + + it('correctly generates plugins config when webpack config path is missing', () => { + snapshotPluginsAstCode(RollupTemplate) + }) + + it('correctly generates plugins config when webpack config path is provided', () => { + snapshotPluginsAstCode(RollupTemplate, { rollupConfigPath: '/config/rollup.config.js' }) + }) }) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/rollup.ts b/npm/create-cypress-tests/src/component-testing/templates/react/rollup.ts index 61bd32285317..b56b91dd74da 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/rollup.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/rollup.ts @@ -4,6 +4,7 @@ import findUp from 'find-up' import highlight from 'cli-highlight' import { createFindPackageJsonIterator } from '../../../findPackageJson' import { Template } from '../Template' +import * as babel from '@babel/core' export function extractRollupConfigPathFromScript (script: string) { if (script.includes('rollup ')) { @@ -25,31 +26,29 @@ export const RollupTemplate: Template<{ rollupConfigPath: string }> = { return 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/rollup' }, recommendedComponentFolder: 'src', - getPluginsCode: (payload, { cypressProjectRoot }) => { + getPluginsCodeAst: (payload, { cypressProjectRoot }) => { const includeWarnComment = !payload const rollupConfigPath = payload ? path.relative(cypressProjectRoot, payload.rollupConfigPath) : 'rollup.config.js' - return [ - `// do not forget to install`, - `const rollupPreprocessor = require('@bahmutov/cy-rollup')`, - `module.exports = (on, config) => {`, - ` on(`, - ` 'file:preprocessor',`, - ` rollupPreprocessor({`, - includeWarnComment - ? ' // TODO replace with valid rollup config path' - : '', - ` configFile: '${rollupConfigPath}',`, - ` }),`, - ` )`, - ``, - ` require('@cypress/code-coverage/task')(on, config)`, - ` // IMPORTANT to return the config object`, - ` return config`, - `}`, - ].join('\n') + return { + Require: babel.template.ast('const rollupPreprocessor = require("@bahmutov/cy-rollup")'), + ModuleExportsBody: babel.template.ast([ + `on(`, + ` 'file:preprocessor',`, + ` rollupPreprocessor({`, + includeWarnComment + ? ' // TODO replace with valid rollup config path' + : '', + ` configFile: '${rollupConfigPath}',`, + ` }),`, + `)`, + ``, + `require('@cypress/code-coverage/task')(on, config)`, + `return config // IMPORTANT to return the config object`, + ].join('\n'), { preserveComments: true }), + } }, printHelper: () => { console.log( diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options-module-exports.template.js b/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options-module-exports.template.js new file mode 100644 index 000000000000..90a4d5b60a33 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options-module-exports.template.js @@ -0,0 +1,29 @@ +const opts = webpackPreprocessor.defaultOptions +const babelLoader = opts.webpackOptions.module.rules[0].use[0] + +// add React preset to be able to transpile JSX +babelLoader.options.presets.push(require.resolve('@babel/preset-react')) + +// We can also push Babel istanbul plugin to instrument the code on the fly +// and get code coverage reports from component tests (optional) +if (!babelLoader.options.plugins) { + babelLoader.options.plugins = [] +} + +babelLoader.options.plugins.push(require.resolve('babel-plugin-istanbul')) + +// in order to mock named imports, need to include a plugin +babelLoader.options.plugins.push([ + require.resolve('@babel/plugin-transform-modules-commonjs'), + { + loose: true, + }, +]) + +// add code coverage plugin +require('@cypress/code-coverage/task')(on, config) + +on('file:preprocessor', webpackPreprocessor(opts)) + +// if adding code coverage, important to return updated config +return config diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options.ts b/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options.ts index 5c46700867cc..7a4240d6fc33 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options.ts @@ -1,3 +1,6 @@ +import fs from 'fs' +import path from 'path' +import * as babel from '@babel/core' import chalk from 'chalk' import { Template } from '../Template' @@ -9,43 +12,14 @@ export const WebpackOptions: Template = { }, test: () => ({ success: false }), recommendedComponentFolder: 'src', - getPluginsCode: () => { - return [ - `const webpackPreprocessor = require('@cypress/webpack-preprocessor')`, - ``, - `// Cypress Webpack preprocessor includes Babel env preset,`, - `// but to transpile JSX code we need to add Babel React preset`, - `module.exports = (on, config) => {`, - ` const opts = webpackPreprocessor.defaultOptions`, - ` const babelLoader = opts.webpackOptions.module.rules[0].use[0]`, - ``, - ` // add React preset to be able to transpile JSX`, - ` babelLoader.options.presets.push(require.resolve('@babel/preset-react'))`, - ``, - ` // We can also push Babel istanbul plugin to instrument the code on the fly`, - ` // and get code coverage reports from component tests (optional)`, - ` if (!babelLoader.options.plugins) {`, - ` babelLoader.options.plugins = []`, - ` }`, - ` babelLoader.options.plugins.push(require.resolve('babel-plugin-istanbul'))`, - ``, - ` // in order to mock named imports, need to include a plugin`, - ` babelLoader.options.plugins.push([`, - ` require.resolve('@babel/plugin-transform-modules-commonjs'),`, - ` {`, - ` loose: true,`, - ` },`, - ` ])`, - ``, - ` // add code coverage plugin`, - ` require('@cypress/code-coverage/task')(on, config)`, - ``, - ` on('file:preprocessor', webpackPreprocessor(opts))`, - ``, - ` // if adding code coverage, important to return updated config`, - ` return config`, - `}`, - ].join('\n') + getPluginsCodeAst: () => { + return { + Require: babel.template.ast('const webpackPreprocessor = require("@cypress/webpack-preprocessor")'), + ModuleExportsBody: babel.template.ast( + fs.readFileSync(path.resolve(__dirname, 'webpack-options-module-exports.template.js'), { encoding: 'utf-8' }), + { preserveComments: true }, + ), + } }, printHelper: () => { console.log( diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/webpackOptions.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/webpackOptions.test.ts new file mode 100644 index 000000000000..c7b7e2e6f9a3 --- /dev/null +++ b/npm/create-cypress-tests/src/component-testing/templates/react/webpackOptions.test.ts @@ -0,0 +1,6 @@ +import { WebpackOptions } from './webpack-options' +import { snapshotPluginsAstCode } from '../../../test-utils' + +describe('webpack-options template', () => { + it('correctly generates plugins config', () => snapshotPluginsAstCode(WebpackOptions)) +}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.test.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.test.ts index 61ae45f49cab..e0b058958ef2 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.test.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.test.ts @@ -1,20 +1,10 @@ import { expect } from 'chai' import mockFs from 'mock-fs' +import { snapshotPluginsAstCode } from '../../../test-utils' import { VueWebpackTemplate } from './vueWebpackFile' describe('vue webpack-file install template', () => { - afterEach(mockFs.restore) - - it('suggests the right code', () => { - expect( - VueWebpackTemplate.getPluginsCode( - { - webpackConfigPath: '/somePath/webpack.config.js', - }, - { cypressProjectRoot: '/' }, - ), - ).to.contain(`on('file:preprocessor', onFilePreprocessor(\'somePath/webpack.config.js\'))`) - }) + beforeEach(mockFs.restore) it('resolves webpack.config.js', () => { mockFs({ @@ -73,4 +63,12 @@ describe('vue webpack-file install template', () => { expect(success).to.equal(false) expect(payload).to.equal(undefined) }) + + it('correctly generates plugins config when webpack config path is missing', () => { + snapshotPluginsAstCode(VueWebpackTemplate) + }) + + it('correctly generates plugins config when webpack config path is provided', () => { + snapshotPluginsAstCode(VueWebpackTemplate, { webpackConfigPath: '/build/webpack.config.js' }) + }) }) diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts index 267c6d8ad110..1ffe5810e239 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts @@ -1,3 +1,4 @@ +import * as babel from '@babel/core' import path from 'path' import { Template } from '../Template' import { findWebpackConfig } from '../templateUtils' @@ -5,28 +6,27 @@ import { findWebpackConfig } from '../templateUtils' export const VueWebpackTemplate: Template<{ webpackConfigPath: string }> = { message: 'It looks like you have custom `webpack.config.js`. We can use it to bundle the components for testing.', - getExampleUrl: () => { - return 'https://github.com/cypress-io/cypress/tree/develop/npm/vue/examples/cli' - }, + getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/vue/examples/cli', recommendedComponentFolder: 'cypress/component', - getPluginsCode: (payload, { cypressProjectRoot }) => { + getPluginsCodeAst: (payload, { cypressProjectRoot }) => { const includeWarnComment = !payload const webpackConfigPath = payload ? path.relative(cypressProjectRoot, payload.webpackConfigPath) : './webpack.config.js' - return [ - 'const {', - ' onFilePreprocessor', - '} = require(\'@cypress/vue/preprocessor/webpack\')', - '', - 'module.exports = (on, config) => {', - includeWarnComment - ? '// TODO replace with valid webpack config path' - : '', - ` on('file:preprocessor', onFilePreprocessor('${webpackConfigPath}'))`, - '}', - ].join('\n') + return { + Require: babel.template.ast([ + 'const {', + ' onFilePreprocessor', + '} = require(\'@cypress/vue/preprocessor/webpack\')', + ].join('\n')), + ModuleExportsBody: babel.template.ast([ + includeWarnComment + ? '// TODO replace with valid webpack config path' + : '', + `on('file:preprocessor', onFilePreprocessor('${webpackConfigPath}'))`, + ].join('\n'), { preserveComments: true }), + } }, test: (root) => { const webpackConfigPath = findWebpackConfig(root) diff --git a/npm/create-cypress-tests/src/test-utils.ts b/npm/create-cypress-tests/src/test-utils.ts index ef00a66f4dc5..6021690678b9 100644 --- a/npm/create-cypress-tests/src/test-utils.ts +++ b/npm/create-cypress-tests/src/test-utils.ts @@ -1,4 +1,9 @@ +import * as babel from '@babel/core' +import snapshot from 'snap-shot-it' +import mockFs from 'mock-fs' import { SinonSpyCallApi } from 'sinon' +import { createTransformPluginsFileBabelPlugin } from './component-testing/babel/babelTransform' +import { Template } from './component-testing/templates/Template' export function someOfSpyCallsIncludes (spy: any, logPart: string) { return spy.getCalls().some( @@ -7,3 +12,23 @@ export function someOfSpyCallsIncludes (spy: any, logPart: string) { }, ) } + +export function snapshotPluginsAstCode (template: Template, payload?: T) { + mockFs.restore() + const code = [ + 'const something = require("something")', + 'module.exports = (on) => {', + '};', + ].join('\n') + + const babelPlugin = createTransformPluginsFileBabelPlugin(template.getPluginsCodeAst(payload ?? null, { cypressProjectRoot: '/' })) + const output = babel.transformSync(code, { + plugins: [babelPlugin], + }) + + if (!output || !output.code) { + throw new Error('Babel transform output is empty.') + } + + snapshot(output.code) +} From dac5c024ed3302d1af399a4f4c63a159e30c5383 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Fri, 6 Nov 2020 17:22:36 +0200 Subject: [PATCH 4/9] Add git status guard --- .../init-component-testing.ts | 4 +++- .../component-testing/templates/Template.ts | 1 - .../templates/vue/vueWebpackFile.ts | 2 +- npm/create-cypress-tests/src/main.ts | 21 +++++++++++++++++-- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/npm/create-cypress-tests/src/component-testing/init-component-testing.ts b/npm/create-cypress-tests/src/component-testing/init-component-testing.ts index b1d08eade154..91404f45a457 100644 --- a/npm/create-cypress-tests/src/component-testing/init-component-testing.ts +++ b/npm/create-cypress-tests/src/component-testing/init-component-testing.ts @@ -80,7 +80,9 @@ async function injectAndShowCypressJsonConfig ( } async function injectAndShowSupportConfig (supportFilePath: string, framework: string) { - const importCode = `import \'@cypress/${framework}/support\'` + const importCode = framework === 'vue' + ? `import '@cypress/vue/dist/support'` // todo change vue bundle to output the right declaration + : `import \'@cypress/${framework}/support\'` await injectOrShowConfigCode(() => injectImportSupportCode(supportFilePath, importCode), { code: importCode, diff --git a/npm/create-cypress-tests/src/component-testing/templates/Template.ts b/npm/create-cypress-tests/src/component-testing/templates/Template.ts index 44933daa1280..426b156bc544 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/Template.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/Template.ts @@ -1,4 +1,3 @@ -import * as babel from '@babel/core' import { PluginsConfigAst } from '../babel/babelTransform' export interface Template { diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts index 1ffe5810e239..1e49d1a9ddc8 100644 --- a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts +++ b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts @@ -18,7 +18,7 @@ export const VueWebpackTemplate: Template<{ webpackConfigPath: string }> = { Require: babel.template.ast([ 'const {', ' onFilePreprocessor', - '} = require(\'@cypress/vue/preprocessor/webpack\')', + '} = require(\'@cypress/vue/dist/preprocessor/webpack\')', ].join('\n')), ModuleExportsBody: babel.template.ast([ includeWarnComment diff --git a/npm/create-cypress-tests/src/main.ts b/npm/create-cypress-tests/src/main.ts index 28d5f435ef2b..5908bdface0d 100644 --- a/npm/create-cypress-tests/src/main.ts +++ b/npm/create-cypress-tests/src/main.ts @@ -15,9 +15,19 @@ type MainArgv = { setupComponentTesting: boolean } -async function shouldUseYarn () { - const execAsync = util.promisify(exec) +const execAsync = util.promisify(exec) + +async function getGitStatus () { + try { + let { stdout } = await execAsync(`git status --porcelain`) + + return stdout.trim() + } catch (e) { + return '' + } +} +async function shouldUseYarn () { return execAsync('yarn --version') .then(() => true) .catch(() => false) @@ -62,6 +72,13 @@ export async function main ({ useNpm, ignoreTs, setupComponentTesting, ignoreExa console.log(`Running ${chalk.green('cypress 🌲')} installation wizard for ${chalk.cyan(`${name}@${version}`)}`) + const gitStatus = await getGitStatus() + + if (gitStatus) { + console.error(`\n${chalk.bold.red('This repository has untracked files or uncommmited changes.')}\nThis command will ${chalk.cyan('make changes in the codebase')}, so please remove untracked files, stash or commit any changes, and try again.`) + process.exit(1) + } + const { config, cypressConfigPath } = await findInstalledOrInstallCypress({ useYarn, useTypescript, ignoreExamples }) const shouldSetupComponentTesting = setupComponentTesting ?? await askForComponentTesting() From 8c3ef407aae212a780ea635f3b73cef35fd23d45 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Fri, 6 Nov 2020 17:51:15 +0200 Subject: [PATCH 5/9] Add git status guard --- .../__snapshots__/vueWebpackFile.test.ts.js | 4 +-- npm/create-cypress-tests/cypress.json | 1 + npm/create-cypress-tests/src/main.test.ts | 26 ++++++++++++++++--- npm/create-cypress-tests/src/main.ts | 8 ++++-- 4 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 npm/create-cypress-tests/cypress.json diff --git a/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js b/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js index 1c7ed57c8395..373b8e8975f6 100644 --- a/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js +++ b/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js @@ -1,7 +1,7 @@ exports['vue webpack-file install template correctly generates plugins config when webpack config path is missing 1'] = ` const { onFilePreprocessor -} = require('@cypress/vue/preprocessor/webpack'); +} = require('@cypress/vue/dist/preprocessor/webpack'); const something = require("something"); @@ -14,7 +14,7 @@ module.exports = (on, config) => { exports['vue webpack-file install template correctly generates plugins config when webpack config path is provided 1'] = ` const { onFilePreprocessor -} = require('@cypress/vue/preprocessor/webpack'); +} = require('@cypress/vue/dist/preprocessor/webpack'); const something = require("something"); diff --git a/npm/create-cypress-tests/cypress.json b/npm/create-cypress-tests/cypress.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/npm/create-cypress-tests/cypress.json @@ -0,0 +1 @@ +{} diff --git a/npm/create-cypress-tests/src/main.test.ts b/npm/create-cypress-tests/src/main.test.ts index c904a408bc29..f8d18633bb79 100644 --- a/npm/create-cypress-tests/src/main.test.ts +++ b/npm/create-cypress-tests/src/main.test.ts @@ -1,11 +1,12 @@ import { expect, use } from 'chai' import path from 'path' -import sinon, { SinonStub, SinonSpy, SinonSpyCallApi } from 'sinon' +import sinon, { SinonStub, SinonSpy, SinonSpyCallApi, restore } from 'sinon' import mockFs from 'mock-fs' import fsExtra from 'fs-extra' import { main } from './main' import sinonChai from 'sinon-chai' import childProcess from 'child_process' +import chalk from 'chalk' use(sinonChai) @@ -17,18 +18,22 @@ function someOfSpyCallsIncludes (spy: any, logPart: string) { ) } -describe('init script', () => { +describe('create-cypress-tests', () => { let promptSpy: SinonStub | null = null let logSpy: SinonSpy | null = null + let errorSpy: SinonSpy | null = null let execStub: SinonStub | null = null let fsCopyStub: SinonStub | null = null + let processExitStub: SinonStub | null = null beforeEach(() => { logSpy = sinon.spy(global.console, 'log') + errorSpy = sinon.spy(global.console, 'error') // @ts-ignore execStub = sinon.stub(childProcess, 'exec').callsFake((command, callback) => callback()) // @ts-ignore fsCopyStub = sinon.stub(fsExtra, 'copy').returns(Promise.resolve()) + processExitStub = sinon.stub(process, 'exit') }) afterEach(() => { @@ -37,6 +42,9 @@ describe('init script', () => { promptSpy?.restore() execStub?.restore() fsCopyStub?.restore() + processExitStub?.restore() + execStub?.restore() + errorSpy?.restore() }) it('Install cypress if no config found', async () => { @@ -53,6 +61,7 @@ describe('init script', () => { execStub ?.onFirstCall().callsFake((command, callback) => callback('yarn is not available')) ?.onSecondCall().callsFake((command, callback) => callback()) + ?.onThirdCall().callsFake((command, callback) => callback()) mockFs({ '/package.json': JSON.stringify({ }), @@ -90,6 +99,17 @@ describe('init script', () => { expect(someOfSpyCallsIncludes(logSpy, 'yarn cypress open')).to.be.true }) + it('Fails if git repository have untracked or uncommited files', async () => { + execStub?.callsFake((_, callback) => callback(null, { stdout: 'test' })) + await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) + + expect( + someOfSpyCallsIncludes(errorSpy, 'This repository has untracked files or uncommmited changes.'), + ).to.equal(true) + + expect(processExitStub).to.be.called + }) + context('e2e fs tests', () => { const e2eTestOutputPath = path.resolve(__dirname, 'test-output') @@ -101,7 +121,7 @@ describe('init script', () => { await fsExtra.mkdir(e2eTestOutputPath) }) - it('Copies plugins and support files', async () => { + it.only('Copies plugins and support files', async () => { await fsExtra.outputFile( path.join(e2eTestOutputPath, 'package.json'), JSON.stringify({ name: 'test' }, null, 2), diff --git a/npm/create-cypress-tests/src/main.ts b/npm/create-cypress-tests/src/main.ts index 5908bdface0d..38447e5fd7a2 100644 --- a/npm/create-cypress-tests/src/main.ts +++ b/npm/create-cypress-tests/src/main.ts @@ -15,12 +15,14 @@ type MainArgv = { setupComponentTesting: boolean } -const execAsync = util.promisify(exec) - async function getGitStatus () { + const execAsync = util.promisify(exec) + try { let { stdout } = await execAsync(`git status --porcelain`) + console.log(stdout) + return stdout.trim() } catch (e) { return '' @@ -28,6 +30,8 @@ async function getGitStatus () { } async function shouldUseYarn () { + const execAsync = util.promisify(exec) + return execAsync('yarn --version') .then(() => true) .catch(() => false) From d414987b48253ad9cdbf38ab5bbc02a27aa59896 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Fri, 6 Nov 2020 18:18:30 +0200 Subject: [PATCH 6/9] Fix last test --- npm/create-cypress-tests/cypress.json | 1 - npm/create-cypress-tests/src/main.test.ts | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 npm/create-cypress-tests/cypress.json diff --git a/npm/create-cypress-tests/cypress.json b/npm/create-cypress-tests/cypress.json deleted file mode 100644 index 0967ef424bce..000000000000 --- a/npm/create-cypress-tests/cypress.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/npm/create-cypress-tests/src/main.test.ts b/npm/create-cypress-tests/src/main.test.ts index f8d18633bb79..c8ae6c868ee6 100644 --- a/npm/create-cypress-tests/src/main.test.ts +++ b/npm/create-cypress-tests/src/main.test.ts @@ -6,7 +6,6 @@ import fsExtra from 'fs-extra' import { main } from './main' import sinonChai from 'sinon-chai' import childProcess from 'child_process' -import chalk from 'chalk' use(sinonChai) @@ -33,7 +32,9 @@ describe('create-cypress-tests', () => { execStub = sinon.stub(childProcess, 'exec').callsFake((command, callback) => callback()) // @ts-ignore fsCopyStub = sinon.stub(fsExtra, 'copy').returns(Promise.resolve()) - processExitStub = sinon.stub(process, 'exit') + processExitStub = sinon.stub(process, 'exit').callsFake(() => { + throw new Error('process.exit should not be called') + }) }) afterEach(() => { @@ -121,7 +122,7 @@ describe('create-cypress-tests', () => { await fsExtra.mkdir(e2eTestOutputPath) }) - it.only('Copies plugins and support files', async () => { + it('Copies plugins and support files', async () => { await fsExtra.outputFile( path.join(e2eTestOutputPath, 'package.json'), JSON.stringify({ name: 'test' }, null, 2), From efeb4c77d1553354a59e227f869fcbd971d194f8 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Mon, 9 Nov 2020 16:35:16 +0200 Subject: [PATCH 7/9] Fix tests --- .../component-testing/init-component-testing.test.ts | 6 +++--- npm/create-cypress-tests/src/main.test.ts | 6 +++++- packages/example/cypress/support/commands.js | 4 ++-- packages/example/cypress/tsconfig.json | 10 ++++++++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts b/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts index 83fd89aa498b..1353623145f4 100644 --- a/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts +++ b/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts @@ -19,7 +19,7 @@ describe('init component tests script', () => { let processExitStub: SinonStub | null = null let execStub: SinonStub | null = null - const e2eTestOutputPath = path.resolve(__dirname, '..', 'test-output', 'init-component-testing') + const e2eTestOutputPath = path.resolve(__dirname, '..', 'test-output') const cypressConfigPath = path.join(e2eTestOutputPath, 'cypress.json') beforeEach(async () => { @@ -30,8 +30,8 @@ describe('init component tests script', () => { throw new Error(`${chalk.red('process.exit')} should not be called`) }) - await fs.removeSync(e2eTestOutputPath) - await fs.mkdirSync(e2eTestOutputPath) + await fs.remove(e2eTestOutputPath) + await fs.mkdir(e2eTestOutputPath) }) afterEach(() => { diff --git a/npm/create-cypress-tests/src/main.test.ts b/npm/create-cypress-tests/src/main.test.ts index c8ae6c868ee6..07ca07c5abd7 100644 --- a/npm/create-cypress-tests/src/main.test.ts +++ b/npm/create-cypress-tests/src/main.test.ts @@ -102,6 +102,8 @@ describe('create-cypress-tests', () => { it('Fails if git repository have untracked or uncommited files', async () => { execStub?.callsFake((_, callback) => callback(null, { stdout: 'test' })) + processExitStub?.callsFake(() => {}) + await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) expect( @@ -116,13 +118,14 @@ describe('create-cypress-tests', () => { beforeEach(async () => { fsCopyStub?.restore() + mockFs.restore() sinon.stub(process, 'cwd').returns(e2eTestOutputPath) await fsExtra.remove(e2eTestOutputPath) await fsExtra.mkdir(e2eTestOutputPath) }) - it('Copies plugins and support files', async () => { + it.only('Copies plugins and support files', async () => { await fsExtra.outputFile( path.join(e2eTestOutputPath, 'package.json'), JSON.stringify({ name: 'test' }, null, 2), @@ -147,6 +150,7 @@ describe('create-cypress-tests', () => { await main({ useNpm: false, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'tsconfig.json')) + console.log(path.resolve(e2eTestOutputPath, 'cypress', 'tsconfig.json')) }) }) }) diff --git a/packages/example/cypress/support/commands.js b/packages/example/cypress/support/commands.js index 7ad2e2fefae7..b39d4ca4954d 100644 --- a/packages/example/cypress/support/commands.js +++ b/packages/example/cypress/support/commands.js @@ -10,7 +10,7 @@ // // // -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) +// Cypress.Commands.add("login", (email, password) => { ... }) // // // -- This is a child command -- @@ -22,4 +22,4 @@ // // // -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/packages/example/cypress/tsconfig.json b/packages/example/cypress/tsconfig.json index e69de29bb2d1..f94e89468ef7 100644 --- a/packages/example/cypress/tsconfig.json +++ b/packages/example/cypress/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress"] + }, + "include": [ + "**/*.ts" + ] +} \ No newline at end of file From 177c0c0bde99c4089c2cf2404275313d4db4efde Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Mon, 9 Nov 2020 16:55:06 +0200 Subject: [PATCH 8/9] Revert changes for packages/example --- packages/example/cypress/plugins/index.js | 1 - packages/example/cypress/tsconfig.json | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/example/cypress/plugins/index.js b/packages/example/cypress/plugins/index.js index 59b2bab6e4e6..aa9918d21530 100644 --- a/packages/example/cypress/plugins/index.js +++ b/packages/example/cypress/plugins/index.js @@ -15,7 +15,6 @@ /** * @type {Cypress.PluginConfig} */ -// eslint-disable-next-line no-unused-vars module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config diff --git a/packages/example/cypress/tsconfig.json b/packages/example/cypress/tsconfig.json index f94e89468ef7..6e395b3e5765 100644 --- a/packages/example/cypress/tsconfig.json +++ b/packages/example/cypress/tsconfig.json @@ -1,10 +1,10 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["es5", "dom"], - "types": ["cypress"] - }, - "include": [ +{ + "compilerOptions": { + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress"] + }, + "include": [ "**/*.ts" - ] + ] } \ No newline at end of file From 1c2e23694f84e8981712da48a6ea5d1bc6914ce2 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Mon, 9 Nov 2020 16:56:12 +0200 Subject: [PATCH 9/9] Revert changes for packages/example/tsconfig.json --- packages/example/cypress/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/example/cypress/tsconfig.json b/packages/example/cypress/tsconfig.json index 6e395b3e5765..4109e0ec1aff 100644 --- a/packages/example/cypress/tsconfig.json +++ b/packages/example/cypress/tsconfig.json @@ -5,6 +5,6 @@ "types": ["cypress"] }, "include": [ - "**/*.ts" + "**/*.ts" ] } \ No newline at end of file