storybook/scripts/run-e2e.ts

362 lines
9.9 KiB
TypeScript
Raw Normal View History

2020-05-09 11:34:50 +02:00
/* eslint-disable no-irregular-whitespace */
import path from 'path';
2020-05-19 10:12:51 +02:00
import { remove, ensureDir, pathExists, writeFile, readJSON, 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';
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
import { listOfPackages } from './utils/list-packages';
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'));
if (useYarn2) {
await shell.rm('-rf', [path.join(siblingDir, '.yarn'), path.join(siblingDir, '.yarnrc.yml')]);
}
};
const configureYarn2 = async ({ cwd }: Options) => {
const command = [
`yarn set version 2`,
// ⚠️ 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"]'`,
].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) => {
let command = generator.replace(/{{name}}/g, name).replace(/{{version}}/g, version);
if (useYarn2) {
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}`;
await exec(`npx -p @storybook/cli sb init --yes ${type}`, { cwd });
2020-05-09 11:34:50 +02:00
} catch (e) {
logger.error(`🚨Storybook initialization failed`);
throw e;
}
};
2020-05-18 23:52:29 +02:00
// Verdaccio doesn't resolve *
// So we set resolutions manually in package.json
2020-05-19 10:12:51 +02:00
const setResolutions = async ({ cwd }: Options) => {
logger.info(`🔒Setting yarn resolutions`);
2020-05-18 15:10:08 +02:00
const packages = await listOfPackages();
2020-05-18 22:29:50 +02:00
2020-05-19 10:12:51 +02:00
const packageJsonPath = path.resolve(cwd, 'package.json');
2020-05-19 09:41:56 +02:00
const packageJson = await readJSON(packageJsonPath, { encoding: 'utf8' });
2020-05-18 23:52:29 +02:00
packageJson.resolutions = {
...packageJson.resolutions,
...packages.reduce(
(acc, { name, version }) => ({
...acc,
[name]: version,
}),
{}
),
};
2020-05-19 10:12:51 +02:00
await writeJSON(packageJsonPath, packageJson, { encoding: 'utf8', spaces: 2 });
2020-05-18 14:58:55 +02:00
};
2020-05-09 11:34:50 +02:00
const addRequiredDeps = async ({ cwd, additionalDeps }: Options) => {
logger.info(`🌍Adding needed deps & installing all deps`);
try {
if (additionalDeps && additionalDeps.length > 0) {
await exec(`yarn add -D ${additionalDeps.join(' ')} --silent`, {
cwd,
});
} else {
await exec(`yarn install --silent`, {
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,
cwd: path.join(siblingDir, `${name}-v${version}`),
};
logger.info(`🏃Starting for ${name} ${version}`);
logger.log();
logger.debug(options);
logger.log();
if (!(await prepareDirectory(options))) {
if (useYarn2) {
await configureYarn2({ ...options, cwd: siblingDir });
}
2020-05-09 11:34:50 +02:00
await generate({ ...options, cwd: siblingDir });
logger.log();
2020-06-05 23:10:19 +02:00
await setResolutions(options);
logger.log();
2020-05-19 23:29:56 +02:00
if (options.typescript) {
await addTypescript(options);
logger.log();
}
2020-05-09 11:34:50 +02:00
await initStorybook(options);
logger.log();
await addRequiredDeps(options);
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?',
}));
}
try {
await runCypress(options, 'http://localhost:4000', open);
logger.log();
} finally {
server.close();
}
2020-05-09 11:34:50 +02:00
};
// Run tests!
const runE2E = (parameters: Parameters) =>
runTests(parameters)
.then(async () => {
if (!process.env.CI) {
const { name, version } = parameters;
const cwd = path.join(siblingDir, `${name}-v${version}`);
const { cleanup } = await prompt({
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;
});
program.option('--use-yarn-2', 'Run tests using Yarn 2 instead of Yarn 1 + npx', false);
program.parse(process.argv);
const { useYarn2, args: frameworkArgs } = program;
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
delete e2eConfigs.yarn_2_cra;
2020-05-09 11:34:50 +02:00
}
const perform = () => {
const limit = pLimit(1);
const narrowedConfigs = Object.values(e2eConfigs);
const nodeIndex = +process.env.CIRCLE_NODE_INDEX || 0;
const numberOfNodes = +process.env.CIRCLE_NODE_TOTAL || 1;
const list = narrowedConfigs.filter((_, index) => {
return index % numberOfNodes === nodeIndex;
});
logger.info(
`📑Assigning jobs ${list
.map((c) => c.name)
.join(', ')} to node ${nodeIndex} (on ${numberOfNodes})`
);
2020-05-09 11:34:50 +02:00
return Promise.all(list.map((config) => limit(() => runE2E(config))));
};
perform().then(() => {
process.exit(process.exitCode || 0);
});