2020-05-09 11:34:50 +02:00
|
|
|
|
/* eslint-disable no-irregular-whitespace */
|
|
|
|
|
import path from 'path';
|
2020-11-14 10:04:33 +01:00
|
|
|
|
import { remove, ensureDir, pathExists, writeFile, writeJSON } from 'fs-extra';
|
2020-05-09 11:34:50 +02:00
|
|
|
|
import { prompt } from 'enquirer';
|
|
|
|
|
import pLimit from 'p-limit';
|
|
|
|
|
|
|
|
|
|
import shell from 'shelljs';
|
2020-06-29 21:20:16 +02:00
|
|
|
|
import program from 'commander';
|
2020-05-09 11:34:50 +02:00
|
|
|
|
import { serve } from './utils/serve';
|
|
|
|
|
import { exec } from './utils/command';
|
2020-05-18 14:58:55 +02:00
|
|
|
|
// @ts-ignore
|
2020-09-19 19:17:43 +02:00
|
|
|
|
import { filterDataForCurrentCircleCINode } from './utils/concurrency';
|
2020-05-09 11:34:50 +02:00
|
|
|
|
|
|
|
|
|
import * as configs from './run-e2e-config';
|
|
|
|
|
|
|
|
|
|
const logger = console;
|
|
|
|
|
|
|
|
|
|
export interface Parameters {
|
2020-05-19 23:29:56 +02:00
|
|
|
|
/** E2E configuration name */
|
2020-05-09 11:34:50 +02:00
|
|
|
|
name: string;
|
2020-05-19 23:29:56 +02:00
|
|
|
|
/** framework version */
|
2020-05-09 11:34:50 +02:00
|
|
|
|
version: string;
|
2020-05-19 23:29:56 +02:00
|
|
|
|
/** CLI to bootstrap the project */
|
2020-05-09 11:34:50 +02:00
|
|
|
|
generator: string;
|
2020-05-19 23:29:56 +02:00
|
|
|
|
/** Use storybook framework detection */
|
2020-05-09 11:34:50 +02:00
|
|
|
|
autoDetect?: boolean;
|
2020-05-19 23:29:56 +02:00
|
|
|
|
/** Pre-build hook */
|
2020-05-09 11:34:50 +02:00
|
|
|
|
preBuildCommand?: string;
|
|
|
|
|
/** When cli complains when folder already exists */
|
|
|
|
|
ensureDir?: boolean;
|
|
|
|
|
/** Dependencies to add before building Storybook */
|
|
|
|
|
additionalDeps?: string[];
|
2020-05-19 23:29:56 +02:00
|
|
|
|
/** Add typescript dependency and creates a tsconfig.json file */
|
|
|
|
|
typescript?: boolean;
|
2020-05-09 11:34:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Options extends Parameters {
|
|
|
|
|
cwd?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rootDir = path.join(__dirname, '..');
|
|
|
|
|
const siblingDir = path.join(__dirname, '..', '..', 'storybook-e2e-testing');
|
|
|
|
|
|
|
|
|
|
const prepareDirectory = async ({
|
|
|
|
|
cwd,
|
|
|
|
|
ensureDir: ensureDirOption = true,
|
|
|
|
|
}: Options): Promise<boolean> => {
|
|
|
|
|
const siblingExists = await pathExists(siblingDir);
|
|
|
|
|
|
|
|
|
|
if (!siblingExists) {
|
|
|
|
|
await ensureDir(siblingDir);
|
|
|
|
|
await exec('git init', { cwd: siblingDir });
|
|
|
|
|
await exec('npm init -y', { cwd: siblingDir });
|
|
|
|
|
await writeFile(path.join(siblingDir, '.gitignore'), 'node_modules\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cwdExists = await pathExists(cwd);
|
|
|
|
|
|
|
|
|
|
if (cwdExists) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ensureDirOption) {
|
|
|
|
|
await ensureDir(cwd);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const cleanDirectory = async ({ cwd }: Options): Promise<void> => {
|
|
|
|
|
await remove(cwd);
|
|
|
|
|
await remove(path.join(siblingDir, 'node_modules'));
|
|
|
|
|
|
2021-01-08 17:59:17 +01:00
|
|
|
|
if (useYarn2PnP) {
|
2020-06-29 21:20:16 +02:00
|
|
|
|
await shell.rm('-rf', [path.join(siblingDir, '.yarn'), path.join(siblingDir, '.yarnrc.yml')]);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2021-01-08 17:59:17 +01:00
|
|
|
|
const configureYarn2PnP = async ({ cwd }: Options) => {
|
2020-06-29 21:20:16 +02:00
|
|
|
|
const command = [
|
2020-06-30 19:06:19 +02:00
|
|
|
|
`yarn set version berry`,
|
2020-06-29 21:20:16 +02:00
|
|
|
|
// ⚠️ Need to set registry because Yarn 2 is not using the conf of Yarn 1
|
|
|
|
|
`yarn config set npmScopes --json '{ "storybook": { "npmRegistryServer": "http://localhost:6000/" } }'`,
|
|
|
|
|
// Some required magic to be able to fetch deps from local registry
|
|
|
|
|
`yarn config set unsafeHttpWhitelist --json '["localhost"]'`,
|
2020-09-11 12:40:37 +02:00
|
|
|
|
// Disable fallback mode to make sure everything is required correctly
|
|
|
|
|
`yarn config set pnpFallbackMode none`,
|
|
|
|
|
// Add package extensions
|
2020-11-16 13:48:55 +01:00
|
|
|
|
// https://github.com/facebook/create-react-app/pull/9872
|
|
|
|
|
`yarn config set "packageExtensions.react-scripts@*.peerDependencies.react" "*"`,
|
|
|
|
|
`yarn config set "packageExtensions.react-scripts@*.dependencies.@pmmmwh/react-refresh-webpack-plugin" "*"`,
|
2020-06-29 21:20:16 +02:00
|
|
|
|
].join(' && ');
|
|
|
|
|
logger.info(`🎛 Configuring Yarn 2`);
|
|
|
|
|
logger.debug(command);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await exec(command, { cwd });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.error(`🚨 Configuring Yarn 2 failed`);
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
2020-05-09 11:34:50 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const generate = async ({ cwd, name, version, generator }: Options) => {
|
2020-06-29 21:24:33 +02:00
|
|
|
|
let command = generator.replace(/{{name}}/g, name).replace(/{{version}}/g, version);
|
2021-01-08 17:59:17 +01:00
|
|
|
|
if (useYarn2PnP) {
|
2020-06-29 21:24:33 +02:00
|
|
|
|
command = command.replace(/npx/g, `yarn dlx`);
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-09 11:34:50 +02:00
|
|
|
|
logger.info(`🏗 Bootstrapping ${name} project`);
|
|
|
|
|
logger.debug(command);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await exec(command, { cwd });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.error(`🚨 Bootstrapping ${name} failed`);
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const initStorybook = async ({ cwd, autoDetect = true, name }: Options) => {
|
|
|
|
|
logger.info(`🎨 Initializing Storybook with @storybook/cli`);
|
|
|
|
|
try {
|
|
|
|
|
const type = autoDetect ? '' : `--type ${name}`;
|
2020-07-21 21:57:20 +02:00
|
|
|
|
|
|
|
|
|
const sbCLICommand = useLocalSbCli
|
|
|
|
|
? 'node ../../storybook/lib/cli/dist/generate'
|
|
|
|
|
: 'npx -p @storybook/cli sb';
|
|
|
|
|
|
|
|
|
|
await exec(`${sbCLICommand} init --yes ${type}`, { cwd });
|
2020-05-09 11:34:50 +02:00
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.error(`🚨 Storybook initialization failed`);
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const addRequiredDeps = async ({ cwd, additionalDeps }: Options) => {
|
|
|
|
|
logger.info(`🌍 Adding needed deps & installing all deps`);
|
|
|
|
|
try {
|
|
|
|
|
if (additionalDeps && additionalDeps.length > 0) {
|
2020-06-30 18:34:42 +02:00
|
|
|
|
await exec(`yarn add -D ${additionalDeps.join(' ')}`, {
|
2020-05-09 11:34:50 +02:00
|
|
|
|
cwd,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
2020-06-30 18:34:42 +02:00
|
|
|
|
await exec(`yarn install`, {
|
2020-05-09 11:34:50 +02:00
|
|
|
|
cwd,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.error(`🚨 Dependencies installation failed`);
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2020-05-19 23:29:56 +02:00
|
|
|
|
const addTypescript = async ({ cwd }: Options) => {
|
|
|
|
|
logger.info(`👮🏻 Adding typescript and tsconfig.json`);
|
|
|
|
|
try {
|
|
|
|
|
await exec(`yarn add -D typescript@latest`, { cwd });
|
|
|
|
|
const tsConfig = {
|
|
|
|
|
compilerOptions: {
|
|
|
|
|
baseUrl: '.',
|
|
|
|
|
esModuleInterop: true,
|
|
|
|
|
jsx: 'preserve',
|
|
|
|
|
skipLibCheck: true,
|
|
|
|
|
strict: true,
|
|
|
|
|
},
|
|
|
|
|
include: ['src/*'],
|
|
|
|
|
};
|
|
|
|
|
const tsConfigJsonPath = path.resolve(cwd, 'tsconfig.json');
|
|
|
|
|
await writeJSON(tsConfigJsonPath, tsConfig, { encoding: 'utf8', spaces: 2 });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.error(`🚨 Creating tsconfig.json failed`);
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2020-05-09 11:34:50 +02:00
|
|
|
|
const buildStorybook = async ({ cwd, preBuildCommand }: Options) => {
|
|
|
|
|
logger.info(`👷 Building Storybook`);
|
|
|
|
|
try {
|
|
|
|
|
if (preBuildCommand) {
|
|
|
|
|
await exec(preBuildCommand, { cwd });
|
|
|
|
|
}
|
|
|
|
|
await exec(`yarn build-storybook --quiet`, { cwd });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.error(`🚨 Storybook build failed`);
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const serveStorybook = async ({ cwd }: Options, port: string) => {
|
|
|
|
|
const staticDirectory = path.join(cwd, 'storybook-static');
|
|
|
|
|
logger.info(`🌍 Serving ${staticDirectory} on http://localhost:${port}`);
|
|
|
|
|
|
|
|
|
|
return serve(staticDirectory, port);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const runCypress = async ({ name, version }: Options, location: string, open: boolean) => {
|
|
|
|
|
const cypressCommand = open ? 'open' : 'run';
|
|
|
|
|
logger.info(`🤖 Running Cypress tests`);
|
|
|
|
|
try {
|
|
|
|
|
await exec(
|
|
|
|
|
`yarn cypress ${cypressCommand} --config integrationFolder="cypress/generated" --env location="${location}"`,
|
|
|
|
|
{ cwd: rootDir }
|
|
|
|
|
);
|
|
|
|
|
logger.info(`✅ E2E tests success`);
|
|
|
|
|
logger.info(`🎉 Storybook is working great with ${name} ${version}!`);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.error(`🚨 E2E tests fails`);
|
2020-06-25 00:06:09 +02:00
|
|
|
|
logger.info(`🥺 Storybook has some issues with ${name} ${version}!`);
|
2020-05-09 11:34:50 +02:00
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const runTests = async ({ name, version, ...rest }: Parameters) => {
|
|
|
|
|
const options = {
|
|
|
|
|
name,
|
|
|
|
|
version,
|
|
|
|
|
...rest,
|
2020-07-21 22:01:40 +02:00
|
|
|
|
cwd: path.join(siblingDir, `${name}-${version}`),
|
2020-05-09 11:34:50 +02:00
|
|
|
|
};
|
|
|
|
|
|
2020-09-16 11:09:08 +02:00
|
|
|
|
logger.log();
|
2020-05-09 11:34:50 +02:00
|
|
|
|
logger.info(`🏃♀️ Starting for ${name} ${version}`);
|
|
|
|
|
logger.log();
|
|
|
|
|
logger.debug(options);
|
|
|
|
|
logger.log();
|
|
|
|
|
|
|
|
|
|
if (!(await prepareDirectory(options))) {
|
2021-01-08 17:59:17 +01:00
|
|
|
|
if (useYarn2PnP) {
|
|
|
|
|
await configureYarn2PnP({ ...options, cwd: siblingDir });
|
2020-06-29 21:20:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-09 11:34:50 +02:00
|
|
|
|
await generate({ ...options, cwd: siblingDir });
|
|
|
|
|
logger.log();
|
|
|
|
|
|
2020-05-19 23:29:56 +02:00
|
|
|
|
if (options.typescript) {
|
|
|
|
|
await addTypescript(options);
|
|
|
|
|
logger.log();
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-14 11:57:34 +01:00
|
|
|
|
await addRequiredDeps(options);
|
2020-05-09 11:34:50 +02:00
|
|
|
|
logger.log();
|
|
|
|
|
|
2020-11-14 11:57:34 +01:00
|
|
|
|
await initStorybook(options);
|
2020-05-09 11:34:50 +02:00
|
|
|
|
logger.log();
|
|
|
|
|
|
|
|
|
|
await buildStorybook(options);
|
|
|
|
|
logger.log();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const server = await serveStorybook(options, '4000');
|
|
|
|
|
logger.log();
|
|
|
|
|
|
|
|
|
|
let open = false;
|
|
|
|
|
if (!process.env.CI) {
|
|
|
|
|
({ open } = await prompt({
|
|
|
|
|
type: 'confirm',
|
|
|
|
|
name: 'open',
|
|
|
|
|
message: 'Should open cypress?',
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-22 09:02:08 +02:00
|
|
|
|
try {
|
|
|
|
|
await runCypress(options, 'http://localhost:4000', open);
|
|
|
|
|
logger.log();
|
|
|
|
|
} finally {
|
|
|
|
|
server.close();
|
|
|
|
|
}
|
2020-05-09 11:34:50 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Run tests!
|
2020-09-16 11:09:08 +02:00
|
|
|
|
const runE2E = async (parameters: Parameters) => {
|
|
|
|
|
const { name, version } = parameters;
|
|
|
|
|
const cwd = path.join(siblingDir, `${name}-${version}`);
|
|
|
|
|
if (startWithCleanSlate) {
|
|
|
|
|
logger.log();
|
|
|
|
|
logger.info(`♻️ Starting with a clean slate, removing existing ${name} folder`);
|
|
|
|
|
await cleanDirectory({ ...parameters, cwd });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return runTests(parameters)
|
2020-05-09 11:34:50 +02:00
|
|
|
|
.then(async () => {
|
|
|
|
|
if (!process.env.CI) {
|
2020-09-16 11:09:08 +02:00
|
|
|
|
const { cleanup } = await prompt<{ cleanup: boolean }>({
|
2020-05-09 11:34:50 +02:00
|
|
|
|
type: 'confirm',
|
|
|
|
|
name: 'cleanup',
|
|
|
|
|
message: 'Should perform cleanup?',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (cleanup) {
|
|
|
|
|
logger.log();
|
|
|
|
|
logger.info(`🗑 Cleaning ${cwd}`);
|
|
|
|
|
await cleanDirectory({ ...parameters, cwd });
|
|
|
|
|
} else {
|
|
|
|
|
logger.log();
|
|
|
|
|
logger.info(`🚯 No cleanup happened: ${cwd}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((e) => {
|
|
|
|
|
logger.error(`🛑 an error occurred:\n${e}`);
|
|
|
|
|
logger.log();
|
|
|
|
|
logger.error(e);
|
|
|
|
|
logger.log();
|
|
|
|
|
process.exitCode = 1;
|
|
|
|
|
});
|
2020-09-16 11:09:08 +02:00
|
|
|
|
};
|
2020-05-09 11:34:50 +02:00
|
|
|
|
|
2020-09-16 11:09:08 +02:00
|
|
|
|
program.option('--clean', 'Clean up existing projects before running the tests', false);
|
2021-01-08 17:59:17 +01:00
|
|
|
|
program.option('--use-yarn-2-pnp', 'Run tests using Yarn 2 PnP instead of Yarn 1 + npx', false);
|
2020-07-21 21:57:20 +02:00
|
|
|
|
program.option(
|
|
|
|
|
'--use-local-sb-cli',
|
|
|
|
|
'Run tests using local @storybook/cli package (⚠️ Be sure @storybook/cli is properly build as it will not be rebuild before running the tests)',
|
|
|
|
|
false
|
|
|
|
|
);
|
2020-06-29 21:20:16 +02:00
|
|
|
|
program.parse(process.argv);
|
|
|
|
|
|
2021-01-08 17:59:17 +01:00
|
|
|
|
const { useYarn2PnP, useLocalSbCli, clean: startWithCleanSlate, args: frameworkArgs } = program;
|
2020-06-29 21:20:16 +02:00
|
|
|
|
|
2020-05-09 11:34:50 +02:00
|
|
|
|
const typedConfigs: { [key: string]: Parameters } = configs;
|
|
|
|
|
let e2eConfigs: { [key: string]: Parameters } = {};
|
|
|
|
|
|
|
|
|
|
if (frameworkArgs.length > 0) {
|
|
|
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
|
|
|
for (const [framework, version = 'latest'] of frameworkArgs.map((arg) => arg.split('@'))) {
|
2020-06-29 22:29:25 +02:00
|
|
|
|
e2eConfigs[`${framework}-${version}`] = Object.values(typedConfigs).find(
|
|
|
|
|
(c) => c.name === framework && c.version === version
|
|
|
|
|
);
|
2020-05-09 11:34:50 +02:00
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
e2eConfigs = typedConfigs;
|
|
|
|
|
// FIXME: For now Yarn 2 E2E tests must be run by explicitly call `yarn test:e2e-framework yarn2Cra@latest`
|
|
|
|
|
// Because it is telling Yarn to use version 2
|
2020-06-30 20:36:31 +02:00
|
|
|
|
delete e2eConfigs.yarn_2_cra;
|
2020-10-12 18:47:56 +02:00
|
|
|
|
|
2020-10-12 22:24:34 +02:00
|
|
|
|
// CRA Bench is a special case of E2E tests, it requires Node 12 as `@storybook/bench` is using `@hapi/hapi@19.2.0`
|
|
|
|
|
// which itself need Node 12.
|
|
|
|
|
delete e2eConfigs.cra_bench;
|
2020-05-09 11:34:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const perform = () => {
|
|
|
|
|
const limit = pLimit(1);
|
|
|
|
|
const narrowedConfigs = Object.values(e2eConfigs);
|
2020-09-19 19:17:43 +02:00
|
|
|
|
const list = filterDataForCurrentCircleCINode(narrowedConfigs) as Parameters[];
|
|
|
|
|
|
|
|
|
|
logger.info(`📑 Will run E2E tests for:${list.map((c) => c.name).join(', ')}`);
|
2020-05-09 11:34:50 +02:00
|
|
|
|
|
|
|
|
|
return Promise.all(list.map((config) => limit(() => runE2E(config))));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
perform().then(() => {
|
|
|
|
|
process.exit(process.exitCode || 0);
|
|
|
|
|
});
|