diff --git a/index.js b/index.js
index 9af0473..22d0089 100644
--- a/index.js
+++ b/index.js
@@ -1,6 +1,8 @@
const { babelOptions } = require('@folio/stripes-webpack');
+const { getBaseCypressConfig } = require('./lib/test/cypress-service');
module.exports = {
babelOptions,
+ getBaseCypressConfig
};
diff --git a/lib/commands/test/cypress.js b/lib/commands/test/cypress.js
new file mode 100644
index 0000000..79bfff2
--- /dev/null
+++ b/lib/commands/test/cypress.js
@@ -0,0 +1,97 @@
+const importLazy = require('import-lazy')(require);
+// const fs = require('fs');
+// const child = require('child_process');
+
+const { contextMiddleware } = importLazy('../../cli/context-middleware');
+const { stripesConfigMiddleware } = importLazy('../../cli/stripes-config-middleware');
+const { CypressService } = importLazy('../../test/cypress-service');
+const StripesPlatform = importLazy('../../platform/stripes-platform');
+const { serverOptions, okapiOptions, stripesConfigFile, stripesConfigStdin, stripesConfigOptions } = importLazy('../common-options');
+const StripesCore = importLazy('../../cli/stripes-core');
+
+function cypressCommand(argv) {
+ const context = argv.context;
+
+ // pass moduleName up to globals so that it can be accessed for standalone webpack config.
+ process.env.stripesCLIContextModuleName = context.moduleName;
+
+ // Default test command to test env
+ if (!process.env.NODE_ENV) {
+ process.env.NODE_ENV = 'test';
+ }
+
+ if (!(context.isUiModule || context.isStripesModule)) {
+ console.log('Tests are only supported within an app context.');
+ return;
+ }
+
+ const platform = new StripesPlatform(argv.stripesConfig, context, argv);
+ const webpackOverrides = platform.getWebpackOverrides(context);
+
+ if (context.plugin && context.plugin.beforeBuild) {
+ webpackOverrides.push(context.plugin.beforeBuild(argv));
+ }
+
+ console.log('Starting Cypress tests...');
+ const stripes = new StripesCore(context, platform.aliases);
+ const webpackConfigOptions = {
+ coverage: argv.coverage,
+ omitPlatform: context.type === 'component',
+ bundle: argv.bundle,
+ webpackOverrides,
+ };
+ const webpackConfig = stripes.getStripesWebpackConfig(platform.getStripesConfig(), webpackConfigOptions, context);
+
+ // This fixes the warnings similar to:
+ // WARNING in ./node_modules/mocha/mocha-es2018.js 18541:26-55
+ // Critical dependency: the request of a dependency is an expression
+ // https://github.com/mochajs/mocha/issues/2448#issuecomment-355222358
+ webpackConfig.module.exprContextCritical = false;
+
+ // This fixes warning:
+ // WARNING in DefinePlugin
+ // Conflicting values for 'process.env.NODE_ENV'
+ // https://webpack.js.org/configuration/mode/#usage
+ webpackConfig.mode = 'none';
+
+ const cypressService = new CypressService(context.cwd);
+ if (argv.cypress?.install) {
+ cypressService.installAndRun(webpackConfig, Object.assign({}, argv.cypress, { watch: argv.watch, cache: argv.cache }), context);
+ } else {
+ cypressService.runCypressTests(webpackConfig, Object.assign({}, argv.cypress, { watch: argv.watch, cache: argv.cache }), context);
+ }
+}
+
+module.exports = {
+ command: 'cypress [configFile]',
+ describe: 'Run the current app module\'s Cypress tests',
+ builder: (yargs) => {
+ yargs
+ .middleware([
+ contextMiddleware(),
+ stripesConfigMiddleware(),
+ ])
+ .positional('configFile', stripesConfigFile.configFile)
+ .option('coverage', {
+ describe: 'Enable Cypress coverage reports',
+ type: 'boolean',
+ alias: 'cypress.coverage', // this allows --coverage to be passed to Cypress
+ })
+ .option('bundle', {
+ describe: 'Create and use a production bundle retaining test hooks',
+ type: 'boolean'
+ })
+ .option('cypress', {
+ describe: 'Options passed to Cypress using dot-notation and camelCase: --cypress.browsers=Chrome --cypress.singleRun',
+ })
+ .option('cypress.install', { type: 'boolean', describe: 'install cypress manually to account for ignore-scripts' })
+ .option('cypress.open', { type: 'boolean', describe: 'run cypress in ui mode' })
+ .option('cypress.browsers', { type: 'array', hidden: true }) // defined but hidden so yargs will parse as an array
+ .option('cypress.reporters', { type: 'array', hidden: true })
+ .option('watch', { type: 'boolean', describe: 'Watch test files for changes and run tests automatically when changes are saved.' })
+ .option('cache', { type: 'boolean', describe: 'Enable caching of test bundle. Defaults to false.' })
+ .options(Object.assign({}, serverOptions, okapiOptions, stripesConfigStdin, stripesConfigOptions))
+ .example('$0 test cypress', 'Run tests with Cypress for the current app module');
+ },
+ handler: cypressCommand,
+};
diff --git a/lib/test/cypress-plugin.js b/lib/test/cypress-plugin.js
new file mode 100644
index 0000000..2811aca
--- /dev/null
+++ b/lib/test/cypress-plugin.js
@@ -0,0 +1,18 @@
+///
+const webpack = require('@cypress/webpack-preprocessor')
+
+/**
+ * @type {Cypress.PluginConfig}
+ */
+module.exports = (on, config) => {
+ const options = {
+ // use the same Webpack options to bundle spec files as your app does "normally"
+ // which should instrument the spec files in this project
+ webpackOptions: require('../../webpack.config'),
+ watchOptions: {}
+ }
+ on('file:preprocessor', webpack(options))
+
+ require('@cypress/code-coverage/task')(on, config)
+ return config
+}
\ No newline at end of file
diff --git a/lib/test/cypress-service.js b/lib/test/cypress-service.js
new file mode 100644
index 0000000..e1c9085
--- /dev/null
+++ b/lib/test/cypress-service.js
@@ -0,0 +1,228 @@
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+const util = require('util');
+const exec = util.promisify(require('child_process').exec);
+
+const _pickBy = require('lodash/pickBy');
+
+const { defineConfig, run, open } = require('cypress');
+const webpack = require('@cypress/webpack-preprocessor');
+const logger = require('../cli/logger')('cypress');
+const getStripesWebpackConfigStandalone = require('./webpack-config-standalone');
+
+function getTestIndex(cwd, dirs) {
+ let file = path.join(cwd, dirs[0], 'index.js');
+ let i = 0;
+
+ while (!fs.existsSync(file) && dirs[++i]) {
+ file = path.join(cwd, dirs[i], 'index.js');
+ }
+
+ return file;
+}
+
+function getBaseCypressConfig(fn = (cfg) => cfg, context) {
+ const baseConfig = defineConfig({
+ browser: 'chrome',
+ viewportWidth: 800,
+ viewportHeight: 600,
+ component: {
+ devServer: {
+ framework: 'react',
+ bundler: 'webpack',
+ webpackConfig : getStripesWebpackConfigStandalone()
+ },
+ specPattern: '**/*[.-]test.js',
+ },
+ });
+ return fn(baseConfig, defineConfig);
+}
+
+class CypressService {
+ constructor(cwd) {
+ this.cwd = cwd;
+ }
+
+ generateCypressConfig(webpackConfig, cypressOptions) {
+ // TODO: Standardize on test folder, `test/bigtest` vs 'test' vs 'tests'
+ const testIndex = getTestIndex(this.cwd, ['test/bigtest', 'test', 'tests']);
+
+ // cypress webpack, ignores 'entry' so to keep it from griping, just exclude it.
+ const {
+ entry, // eslint-disable-line no-unused-vars
+ ...webpackConfigRest
+ } = webpackConfig;
+
+ const output = {
+ // The path defined here is the same as what cypress-webpack is using by default.
+ // https://github.com/ryanclark/cypress-webpack/blob/master/lib/webpack/defaults.js#L10
+
+ // We are redefining it here so we can work around a current limitation
+ // related to static files (translations) not loading correctly.
+ // Please see more comments under:
+ // https://github.com/ryanclark/cypress-webpack/issues/498
+ // path: path.join(os.tmpdir(), '_cypress_webpack_') + Math.floor(Math.random() * 1000000),
+ };
+
+ // set webpack's watch/cache features via cypress config.
+ // these features are unnecessary in the CI environment, so we turn them off by default.
+ // They can be enabled individually via command-line options '--watch' and '--cache'.
+ let webpackTestConfig = {};
+ webpackTestConfig.watch = !!cypressOptions?.watch;
+ webpackTestConfig.cache = !!cypressOptions?.cache;
+ // only apply 'false' options as overrides to what cypress-webpack wants to set.
+ webpackTestConfig = _pickBy(webpackTestConfig, (opt) => !opt);
+
+ // let cypressConfig = {
+ // frameworks: ['mocha', 'webpack'],
+ // reporters: ['mocha'],
+ // port: 9876,
+
+ // browsers: ['Chrome'],
+
+ // customLaunchers: {
+ // // Custom launcher for CI
+ // ChromeHeadlessDocker: {
+ // base: 'ChromeHeadless',
+ // flags: [
+ // '--no-sandbox',
+ // '--disable-web-security'
+ // ]
+ // },
+ // ChromeDocker: {
+ // base: 'Chrome',
+ // flags: [
+ // '--no-sandbox',
+ // '--disable-web-security'
+ // ]
+ // }
+ // },
+
+ // junitReporter: {
+ // outputDir: 'artifacts/runTest',
+ // useBrowserName: true,
+ // },
+
+ // files: [
+ // { pattern: testIndex, watched: false },
+ // // use output.path to work around the issue with loading
+ // // static files
+ // // https://github.com/ryanclark/cypress-webpack/issues/498
+ // {
+ // pattern: `${output.path}/**/*`,
+ // watched: false,
+ // included: false,
+ // served: true,
+ // },
+ // ],
+
+ // preprocessors: {
+ // [testIndex]: ['webpack']
+ // },
+
+ // webpack: {
+ // ...webpackConfigRest,
+ // ...webpackTestConfig,
+ // output,
+ // },
+ // webpackMiddleware: {
+ // stats: 'errors-only',
+ // },
+
+ // mochaReporter: {
+ // showDiff: true,
+ // },
+
+ // plugins: [
+ // 'cypress-chrome-launcher',
+ // 'cypress-firefox-launcher',
+ // 'cypress-mocha',
+ // 'cypress-webpack',
+ // 'cypress-mocha-reporter',
+ // ],
+
+ // coverageIstanbulReporter: {
+ // dir: 'artifacts/coverage',
+ // reports: ['text-summary', 'lcov'],
+ // thresholds: {
+ // // Thresholds under which cypress will return failure
+ // // Modules are expected to define their own values in cypress.conf.js
+ // global: {},
+ // each: {},
+ // }
+ // },
+ // };
+
+ // Apply user supplied --cypress options to configuration
+ // Added now so they will be available within app-supplied config function
+ // if (cypressOptions) {
+ // logger.log('Applying command-line Cypress options', cypressOptions);
+ // Object.assign(cypressConfig, cypressOptions);
+ // }
+
+ // if (cypressOptions && cypressOptions.coverage) {
+ // logger.log('Enabling coverage');
+ // cypressConfig.reporters.push('coverage-istanbul');
+ // cypressConfig.plugins.push('cypress-coverage-istanbul-reporter');
+ // }
+
+ // if (cypressConfig.reporters.includes('junit')) {
+ // logger.log('Enabling junit reporter');
+ // cypressConfig.plugins.push('cypress-junit-reporter');
+ // }
+
+ // Use cypress's parser to prep the base config
+ let cypressConfig = getBaseCypressConfig();
+
+ // Check for an app-supplied cypress config and apply it
+ const localConfig = path.join(this.cwd, 'cypress.config.js');
+ if (fs.existsSync(localConfig)) {
+ const appCypressConfig = require(localConfig); // eslint-disable-line
+ logger.log('Applying local cypress config', localConfig);
+ cypressConfig = appCypressConfig;
+
+ // Reapply user options so they take precedence
+ if (cypressOptions) {
+ Object.assign(cypressConfig, cypressOptions);
+ }
+ }
+ // console.log(JSON.stringify(cypressConfig, null, 2));
+ return cypressConfig;
+ }
+
+ // Runs the specified integration tests
+ runCypressTests(webpackConfig, cypressOptions) {
+ const cypressConfig = this.generateCypressConfig(webpackConfig, cypressOptions);
+
+ try {
+ const cyCommand = cypressOptions.open ? open : run;
+ cyCommand(cypressConfig)
+ .then(res => {
+ logger.log('Cypress results:', res);
+ })
+ .catch(err => {
+ logger.log(err.message);
+ console.log(err.message);
+ process.exit(1);
+ });
+ } catch (e) {
+ logger.log('Error running cypress tests:', e);
+ }
+ }
+
+ async installAndRun() {
+ console.log('Attempting cypress install...');
+ try {
+ await exec('npx cypress install');
+ this.runCypressTests.apply(this, arguments); // eslint-disable-line
+ } catch (e) {
+ console.log(e);
+ }
+ }
+}
+
+module.exports = {
+ getBaseCypressConfig,
+ CypressService
+};
diff --git a/lib/test/cypress-support.js b/lib/test/cypress-support.js
new file mode 100644
index 0000000..a7ecd76
--- /dev/null
+++ b/lib/test/cypress-support.js
@@ -0,0 +1,28 @@
+// ***********************************************************
+// This example support/component.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+import '@cypress/code-coverage/support';
+
+// Import commands.js using ES2015 syntax:
+// import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+
+import { mount } from 'cypress/react18';
+
+Cypress.Commands.add('mount', mount);
+
+// Example use:
+// cy.mount()
diff --git a/lib/test/support/component-index.html b/lib/test/support/component-index.html
new file mode 100644
index 0000000..1046822
--- /dev/null
+++ b/lib/test/support/component-index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Components App
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/test/webpack-config-standalone.js b/lib/test/webpack-config-standalone.js
new file mode 100644
index 0000000..697aded
--- /dev/null
+++ b/lib/test/webpack-config-standalone.js
@@ -0,0 +1,18 @@
+const process = require('process');
+const path = require('path');
+const getStripesWebpackConfig = require('./webpack-config');
+const StripesCore = require('../cli/stripes-core');
+
+module.exports = function getStripesWebpackConfigStandalone() {
+ const context = {
+ moduleName: process.env.stripesCLIContextModuleName || '',
+ cwd: process.cwd(),
+ cliRoot: path.resolve('@folio/stripes-cli')
+ };
+
+ const stripesCore = new StripesCore(context, { '@folio/stripes-webpack': '@folio/stripes-webpack' });
+
+ const componentsStripesConfig = { config: [], modules:[], languages: [] };
+ const webpackConfig = getStripesWebpackConfig(stripesCore, componentsStripesConfig, { config: componentsStripesConfig }, context);
+ return webpackConfig;
+};
diff --git a/lib/test/webpack-config.js b/lib/test/webpack-config.js
index c3aade1..9f017cc 100644
--- a/lib/test/webpack-config.js
+++ b/lib/test/webpack-config.js
@@ -54,3 +54,13 @@ module.exports = function getStripesWebpackConfig(stripeCore, stripesConfig, opt
config = applyWebpackOverrides(options.webpackOverrides, config);
return config;
};
+
+function getStripesWebpackConfigStandalone() {
+ const webpackConfig = getStripesWebpackConfig();
+ return webpackConfig;
+}
+
+// module.exports = {
+// default: getStripesWebpackConfig,
+// getStripesWebpackConfigStandalone,
+// };
diff --git a/package.json b/package.json
index 57aed75..2188644 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"docs": "node ./lib/doc/generator"
},
"dependencies": {
+ "@cypress/code-coverage": "^3.13.6",
"@folio/stripes-testing": "^3.0.0",
"@folio/stripes-webpack": "^5.0.0",
"@formatjs/cli": "^6.1.3",
@@ -29,6 +30,7 @@
"@octokit/rest": "^19.0.7",
"babel-plugin-istanbul": "^6.0.0",
"configstore": "^3.1.1",
+ "cypress": "12.0.0",
"debug": "^4.0.1",
"express": "^4.17.1",
"fast-glob": "^3.3.1",