mirror of
https://github.com/storybookjs/storybook.git
synced 2025-03-31 05:03:21 +08:00
Merge branch 'next' into 14118-update-react-docgen-typescript-plugin
This commit is contained in:
commit
f2116f29b2
@ -110,10 +110,11 @@ program
|
||||
);
|
||||
|
||||
program
|
||||
.command('link <repro-url>')
|
||||
.description('Pull down a repro from a URL, link it, and run storybook')
|
||||
.action((reproUrl) =>
|
||||
link({ reproUrl }).catch((e) => {
|
||||
.command('link <repo-url-or-directory>')
|
||||
.description('Pull down a repro from a URL (or a local directory), link it, and run storybook')
|
||||
.option('--local', 'Link a local directory already in your file system')
|
||||
.action((target, { local }) =>
|
||||
link({ target, local }).catch((e) => {
|
||||
logger.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
|
@ -1,14 +1,16 @@
|
||||
import fse from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { sync as spawnSync } from 'cross-spawn';
|
||||
import { logger } from '@storybook/node-logger';
|
||||
import { exec } from './repro-generators/scripts';
|
||||
|
||||
interface LinkOptions {
|
||||
reproUrl: string;
|
||||
target: string;
|
||||
local?: boolean;
|
||||
}
|
||||
|
||||
export const link = async ({ reproUrl }: LinkOptions) => {
|
||||
const storybookDirectory = process.cwd();
|
||||
export const link = async ({ target, local }: LinkOptions) => {
|
||||
const storybookDir = process.cwd();
|
||||
try {
|
||||
const packageJson = JSON.parse(fse.readFileSync('package.json', 'utf8'));
|
||||
if (packageJson.name !== '@storybook/root') throw new Error();
|
||||
@ -16,28 +18,46 @@ export const link = async ({ reproUrl }: LinkOptions) => {
|
||||
throw new Error('Expected to run link from the root of the storybook monorepo');
|
||||
}
|
||||
|
||||
const reprosDirectory = path.join(storybookDirectory, '../storybook-repros');
|
||||
logger.info(`Ensuring directory ${reprosDirectory}`);
|
||||
fse.ensureDirSync(reprosDirectory);
|
||||
let reproDir = target;
|
||||
let reproName = path.basename(target);
|
||||
|
||||
logger.info(`Cloning ${reproUrl}`);
|
||||
await exec(`git clone ${reproUrl}`, { cwd: reprosDirectory });
|
||||
// Extract a repro name from url given as input (take the last part of the path and remove the extension)
|
||||
const reproName = path.basename(reproUrl, path.extname(reproUrl));
|
||||
const repro = path.join(reprosDirectory, reproName);
|
||||
if (!local) {
|
||||
const reprosDir = path.join(storybookDir, '../storybook-repros');
|
||||
logger.info(`Ensuring directory ${reprosDir}`);
|
||||
fse.ensureDirSync(reprosDir);
|
||||
|
||||
logger.info(`Linking ${repro}`);
|
||||
await exec(`yarn link --all ${storybookDirectory}`, { cwd: repro });
|
||||
logger.info(`Cloning ${target}`);
|
||||
await exec(`git clone ${target}`, { cwd: reprosDir });
|
||||
// Extract a repro name from url given as input (take the last part of the path and remove the extension)
|
||||
reproName = path.basename(target, path.extname(target));
|
||||
reproDir = path.join(reprosDir, reproName);
|
||||
}
|
||||
|
||||
const version = spawnSync('yarn', ['--version'], {
|
||||
cwd: reproDir,
|
||||
stdio: 'pipe',
|
||||
}).stdout.toString();
|
||||
|
||||
if (!version.startsWith('2.')) {
|
||||
logger.warn(`🚨 Expected yarn 2 in ${reproDir}!`);
|
||||
logger.warn('');
|
||||
logger.warn('Please set it up with `yarn set version berry`,');
|
||||
logger.warn(`then link '${reproDir}' with the '--local' flag.`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Linking ${reproDir}`);
|
||||
await exec(`yarn link --all ${storybookDir}`, { cwd: reproDir });
|
||||
|
||||
logger.info(`Installing ${reproName}`);
|
||||
await exec(`yarn install`, { cwd: repro });
|
||||
await exec(`yarn install`, { cwd: reproDir });
|
||||
|
||||
// ⚠️ TODO: Fix peer deps in `@storybook/preset-create-react-app`
|
||||
logger.info(
|
||||
`Magic stuff related to @storybook/preset-create-react-app, we need to fix peerDependencies`
|
||||
);
|
||||
await exec(`yarn add -D webpack-hot-middleware`, { cwd: repro });
|
||||
await exec(`yarn add -D webpack-hot-middleware`, { cwd: reproDir });
|
||||
|
||||
logger.info(`Running ${reproName} storybook`);
|
||||
await exec(`yarn run storybook`, { cwd: repro });
|
||||
await exec(`yarn run storybook`, { cwd: reproDir });
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import React, { FC, ChangeEvent, useCallback, useState } from 'react';
|
||||
import { styled } from '@storybook/theming';
|
||||
|
||||
import { Form } from '../form';
|
||||
import { getControlId } from './helpers';
|
||||
import { ControlProps, ArrayValue, ArrayConfig } from './types';
|
||||
|
||||
const parse = (value: string, separator: string): ArrayValue =>
|
||||
@ -45,7 +46,7 @@ export const ArrayControl: FC<ArrayProps> = ({
|
||||
return (
|
||||
<Wrapper>
|
||||
<Form.Textarea
|
||||
id={name}
|
||||
id={getControlId(name)}
|
||||
value={format(value, separator)}
|
||||
onChange={handleChange}
|
||||
size="flex"
|
||||
|
@ -3,6 +3,7 @@ import React, { FC, useCallback } from 'react';
|
||||
import { opacify, transparentize } from 'polished';
|
||||
import { styled } from '@storybook/theming';
|
||||
|
||||
import { getControlId } from './helpers';
|
||||
import { Form } from '../form';
|
||||
import { ControlProps, BooleanValue, BooleanConfig } from './types';
|
||||
|
||||
@ -90,7 +91,7 @@ export const BooleanControl: FC<BooleanProps> = ({ name, value, onChange, onBlur
|
||||
return (
|
||||
<Label htmlFor={name} title={value ? 'Change to false' : 'Change to true'}>
|
||||
<input
|
||||
id={name}
|
||||
id={getControlId(name)}
|
||||
type="checkbox"
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
checked={value || false}
|
||||
|
@ -9,6 +9,7 @@ import { TooltipNote } from '../tooltip/TooltipNote';
|
||||
import { WithTooltip } from '../tooltip/lazy-WithTooltip';
|
||||
import { Form } from '../form';
|
||||
import { Icons } from '../icon/icon';
|
||||
import { getControlId } from './helpers';
|
||||
|
||||
const Wrapper = styled.div({
|
||||
position: 'relative',
|
||||
@ -274,6 +275,7 @@ const usePresets = (
|
||||
|
||||
export type ColorProps = ControlProps<ColorValue> & ColorConfig;
|
||||
export const ColorControl: FC<ColorProps> = ({
|
||||
name,
|
||||
value: initialValue,
|
||||
onChange,
|
||||
onFocus,
|
||||
@ -325,6 +327,7 @@ export const ColorControl: FC<ColorProps> = ({
|
||||
<Swatch value={realValue} style={{ margin: 4 }} />
|
||||
</PickerTooltip>
|
||||
<Input
|
||||
id={getControlId(name)}
|
||||
value={value}
|
||||
onChange={(e: any) => updateValue(e.target.value)}
|
||||
onFocus={(e) => e.target.select()}
|
||||
|
@ -3,6 +3,7 @@ import { styled } from '@storybook/theming';
|
||||
|
||||
import { Form } from '../form';
|
||||
import { ControlProps, DateValue, DateConfig } from './types';
|
||||
import { getControlId } from './helpers';
|
||||
|
||||
const parseDate = (value: string) => {
|
||||
const [year, month, day] = value.split('-');
|
||||
@ -89,21 +90,23 @@ export const DateControl: FC<DateProps> = ({ name, value, onChange, onFocus, onB
|
||||
setValid(!!time);
|
||||
};
|
||||
|
||||
const controlId = getControlId(name);
|
||||
|
||||
return (
|
||||
<FlexSpaced>
|
||||
<Form.Input
|
||||
type="date"
|
||||
max="9999-12-31" // I do this because of a rendering bug in chrome
|
||||
ref={dateRef as RefObject<HTMLInputElement>}
|
||||
id={`${name}date`}
|
||||
name={`${name}date`}
|
||||
id={`${controlId}-date`}
|
||||
name={`${controlId}-date`}
|
||||
onChange={onDateChange}
|
||||
{...{ onFocus, onBlur }}
|
||||
/>
|
||||
<Form.Input
|
||||
type="time"
|
||||
id={`${name}time`}
|
||||
name={`${name}time`}
|
||||
id={`${controlId}-time`}
|
||||
name={`${controlId}-time`}
|
||||
ref={timeRef as RefObject<HTMLInputElement>}
|
||||
onChange={onTimeChange}
|
||||
{...{ onFocus, onBlur }}
|
||||
|
@ -3,6 +3,7 @@ import { styled } from '@storybook/theming';
|
||||
import { ControlProps } from './types';
|
||||
|
||||
import { Form } from '../form';
|
||||
import { getControlId } from './helpers';
|
||||
|
||||
export interface FilesControlProps extends ControlProps<string[]> {
|
||||
accept?: string;
|
||||
@ -37,6 +38,7 @@ export const FilesControl: FunctionComponent<FilesControlProps> = ({
|
||||
|
||||
return (
|
||||
<FileInput
|
||||
id={getControlId(name)}
|
||||
type="file"
|
||||
name={name}
|
||||
multiple
|
||||
|
@ -2,6 +2,7 @@ import React, { FC, ChangeEvent, useState, useCallback, useEffect, useRef } from
|
||||
import { styled } from '@storybook/theming';
|
||||
|
||||
import { Form } from '../form';
|
||||
import { getControlId } from './helpers';
|
||||
import { ControlProps, NumberValue, NumberConfig } from './types';
|
||||
|
||||
const Wrapper = styled.label({
|
||||
@ -65,6 +66,7 @@ export const NumberControl: FC<NumberProps> = ({
|
||||
<Wrapper>
|
||||
<Form.Input
|
||||
ref={htmlElRef}
|
||||
id={getControlId(name)}
|
||||
type="number"
|
||||
onChange={handleChange}
|
||||
size="flex"
|
||||
|
@ -13,6 +13,7 @@ import { styled, useTheme, Theme } from '@storybook/theming';
|
||||
|
||||
// @ts-ignore
|
||||
import { JsonTree, getObjectType } from './react-editable-json-tree';
|
||||
import { getControlId } from './helpers';
|
||||
import type { ControlProps, ObjectValue, ObjectConfig } from './types';
|
||||
import { Form } from '../form';
|
||||
import { Icons, IconsProps } from '../icon/icon';
|
||||
@ -274,7 +275,7 @@ export const ObjectControl: React.FC<ObjectProps> = ({ name, value, onChange })
|
||||
const rawJSONForm = (
|
||||
<RawInput
|
||||
ref={htmlElRef}
|
||||
id={name}
|
||||
id={getControlId(name)}
|
||||
name={name}
|
||||
defaultValue={value === null ? '' : JSON.stringify(value, null, 2)}
|
||||
onBlur={(event) => updateRaw(event.target.value)}
|
||||
|
@ -2,6 +2,7 @@ import React, { FC, ChangeEvent } from 'react';
|
||||
|
||||
import { styled } from '@storybook/theming';
|
||||
import { lighten, darken, rgba } from 'polished';
|
||||
import { getControlId } from './helpers';
|
||||
import { ControlProps, NumberValue, RangeConfig } from './types';
|
||||
import { parse } from './Number';
|
||||
|
||||
@ -169,6 +170,7 @@ export const RangeControl: FC<RangeProps> = ({
|
||||
<RangeWrapper>
|
||||
<RangeLabel>{min}</RangeLabel>
|
||||
<RangeInput
|
||||
id={getControlId(name)}
|
||||
type="range"
|
||||
onChange={handleChange}
|
||||
{...{ name, value, min, max, step, onFocus, onBlur }}
|
||||
|
@ -2,6 +2,7 @@ import React, { FC, ChangeEvent, useCallback, useState } from 'react';
|
||||
import { styled } from '@storybook/theming';
|
||||
|
||||
import { Form } from '../form';
|
||||
import { getControlId } from './helpers';
|
||||
import { ControlProps, TextValue, TextConfig } from './types';
|
||||
|
||||
export type TextProps = ControlProps<TextValue | undefined> & TextConfig;
|
||||
@ -14,7 +15,7 @@ export const TextControl: FC<TextProps> = ({ name, value, onChange, onFocus, onB
|
||||
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onChange(event.target.value);
|
||||
};
|
||||
|
||||
|
||||
const [forceVisible, setForceVisible] = useState(false);
|
||||
const onForceVisible = useCallback(() => {
|
||||
onChange('');
|
||||
@ -28,7 +29,7 @@ export const TextControl: FC<TextProps> = ({ name, value, onChange, onFocus, onB
|
||||
return (
|
||||
<Wrapper>
|
||||
<Form.Textarea
|
||||
id={name}
|
||||
id={getControlId(name)}
|
||||
onChange={handleChange}
|
||||
size="flex"
|
||||
placeholder="Edit string..."
|
||||
|
12
lib/components/src/controls/helpers.test.ts
Normal file
12
lib/components/src/controls/helpers.test.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { getControlId } from './helpers';
|
||||
|
||||
describe('getControlId', () => {
|
||||
it.each([
|
||||
// caseName, input, expected
|
||||
['lower case', 'some-id', 'control-some-id'],
|
||||
['upper case', 'SOME-ID', 'control-SOME-ID'],
|
||||
['all valid characters', 'some_weird-:custom.id', 'control-some_weird-:custom.id'],
|
||||
])('%s', (a, input, expected) => {
|
||||
expect(getControlId(input)).toBe(expected);
|
||||
});
|
||||
});
|
7
lib/components/src/controls/helpers.ts
Normal file
7
lib/components/src/controls/helpers.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Adds `control` prefix to make ID attribute more specific.
|
||||
* Removes spaces because spaces are not allowed in ID attributes
|
||||
* @link http://xahlee.info/js/html_allowed_chars_in_attribute.html
|
||||
* @example getControlId('my prop name') -> 'control-my-prop-name'
|
||||
*/
|
||||
export const getControlId = (value: string) => `control-${value.replace(/\s+/g, '-')}`;
|
@ -3,6 +3,7 @@ import { styled } from '@storybook/theming';
|
||||
import { logger } from '@storybook/client-logger';
|
||||
import { ControlProps, OptionsMultiSelection, NormalizedOptionsConfig } from '../types';
|
||||
import { selectedKeys, selectedValues } from './helpers';
|
||||
import { getControlId } from '../helpers';
|
||||
|
||||
const Wrapper = styled.div<{ isInline: boolean }>(({ isInline }) =>
|
||||
isInline
|
||||
@ -69,10 +70,12 @@ export const CheckboxControl: FC<CheckboxProps> = ({
|
||||
setSelected(updated);
|
||||
};
|
||||
|
||||
const controlId = getControlId(name);
|
||||
|
||||
return (
|
||||
<Wrapper isInline={isInline}>
|
||||
{Object.keys(options).map((key) => {
|
||||
const id = `${name}-${key}`;
|
||||
{Object.keys(options).map((key, index) => {
|
||||
const id = `${controlId}-${index}`;
|
||||
return (
|
||||
<Label key={id} htmlFor={id}>
|
||||
<input
|
||||
|
@ -3,6 +3,7 @@ import { styled } from '@storybook/theming';
|
||||
import { logger } from '@storybook/client-logger';
|
||||
import { ControlProps, OptionsSingleSelection, NormalizedOptionsConfig } from '../types';
|
||||
import { selectedKey } from './helpers';
|
||||
import { getControlId } from '../helpers';
|
||||
|
||||
const Wrapper = styled.div<{ isInline: boolean }>(({ isInline }) =>
|
||||
isInline
|
||||
@ -54,10 +55,12 @@ export const RadioControl: FC<RadioProps> = ({ name, options, value, onChange, i
|
||||
return <>-</>;
|
||||
}
|
||||
const selection = selectedKey(value, options);
|
||||
const controlId = getControlId(name);
|
||||
|
||||
return (
|
||||
<Wrapper isInline={isInline}>
|
||||
{Object.keys(options).map((key) => {
|
||||
const id = `${name}-${key}`;
|
||||
{Object.keys(options).map((key, index) => {
|
||||
const id = `${controlId}-${index}`;
|
||||
return (
|
||||
<Label key={id} htmlFor={id}>
|
||||
<input
|
||||
|
@ -4,6 +4,7 @@ import { logger } from '@storybook/client-logger';
|
||||
import { ControlProps, OptionsSelection, NormalizedOptionsConfig } from '../types';
|
||||
import { selectedKey, selectedKeys, selectedValues } from './helpers';
|
||||
import { Icons } from '../../icon/icon';
|
||||
import { getControlId } from '../helpers';
|
||||
|
||||
const styleResets: CSSObject = {
|
||||
// resets
|
||||
@ -94,11 +95,12 @@ const SingleSelect: FC<SelectProps> = ({ name, value, options, onChange }) => {
|
||||
onChange(options[e.currentTarget.value]);
|
||||
};
|
||||
const selection = selectedKey(value, options) || NO_SELECTION;
|
||||
const controlId = getControlId(name);
|
||||
|
||||
return (
|
||||
<SelectWrapper>
|
||||
<Icons icon="arrowdown" />
|
||||
<OptionsSelect value={selection} onChange={handleChange}>
|
||||
<OptionsSelect id={controlId} value={selection} onChange={handleChange}>
|
||||
<option key="no-selection" disabled>
|
||||
{NO_SELECTION}
|
||||
</option>
|
||||
@ -118,10 +120,11 @@ const MultiSelect: FC<SelectProps> = ({ name, value, options, onChange }) => {
|
||||
onChange(selectedValues(selection, options));
|
||||
};
|
||||
const selection = selectedKeys(value, options);
|
||||
const controlId = getControlId(name);
|
||||
|
||||
return (
|
||||
<SelectWrapper>
|
||||
<OptionsSelect multiple value={selection} onChange={handleChange}>
|
||||
<OptionsSelect id={controlId} multiple value={selection} onChange={handleChange}>
|
||||
{Object.keys(options).map((key) => (
|
||||
<option key={key}>{key}</option>
|
||||
))}
|
||||
|
@ -186,7 +186,7 @@
|
||||
"@types/lodash": "^4.14.167",
|
||||
"@types/node": "^14.14.20",
|
||||
"@types/node-cleanup": "^2.1.1",
|
||||
"@types/prompts": "^2.0.9",
|
||||
"@types/prompts": "2.0.11",
|
||||
"@types/semver": "^7.3.4",
|
||||
"@types/serve-static": "^1.13.8",
|
||||
"@types/shelljs": "^0.8.7",
|
||||
@ -211,7 +211,6 @@
|
||||
"danger": "^10.6.2",
|
||||
"detect-port": "^1.3.0",
|
||||
"downlevel-dts": "^0.6.0",
|
||||
"enquirer": "^2.3.6",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.5",
|
||||
"eslint": "^7.17.0",
|
||||
|
@ -1,17 +1,15 @@
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
import path from 'path';
|
||||
import { remove, ensureDir, pathExists } from 'fs-extra';
|
||||
import { prompt } from 'enquirer';
|
||||
import pLimit from 'p-limit';
|
||||
import { ensureDir, pathExists, remove } from 'fs-extra';
|
||||
import prompts from 'prompts';
|
||||
|
||||
import program from 'commander';
|
||||
import { serve } from './utils/serve';
|
||||
import { exec } from './utils/command';
|
||||
// @ts-ignore
|
||||
import { filterDataForCurrentCircleCINode } from './utils/concurrency';
|
||||
|
||||
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';
|
||||
|
||||
const logger = console;
|
||||
|
||||
@ -33,13 +31,7 @@ const prepareDirectory = async ({ cwd }: Options): Promise<boolean> => {
|
||||
await ensureDir(siblingDir);
|
||||
}
|
||||
|
||||
const cwdExists = await pathExists(cwd);
|
||||
|
||||
if (cwdExists) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return pathExists(cwd);
|
||||
};
|
||||
|
||||
const cleanDirectory = async ({ cwd }: Options): Promise<void> => {
|
||||
@ -47,37 +39,30 @@ const cleanDirectory = async ({ cwd }: Options): Promise<void> => {
|
||||
};
|
||||
|
||||
const buildStorybook = async ({ cwd }: Options) => {
|
||||
logger.info(`👷 Building Storybook`);
|
||||
try {
|
||||
await exec(`yarn build-storybook --quiet`, { cwd });
|
||||
} catch (e) {
|
||||
logger.error(`🚨 Storybook build failed`);
|
||||
throw e;
|
||||
}
|
||||
await exec(
|
||||
`yarn build-storybook --quiet`,
|
||||
{ cwd, silent: false },
|
||||
{ startMessage: `👷 Building Storybook`, errorMessage: `🚨 Storybook build failed` }
|
||||
);
|
||||
};
|
||||
|
||||
const serveStorybook = async ({ cwd }: Options, port: string) => {
|
||||
const staticDirectory = path.join(cwd, 'storybook-static');
|
||||
logger.info(`🌍 Serving ${staticDirectory} on http://localhost:${port}`);
|
||||
logger.info(`🌍 Serving ${staticDirectory} on http://localhost:${port}`);
|
||||
|
||||
return serve(staticDirectory, port);
|
||||
};
|
||||
|
||||
const runCypress = async ({ name }: Options, location: string, open: boolean) => {
|
||||
const cypressCommand = open ? 'open' : 'run';
|
||||
logger.info(`🤖 Running Cypress tests`);
|
||||
try {
|
||||
await exec(
|
||||
`yarn cypress ${cypressCommand} --config pageLoadTimeout=4000,execTimeout=4000,taskTimeout=4000,responseTimeout=4000,integrationFolder="cypress/generated" --env location="${location}"`,
|
||||
{ cwd: rootDir }
|
||||
);
|
||||
logger.info(`✅ E2E tests success`);
|
||||
logger.info(`🎉 Storybook is working great with ${name}!`);
|
||||
} catch (e) {
|
||||
logger.error(`🚨 E2E tests fails`);
|
||||
logger.info(`🥺 Storybook has some issues with ${name}!`);
|
||||
throw e;
|
||||
}
|
||||
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`,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const runTests = async ({ name, ...rest }: Parameters) => {
|
||||
@ -88,7 +73,7 @@ const runTests = async ({ name, ...rest }: Parameters) => {
|
||||
};
|
||||
|
||||
logger.log();
|
||||
logger.info(`🏃♀️ Starting for ${name}`);
|
||||
logger.info(`🏃️Starting for ${name}`);
|
||||
logger.log();
|
||||
logger.debug(options);
|
||||
logger.log();
|
||||
@ -113,8 +98,14 @@ const runTests = async ({ name, ...rest }: Parameters) => {
|
||||
}
|
||||
|
||||
const command = `${sbCLICommand} ${commandArgs.join(' ')}`;
|
||||
logger.debug(command);
|
||||
await exec(command, { cwd: siblingDir });
|
||||
await exec(
|
||||
command,
|
||||
{ cwd: siblingDir, silent: false },
|
||||
{
|
||||
startMessage: `👷 Bootstrapping ${options.framework} project`,
|
||||
errorMessage: `🚨 Unable to bootstrap project`,
|
||||
}
|
||||
);
|
||||
|
||||
await buildStorybook(options);
|
||||
logger.log();
|
||||
@ -123,120 +114,202 @@ const runTests = async ({ name, ...rest }: Parameters) => {
|
||||
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();
|
||||
await runCypress('http://localhost:4000');
|
||||
logger.info(`🎉 Storybook is working great with ${name}!`);
|
||||
} catch (e) {
|
||||
logger.info(`🥺 Storybook has some issues with ${name}!`);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Run tests!
|
||||
const runE2E = async (parameters: Parameters) => {
|
||||
const { name } = parameters;
|
||||
const cwd = path.join(siblingDir, `${name}`);
|
||||
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`);
|
||||
logger.info(`♻️ Starting with a clean slate, removing existing ${name} folder`);
|
||||
await cleanDirectory({ ...parameters, cwd });
|
||||
}
|
||||
}
|
||||
|
||||
return runTests(parameters)
|
||||
.then(async () => {
|
||||
if (!process.env.CI) {
|
||||
const { cleanup } = await prompt<{ cleanup: boolean }>({
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
/**
|
||||
* 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)
|
||||
.catch((e) => {
|
||||
logger.error(`🛑 an error occurred:\n${e}`);
|
||||
logger.log();
|
||||
logger.error(`🛑 an error occurred:`);
|
||||
logger.error(e);
|
||||
logger.log();
|
||||
process.exitCode = 1;
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
program.option('--clean', 'Clean up existing projects before running the tests', false);
|
||||
program.option('--pnp', 'Run tests using Yarn 2 PnP instead of Yarn 1 + npx', false);
|
||||
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
|
||||
);
|
||||
program.option(
|
||||
'--skip <value>',
|
||||
'Skip a framework, can be used multiple times "--skip angular@latest --skip preact"',
|
||||
(value, previous) => previous.concat([value]),
|
||||
[]
|
||||
);
|
||||
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);
|
||||
|
||||
const {
|
||||
pnp,
|
||||
useLocalSbCli,
|
||||
clean: startWithCleanSlate,
|
||||
args: frameworkArgs,
|
||||
skip: frameworksToSkip,
|
||||
}: {
|
||||
type ProgramOptions = {
|
||||
all?: boolean;
|
||||
pnp?: boolean;
|
||||
useLocalSbCli?: boolean;
|
||||
clean?: boolean;
|
||||
args?: string[];
|
||||
skip?: string[];
|
||||
} = program;
|
||||
};
|
||||
|
||||
const {
|
||||
all: shouldRunAllFrameworks,
|
||||
args: frameworkArgs,
|
||||
skip: frameworksToSkip,
|
||||
}: ProgramOptions = program;
|
||||
|
||||
let { pnp, useLocalSbCli, clean: startWithCleanSlate }: ProgramOptions = program;
|
||||
|
||||
const typedConfigs: { [key: string]: Parameters } = configs;
|
||||
const e2eConfigs: { [key: string]: Parameters } = {};
|
||||
let e2eConfigs: { [key: string]: Parameters } = {};
|
||||
|
||||
// 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);
|
||||
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];
|
||||
});
|
||||
} else {
|
||||
Object.values(typedConfigs).forEach((config) => {
|
||||
e2eConfigs[config.name] = config;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Remove frameworks listed with `--skip` arg
|
||||
frameworksToSkip.forEach((framework) => {
|
||||
delete e2eConfigs[framework];
|
||||
});
|
||||
|
||||
const perform = () => {
|
||||
const limit = pLimit(1);
|
||||
const perform = async (): Promise<Record<string, boolean>> => {
|
||||
await getConfig();
|
||||
const narrowedConfigs = Object.values(e2eConfigs);
|
||||
|
||||
const list = filterDataForCurrentCircleCINode(narrowedConfigs) as Parameters[];
|
||||
|
||||
logger.info(`📑 Will run E2E tests for:${list.map((c) => `${c.name}`).join(', ')}`);
|
||||
|
||||
return Promise.all(list.map((config) => limit(() => runE2E(config))));
|
||||
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;
|
||||
};
|
||||
|
||||
perform().then(() => {
|
||||
perform().then((e2eResult) => {
|
||||
logger.info(`🧮 E2E Results`);
|
||||
|
||||
Object.entries(e2eResult).forEach(([configName, result]) => {
|
||||
logger.info(`${configName}: ${result ? 'OK' : 'KO'}`);
|
||||
});
|
||||
|
||||
process.exit(process.exitCode || 0);
|
||||
});
|
||||
|
@ -1,19 +1,19 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"types": ["node", "jest"],
|
||||
"strict": false,
|
||||
"strictNullChecks": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"verdaccio": ["./typings.d.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["./**/*"]
|
||||
}
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"types": ["node", "jest"],
|
||||
"strict": false,
|
||||
"strictNullChecks": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"verdaccio": ["./typings.d.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["./**/*"]
|
||||
}
|
||||
|
2
scripts/typings.d.ts
vendored
2
scripts/typings.d.ts
vendored
@ -1 +1 @@
|
||||
declare module 'verdaccio';
|
||||
declare module 'verdaccio';
|
||||
|
12
yarn.lock
12
yarn.lock
@ -7206,7 +7206,7 @@ __metadata:
|
||||
"@types/lodash": ^4.14.167
|
||||
"@types/node": ^14.14.20
|
||||
"@types/node-cleanup": ^2.1.1
|
||||
"@types/prompts": ^2.0.9
|
||||
"@types/prompts": 2.0.11
|
||||
"@types/semver": ^7.3.4
|
||||
"@types/serve-static": ^1.13.8
|
||||
"@types/shelljs": ^0.8.7
|
||||
@ -7232,7 +7232,6 @@ __metadata:
|
||||
danger: ^10.6.2
|
||||
detect-port: ^1.3.0
|
||||
downlevel-dts: ^0.6.0
|
||||
enquirer: ^2.3.6
|
||||
enzyme: ^3.11.0
|
||||
enzyme-adapter-react-16: ^1.15.5
|
||||
eslint: ^7.17.0
|
||||
@ -8705,6 +8704,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/prompts@npm:2.0.11":
|
||||
version: 2.0.11
|
||||
resolution: "@types/prompts@npm:2.0.11"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: f0d1111156a4ea4ec80dac1f0354f53b94f79e2627ff092adc18218a4919f946f82e191cb3f58a2fa8fbc73d02fcaf5c07c633b59469f87d3f8cd334608669a1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/prompts@npm:^2.0.9":
|
||||
version: 2.4.0
|
||||
resolution: "@types/prompts@npm:2.4.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user