storybook/scripts/run-e2e.ts

317 lines
8.7 KiB
TypeScript
Raw Normal View History

2020-05-09 11:34:50 +02:00
import path from 'path';
import { ensureDir, pathExists, remove } from 'fs-extra';
import prompts from 'prompts';
2020-05-09 11:34:50 +02:00
import program from 'commander';
2020-05-09 11:34:50 +02:00
import { serve } from './utils/serve';
2020-05-18 14:58:55 +02:00
// @ts-ignore
import { filterDataForCurrentCircleCINode } from './utils/concurrency';
2020-05-09 11:34:50 +02:00
import * as configs from '../lib/cli/src/repro-generators/configs';
import { Parameters } from '../lib/cli/src/repro-generators/configs';
import { exec } from '../lib/cli/src/repro-generators/scripts';
2020-05-09 11:34:50 +02:00
const logger = console;
export interface Options {
/** CLI repro template to use */
2020-05-09 11:34:50 +02:00
name: string;
2020-05-19 23:29:56 +02:00
/** Pre-build hook */
2020-05-09 11:34:50 +02:00
ensureDir?: boolean;
cwd?: string;
}
const rootDir = path.join(__dirname, '..');
const siblingDir = path.join(__dirname, '..', '..', 'storybook-e2e-testing');
const prepareDirectory = async ({ cwd }: Options): Promise<boolean> => {
2020-05-09 11:34:50 +02:00
const siblingExists = await pathExists(siblingDir);
if (!siblingExists) {
await ensureDir(siblingDir);
}
return pathExists(cwd);
2020-05-09 11:34:50 +02:00
};
const cleanDirectory = async ({ cwd }: Options): Promise<void> => {
await remove(cwd);
2021-02-15 17:45:08 +01:00
};
2020-05-09 11:34:50 +02:00
const buildStorybook = async ({ cwd }: Options) => {
await exec(
`yarn build-storybook --quiet`,
{ cwd, silent: false },
{ startMessage: `👷 Building Storybook`, errorMessage: `🚨 Storybook build failed` }
);
2020-05-09 11:34:50 +02:00
};
const serveStorybook = async ({ cwd }: Options, port: string) => {
const staticDirectory = path.join(cwd, 'storybook-static');
logger.info(`🌍 Serving ${staticDirectory} on http://localhost:${port}`);
2020-05-09 11:34:50 +02:00
return serve(staticDirectory, port);
};
2021-05-18 18:08:19 +02:00
const runCypress = async (location: string) => {
const cypressCommand = openCypressInUIMode ? 'open' : 'run';
await exec(
`yarn cypress ${cypressCommand} --config pageLoadTimeout=4000,execTimeout=4000,taskTimeout=4000,responseTimeout=4000,integrationFolder="cypress/generated" --env location="${location}"`,
{ cwd: rootDir },
{
startMessage: `🤖 Running Cypress tests`,
errorMessage: `🚨 E2E tests fails`,
}
);
2020-05-09 11:34:50 +02:00
};
const runTests = async ({ name, ...rest }: Parameters) => {
2020-05-09 11:34:50 +02:00
const options = {
name,
...rest,
cwd: path.join(siblingDir, `${name}`),
2020-05-09 11:34:50 +02:00
};
logger.log();
logger.info(`🏃Starting for ${name}`);
2020-05-09 11:34:50 +02:00
logger.log();
logger.debug(options);
logger.log();
if (!(await prepareDirectory(options))) {
// Call repro cli
const sbCLICommand = useLocalSbCli
? 'node ../storybook/lib/cli/bin repro'
: // Need to use npx because at this time we don't have Yarn 2 installed
'npx -p @storybook/cli sb repro';
2020-05-19 23:29:56 +02:00
2021-05-10 15:08:37 +08:00
const targetFolder = path.join(siblingDir, `${name}`);
const commandArgs = [
targetFolder,
`--framework ${options.framework}`,
`--template ${options.name}`,
'--e2e',
];
if (pnp) {
commandArgs.push('--pnp');
}
2020-05-09 11:34:50 +02:00
2021-05-10 15:08:37 +08:00
const command = `${sbCLICommand} ${commandArgs.join(' ')}`;
await exec(
command,
{ cwd: siblingDir, silent: false },
{
startMessage: `👷 Bootstrapping ${options.framework} project`,
errorMessage: `🚨 Unable to bootstrap project`,
}
);
2020-05-09 11:34:50 +02:00
await buildStorybook(options);
logger.log();
2020-05-09 11:34:50 +02:00
}
const server = await serveStorybook(options, '4000');
logger.log();
try {
2021-05-18 18:08:19 +02:00
await runCypress('http://localhost:4000');
logger.info(`🎉 Storybook is working great with ${name}!`);
} catch (e) {
logger.info(`🥺 Storybook has some issues with ${name}!`);
throw e;
} finally {
server.close();
}
2020-05-09 11:34:50 +02:00
};
async function postE2ECleanup(cwd: string, parameters: Parameters) {
if (!process.env.CI) {
const { cleanup } = await prompts({
type: 'toggle',
name: 'cleanup',
message: 'Should perform cleanup?',
initial: false,
active: 'yes',
inactive: 'no',
});
if (cleanup) {
logger.log();
logger.info(`🗑 Cleaning ${cwd}`);
await cleanDirectory({ ...parameters, cwd });
} else {
logger.log();
logger.info(`🚯 No cleanup happened: ${cwd}`);
}
}
}
async function preE2ECleanup(name: string, parameters: Parameters, cwd: string) {
if (startWithCleanSlate) {
logger.log();
logger.info(`♻️ Starting with a clean slate, removing existing ${name} folder`);
await cleanDirectory({ ...parameters, cwd });
}
}
/**
* Execute E2E for input parameters and return true is everything is ok, false
* otherwise.
* @param parameters
*/
const runE2E = async (parameters: Parameters): Promise<boolean> => {
const { name } = parameters;
const cwd = path.join(siblingDir, `${name}`);
return preE2ECleanup(name, parameters, cwd)
.then(() => runTests(parameters))
.then(() => postE2ECleanup(cwd, parameters))
.then(() => true)
2020-05-09 11:34:50 +02:00
.catch((e) => {
logger.error(`🛑 an error occurred:`);
2020-05-09 11:34:50 +02:00
logger.error(e);
logger.log();
process.exitCode = 1;
return false;
2020-05-09 11:34:50 +02:00
});
};
2020-05-09 11:34:50 +02:00
program
.option('--clean', 'Clean up existing projects before running the tests', false)
.option('--pnp', 'Run tests using Yarn 2 PnP instead of Yarn 1 + npx', false)
.option(
'--use-local-sb-cli',
'Run tests using local @storybook/cli package (⚠️ Be sure @storybook/cli is properly built as it will not be rebuilt before running the tests)',
false
)
.option(
'--skip <value>',
'Skip a framework, can be used multiple times "--skip angular@latest --skip preact"',
(value, previous) => previous.concat([value]),
[]
)
.option('--all', `run e2e tests for every framework`, false);
program.parse(process.argv);
type ProgramOptions = {
all?: boolean;
pnp?: boolean;
useLocalSbCli?: boolean;
clean?: boolean;
args?: string[];
skip?: string[];
};
const {
all: shouldRunAllFrameworks,
args: frameworkArgs,
skip: frameworksToSkip,
}: ProgramOptions = program;
let { pnp, useLocalSbCli, clean: startWithCleanSlate }: ProgramOptions = program;
2020-05-09 11:34:50 +02:00
const typedConfigs: { [key: string]: Parameters } = configs;
let e2eConfigs: { [key: string]: Parameters } = {};
2020-05-09 11:34:50 +02:00
let openCypressInUIMode = !process.env.CI;
const getConfig = async () => {
if (shouldRunAllFrameworks) {
logger.info(`📑 Running test for ALL frameworks`);
Object.values(typedConfigs).forEach((config) => {
e2eConfigs[`${config.name}-${config.version}`] = config;
});
}
// Compute the list of frameworks we will run E2E for
if (frameworkArgs.length > 0) {
frameworkArgs.forEach((framework) => {
e2eConfigs[framework] = Object.values(typedConfigs).find((c) => c.name === framework);
});
} else {
const selectedValues = await prompts([
{
type: 'toggle',
name: 'openCypressInUIMode',
message: 'Open cypress in UI mode',
initial: false,
active: 'yes',
inactive: 'no',
},
{
type: 'toggle',
name: 'useLocalSbCli',
message: 'Use local Storybook CLI',
initial: false,
active: 'yes',
inactive: 'no',
},
{
type: 'autocompleteMultiselect',
message: 'Select the frameworks to run',
name: 'frameworks',
hint:
'You can also run directly with package name like `test:e2e-framework react`, or `yarn test:e2e-framework --all` for all packages!',
choices: Object.keys(configs).map((key) => {
// @ts-ignore
const { name, version } = configs[key];
return {
// @ts-ignore
value: configs[key],
title: `${name}@${version}`,
selected: false,
};
}),
},
]);
if (!selectedValues.frameworks) {
logger.info(`No framework was selected.`);
process.exit(process.exitCode || 0);
}
useLocalSbCli = selectedValues.useLocalSbCli;
openCypressInUIMode = selectedValues.openCypressInUIMode;
e2eConfigs = selectedValues.frameworks;
}
// Remove frameworks listed with `--skip` arg
frameworksToSkip.forEach((framework) => {
delete e2eConfigs[framework];
});
};
2020-05-09 11:34:50 +02:00
const perform = async (): Promise<Record<string, boolean>> => {
await getConfig();
2020-05-09 11:34:50 +02:00
const narrowedConfigs = Object.values(e2eConfigs);
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
const e2eResult: Record<string, boolean> = {};
// Run all e2e tests one after another and fill result map
await list.reduce(
(previousValue, config) =>
previousValue
.then(() => runE2E(config))
.then((result) => {
e2eResult[config.name] = result;
}),
Promise.resolve()
);
return e2eResult;
2020-05-09 11:34:50 +02:00
};
perform().then((e2eResult) => {
logger.info(`🧮 E2E Results`);
Object.entries(e2eResult).forEach(([configName, result]) => {
logger.info(`${configName}: ${result ? 'OK' : 'KO'}`);
});
2020-05-09 11:34:50 +02:00
process.exit(process.exitCode || 0);
});